-
-
Notifications
You must be signed in to change notification settings - Fork 307
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Feature/systemd query events #1728
Changes from 5 commits
9d26cd2
811a3d9
194eac2
b3b1615
8345c17
a873a01
bf6d8b5
1a6237b
f189adc
6137358
5019a12
7f2b8fc
1b4f5f1
dff2f34
eda62f4
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -43,6 +43,18 @@ func PreventBypassing(ctx context.Context, conn *network.Connection) (endpoints. | |
return endpoints.NoMatch, "", nil | ||
} | ||
|
||
// If Portmaster resolver is disabled allow requests going to system dns resolver. | ||
// And allow all connections out of the System Resolver. | ||
if module.instance.Resolver().IsDisabled.IsSet() { | ||
// TODO(vladimir): Is there a more specific check that can be done? | ||
if conn.Process().IsSystemResolver() { | ||
return endpoints.NoMatch, "", nil | ||
} | ||
if conn.Entity.Port == 53 && conn.Entity.IPScope.IsLocalhost() { | ||
return endpoints.NoMatch, "", nil | ||
} | ||
Comment on lines
+53
to
+55
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think we can allow any normal DNS, as we can see the packets. We only need to prevent encrypted DNS connections from apps. This is getting complicated. Maybe we can simplify and regroup the logic here? Let's talk about options. |
||
} | ||
|
||
// Block bypass attempts using an (encrypted) DNS server. | ||
switch { | ||
case conn.Entity.Port == 53: | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,98 @@ | ||
package dnslistener | ||
|
||
import ( | ||
"fmt" | ||
"runtime" | ||
"sync" | ||
"sync/atomic" | ||
|
||
"github.com/safing/portmaster/service/integration" | ||
"golang.org/x/sys/windows" | ||
) | ||
|
||
type ETWSession struct { | ||
i integration.ETWFunctions | ||
|
||
shutdownGuard atomic.Bool | ||
shutdownMutex sync.Mutex | ||
|
||
state uintptr | ||
} | ||
|
||
// NewSession creates new ETW event listener and initilizes it. This is a low level interface, make sure to call DestorySession when you are done using it. | ||
func NewSession(etwInterface integration.ETWFunctions, callback func(domain string, result string)) (*ETWSession, error) { | ||
etwSession := &ETWSession{ | ||
i: etwInterface, | ||
} | ||
|
||
// Make sure session from previous instances are not running. | ||
_ = etwSession.i.StopOldSession() | ||
|
||
// Initialize notification activated callback | ||
win32Callback := windows.NewCallback(func(domain *uint16, result *uint16) uintptr { | ||
callback(windows.UTF16PtrToString(domain), windows.UTF16PtrToString(result)) | ||
return 0 | ||
}) | ||
vlabo marked this conversation as resolved.
Show resolved
Hide resolved
|
||
// The function only allocates memory it will not fail. | ||
etwSession.state = etwSession.i.CreateState(win32Callback) | ||
|
||
// Make sure DestroySession is called even if caller forgets to call it. | ||
runtime.SetFinalizer(etwSession, func(s *ETWSession) { | ||
_ = s.i.DestroySession(s.state) | ||
}) | ||
|
||
// Initialize session. | ||
err := etwSession.i.InitializeSession(etwSession.state) | ||
if err != nil { | ||
return nil, fmt.Errorf("failed to initialzie session: %q", err) | ||
} | ||
|
||
return etwSession, nil | ||
} | ||
|
||
// StartTrace starts the tracing session of dns events. This is a blocking call. It will not return until the trace is stopped. | ||
func (l *ETWSession) StartTrace() error { | ||
return l.i.StartTrace(l.state) | ||
} | ||
|
||
// IsRunning returns true if DestroySession has NOT been called. | ||
func (l *ETWSession) IsRunning() bool { | ||
return !l.shutdownGuard.Load() | ||
} | ||
|
||
// FlushTrace flushes the trace buffer. | ||
func (l *ETWSession) FlushTrace() error { | ||
l.shutdownMutex.Lock() | ||
defer l.shutdownMutex.Unlock() | ||
|
||
// Make sure session is still running. | ||
if l.shutdownGuard.Load() { | ||
return nil | ||
} | ||
|
||
return l.i.FlushTrace(l.state) | ||
} | ||
|
||
// StopTrace stopes the trace. This will cause StartTrace to return. | ||
func (l *ETWSession) StopTrace() error { | ||
return l.i.StopTrace(l.state) | ||
} | ||
|
||
// DestroySession closes the session and frees the allocated memory. Listener cannot be used after this function is called. | ||
func (l *ETWSession) DestroySession() error { | ||
if l.shutdownGuard.Load() { | ||
return nil | ||
} | ||
|
||
l.shutdownMutex.Lock() | ||
defer l.shutdownMutex.Unlock() | ||
|
||
l.shutdownGuard.Store(true) | ||
vlabo marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
err := l.i.DestroySession(l.state) | ||
if err != nil { | ||
return err | ||
} | ||
l.state = 0 | ||
return nil | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,19 @@ | ||
//go:build !linux && !windows | ||
// +build !linux,!windows | ||
|
||
package dnslistener | ||
|
||
type Listener struct{} | ||
|
||
func newListener(module *DNSListener) (*Listener, error) { | ||
return &Listener{}, nil | ||
} | ||
|
||
func (l *Listener) flush() error { | ||
// Nothing to flush | ||
return nil | ||
} | ||
|
||
func (l *Listener) stop() error { | ||
return nil | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,107 @@ | ||
//go:build linux | ||
// +build linux | ||
|
||
package dnslistener | ||
|
||
import ( | ||
"errors" | ||
"fmt" | ||
"net" | ||
|
||
"github.com/safing/portmaster/service/mgr" | ||
"github.com/varlink/go/varlink" | ||
) | ||
|
||
type Listener struct { | ||
varlinkConn *varlink.Connection | ||
} | ||
|
||
func newListener(module *DNSListener) (*Listener, error) { | ||
// Create the varlink connection with the systemd resolver. | ||
varlinkConn, err := varlink.NewConnection(module.mgr.Ctx(), "unix:/run/systemd/resolve/io.systemd.Resolve.Monitor") | ||
if err != nil { | ||
return nil, fmt.Errorf("dnslistener: failed to connect to systemd-resolver varlink service: %w", err) | ||
} | ||
|
||
listener := &Listener{varlinkConn: varlinkConn} | ||
|
||
module.mgr.Go("systemd-resolver-event-listener", func(w *mgr.WorkerCtx) error { | ||
vlabo marked this conversation as resolved.
Show resolved
Hide resolved
|
||
// Subscribe to the dns query events | ||
receive, err := listener.varlinkConn.Send(w.Ctx(), "io.systemd.Resolve.Monitor.SubscribeQueryResults", nil, varlink.More) | ||
if err != nil { | ||
var varlinkErr *varlink.Error | ||
if errors.As(err, &varlinkErr) { | ||
return fmt.Errorf("failed to issue Varlink call: %+v", varlinkErr.Parameters) | ||
} else { | ||
return fmt.Errorf("failed to issue Varlink call: %w", err) | ||
} | ||
} | ||
|
||
for { | ||
queryResult := QueryResult{} | ||
vlabo marked this conversation as resolved.
Show resolved
Hide resolved
|
||
// Receive the next event from the resolver. | ||
flags, err := receive(w.Ctx(), &queryResult) | ||
if err != nil { | ||
var varlinkErr *varlink.Error | ||
if errors.As(err, &varlinkErr) { | ||
return fmt.Errorf("failed to receive Varlink reply: %+v", varlinkErr.Parameters) | ||
} else { | ||
return fmt.Errorf("failed to receive Varlink reply: %w", err) | ||
} | ||
} | ||
|
||
// Check if the reply indicates the end of the stream | ||
if flags&varlink.Continues == 0 { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If the flag is set, shouldn't the value be There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't like the way gofmt formats this. |
||
break | ||
} | ||
|
||
if queryResult.Rcode != nil { | ||
continue // Ignore DNS errors | ||
} | ||
|
||
listener.processAnswer(&queryResult) | ||
} | ||
return nil | ||
}) | ||
return listener, nil | ||
} | ||
vlabo marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
func (l *Listener) flush() error { | ||
// Nothing to flush | ||
return nil | ||
} | ||
|
||
func (l *Listener) stop() error { | ||
if l.varlinkConn != nil { | ||
return l.varlinkConn.Close() | ||
} | ||
return nil | ||
} | ||
|
||
func (l *Listener) processAnswer(queryResult *QueryResult) { | ||
// Allocated data struct for the parsed result. | ||
cnames := make(map[string]string) | ||
ips := make([]net.IP, 0, 5) | ||
|
||
// Check if the query is valid | ||
if queryResult.Question == nil || len(*queryResult.Question) == 0 || queryResult.Answer == nil { | ||
return | ||
} | ||
|
||
domain := (*queryResult.Question)[0].Name | ||
|
||
// Go trough each answer entry. | ||
for _, a := range *queryResult.Answer { | ||
if a.RR.Address != nil { | ||
ip := net.IP(*a.RR.Address) | ||
// Answer contains ip address. | ||
ips = append(ips, ip) | ||
|
||
} else if a.RR.Name != nil { | ||
// Answer is a CNAME. | ||
cnames[domain] = *a.RR.Name | ||
} | ||
} | ||
|
||
saveDomain(domain, ips, cnames) | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
💡 Codebase verification
Missing Resolver() implementation in spn/instance.go
The verification reveals that
spn/instance.go
implementsNetEnv()
but lacks the newly addedResolver()
method. This implementation needs to be updated to maintain interface compatibility.spn/instance.go
: Add the missingResolver()
method to match the interface requirementsservice/instance.go
and test files are correctly updated🔗 Analysis chain
Verify interface implementation across the codebase.
The addition of
Resolver()
method to the interface looks good, but we should verify that all types implementing this interface have been updated.The previous attempt might have failed due to complex AST pattern. Let's try using ripgrep to:
The previous output shows we have potentially found an issue - the
spn/instance.go
implementation hasNetEnv()
but might be missingResolver()
. Let's verify the complete interface definition and check the full implementation in spn/instance.go to confirm this.🏁 Scripts executed
The following scripts were executed for the analysis:
Script:
Length of output: 95
Script:
Length of output: 695
Script:
Length of output: 649