-
-
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 2 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,171 @@ | ||||||||||||||
package dnslistener | ||||||||||||||
|
||||||||||||||
import ( | ||||||||||||||
"errors" | ||||||||||||||
"fmt" | ||||||||||||||
"net" | ||||||||||||||
"sync/atomic" | ||||||||||||||
|
||||||||||||||
"github.com/miekg/dns" | ||||||||||||||
"github.com/safing/portmaster/base/log" | ||||||||||||||
"github.com/safing/portmaster/service/mgr" | ||||||||||||||
"github.com/safing/portmaster/service/network/netutils" | ||||||||||||||
"github.com/safing/portmaster/service/resolver" | ||||||||||||||
"github.com/varlink/go/varlink" | ||||||||||||||
) | ||||||||||||||
|
||||||||||||||
var ResolverInfo = resolver.ResolverInfo{ | ||||||||||||||
Name: "SystemdResolver", | ||||||||||||||
Type: "env", | ||||||||||||||
Source: "System", | ||||||||||||||
} | ||||||||||||||
vlabo marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||||||||
|
||||||||||||||
type DNSListener struct { | ||||||||||||||
instance instance | ||||||||||||||
mgr *mgr.Manager | ||||||||||||||
|
||||||||||||||
varlinkConn *varlink.Connection | ||||||||||||||
} | ||||||||||||||
|
||||||||||||||
func (dl *DNSListener) Manager() *mgr.Manager { | ||||||||||||||
return dl.mgr | ||||||||||||||
} | ||||||||||||||
|
||||||||||||||
func (dl *DNSListener) Start() error { | ||||||||||||||
var err error | ||||||||||||||
|
||||||||||||||
// Create the varlink connection with the systemd resolver. | ||||||||||||||
dl.varlinkConn, err = varlink.NewConnection(dl.mgr.Ctx(), "unix:/run/systemd/resolve/io.systemd.Resolve.Monitor") | ||||||||||||||
if err != nil { | ||||||||||||||
log.Errorf("dnslistener: failed to connect to systemd-resolver varlink service: %s", err) | ||||||||||||||
return nil | ||||||||||||||
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. Return error instead of In the Apply this diff to fix the issue: if err != nil {
log.Errorf("dnslistener: failed to connect to systemd-resolver varlink service: %s", err)
- return nil
+ return err
} 📝 Committable suggestion
Suggested change
|
||||||||||||||
} | ||||||||||||||
|
||||||||||||||
dl.mgr.Go("systemd-resolver-event-listener", func(w *mgr.WorkerCtx) error { | ||||||||||||||
// Subscribe to the dns query events | ||||||||||||||
receive, err := dl.varlinkConn.Send(dl.mgr.Ctx(), "io.systemd.Resolve.Monitor.SubscribeQueryResults", nil, varlink.More) | ||||||||||||||
if err != nil { | ||||||||||||||
if varlinkErr, ok := err.(*varlink.Error); ok { | ||||||||||||||
return fmt.Errorf("failed to issue Varlink call: %+v", varlinkErr.Parameters) | ||||||||||||||
} else { | ||||||||||||||
return fmt.Errorf("failed to issue Varlink call: %v", err) | ||||||||||||||
vlabo marked this conversation as resolved.
Show resolved
Hide resolved
vlabo marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||||||||
} | ||||||||||||||
} | ||||||||||||||
|
||||||||||||||
for { | ||||||||||||||
queryResult := QueryResult{} | ||||||||||||||
// Receive the next event from the resolver. | ||||||||||||||
flags, err := receive(w.Ctx(), &queryResult) | ||||||||||||||
if err != nil { | ||||||||||||||
if varlinkErr, ok := err.(*varlink.Error); ok { | ||||||||||||||
return fmt.Errorf("failed to receive Varlink reply: %+v", varlinkErr.Parameters) | ||||||||||||||
} else { | ||||||||||||||
return fmt.Errorf("failed to receive Varlink reply: %v", err) | ||||||||||||||
vlabo marked this conversation as resolved.
Show resolved
Hide resolved
vlabo marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||||||||
} | ||||||||||||||
} | ||||||||||||||
|
||||||||||||||
// Check if the reply indicates the end of the stream | ||||||||||||||
if flags&varlink.Continues == 0 { | ||||||||||||||
break | ||||||||||||||
} | ||||||||||||||
|
||||||||||||||
if queryResult.Rcode != nil { | ||||||||||||||
continue // Ignore DNS errors | ||||||||||||||
} | ||||||||||||||
|
||||||||||||||
dl.processAnswer(&queryResult) | ||||||||||||||
|
||||||||||||||
} | ||||||||||||||
|
||||||||||||||
return nil | ||||||||||||||
}) | ||||||||||||||
|
||||||||||||||
return nil | ||||||||||||||
} | ||||||||||||||
|
||||||||||||||
func (dl *DNSListener) 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 | ||||||||||||||
} | ||||||||||||||
} | ||||||||||||||
|
||||||||||||||
for _, ip := range ips { | ||||||||||||||
// Never save domain attributions for localhost IPs. | ||||||||||||||
if netutils.GetIPScope(ip) == netutils.HostLocal { | ||||||||||||||
continue | ||||||||||||||
} | ||||||||||||||
fqdn := dns.Fqdn(domain) | ||||||||||||||
|
||||||||||||||
// Create new record for this IP. | ||||||||||||||
record := resolver.ResolvedDomain{ | ||||||||||||||
Domain: fqdn, | ||||||||||||||
Resolver: &ResolverInfo, | ||||||||||||||
DNSRequestContext: &resolver.DNSRequestContext{}, | ||||||||||||||
Expires: 0, | ||||||||||||||
} | ||||||||||||||
|
||||||||||||||
for { | ||||||||||||||
nextDomain, isCNAME := cnames[domain] | ||||||||||||||
if !isCNAME { | ||||||||||||||
break | ||||||||||||||
} | ||||||||||||||
vlabo marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||||||||
|
||||||||||||||
record.CNAMEs = append(record.CNAMEs, nextDomain) | ||||||||||||||
domain = nextDomain | ||||||||||||||
} | ||||||||||||||
|
||||||||||||||
info := resolver.IPInfo{ | ||||||||||||||
IP: ip.String(), | ||||||||||||||
} | ||||||||||||||
|
||||||||||||||
// Add the new record to the resolved domains for this IP and scope. | ||||||||||||||
info.AddDomain(record) | ||||||||||||||
|
||||||||||||||
// Save if the record is new or has been updated. | ||||||||||||||
if err := info.Save(); err != nil { | ||||||||||||||
log.Errorf("nameserver: failed to save IP info record: %s", err) | ||||||||||||||
} | ||||||||||||||
} | ||||||||||||||
} | ||||||||||||||
|
||||||||||||||
func (dl *DNSListener) Stop() error { | ||||||||||||||
if dl.varlinkConn != nil { | ||||||||||||||
_ = dl.varlinkConn.Close() | ||||||||||||||
} | ||||||||||||||
return nil | ||||||||||||||
} | ||||||||||||||
|
||||||||||||||
var shimLoaded atomic.Bool | ||||||||||||||
|
||||||||||||||
func New(instance instance) (*DNSListener, error) { | ||||||||||||||
if !shimLoaded.CompareAndSwap(false, true) { | ||||||||||||||
return nil, errors.New("only one instance allowed") | ||||||||||||||
} | ||||||||||||||
m := mgr.New("DNSListener") | ||||||||||||||
module := &DNSListener{ | ||||||||||||||
mgr: m, | ||||||||||||||
instance: instance, | ||||||||||||||
} | ||||||||||||||
return module, nil | ||||||||||||||
} | ||||||||||||||
|
||||||||||||||
type instance interface{} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,79 @@ | ||
package dnslistener | ||
|
||
// List of struct that define the systemd-resolver varlink dns event protocol. | ||
|
||
type ResourceKey struct { | ||
Class int `json:"class"` | ||
Type int `json:"type"` | ||
Name string `json:"name"` | ||
} | ||
|
||
type ResourceRecord struct { | ||
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. Do we have a source where this came from? 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. The source is:
In varlink the service is required to provide a description of its functions. |
||
Key ResourceKey `json:"key"` | ||
Name *string `json:"name,omitempty"` | ||
Address *[]byte `json:"address,omitempty"` | ||
// Rest of the fields are not used. | ||
// Priority *int `json:"priority,omitempty"` | ||
// Weight *int `json:"weight,omitempty"` | ||
// Port *int `json:"port,omitempty"` | ||
// CPU *string `json:"cpu,omitempty"` | ||
// OS *string `json:"os,omitempty"` | ||
// Items *[]string `json:"items,omitempty"` | ||
// MName *string `json:"mname,omitempty"` | ||
// RName *string `json:"rname,omitempty"` | ||
// Serial *int `json:"serial,omitempty"` | ||
// Refresh *int `json:"refresh,omitempty"` | ||
// Expire *int `json:"expire,omitempty"` | ||
// Minimum *int `json:"minimum,omitempty"` | ||
// Exchange *string `json:"exchange,omitempty"` | ||
// Version *int `json:"version,omitempty"` | ||
// Size *int `json:"size,omitempty"` | ||
// HorizPre *int `json:"horiz_pre,omitempty"` | ||
// VertPre *int `json:"vert_pre,omitempty"` | ||
// Latitude *int `json:"latitude,omitempty"` | ||
// Longitude *int `json:"longitude,omitempty"` | ||
// Altitude *int `json:"altitude,omitempty"` | ||
// KeyTag *int `json:"key_tag,omitempty"` | ||
// Algorithm *int `json:"algorithm,omitempty"` | ||
// DigestType *int `json:"digest_type,omitempty"` | ||
// Digest *string `json:"digest,omitempty"` | ||
// FPType *int `json:"fptype,omitempty"` | ||
// Fingerprint *string `json:"fingerprint,omitempty"` | ||
// Flags *int `json:"flags,omitempty"` | ||
// Protocol *int `json:"protocol,omitempty"` | ||
// DNSKey *string `json:"dnskey,omitempty"` | ||
// Signer *string `json:"signer,omitempty"` | ||
// TypeCovered *int `json:"type_covered,omitempty"` | ||
// Labels *int `json:"labels,omitempty"` | ||
// OriginalTTL *int `json:"original_ttl,omitempty"` | ||
// Expiration *int `json:"expiration,omitempty"` | ||
// Inception *int `json:"inception,omitempty"` | ||
// Signature *string `json:"signature,omitempty"` | ||
// NextDomain *string `json:"next_domain,omitempty"` | ||
// Types *[]int `json:"types,omitempty"` | ||
// Iterations *int `json:"iterations,omitempty"` | ||
// Salt *string `json:"salt,omitempty"` | ||
// Hash *string `json:"hash,omitempty"` | ||
// CertUsage *int `json:"cert_usage,omitempty"` | ||
// Selector *int `json:"selector,omitempty"` | ||
// MatchingType *int `json:"matching_type,omitempty"` | ||
// Data *string `json:"data,omitempty"` | ||
// Tag *string `json:"tag,omitempty"` | ||
// Value *string `json:"value,omitempty"` | ||
} | ||
vlabo marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
type Answer struct { | ||
RR *ResourceRecord `json:"rr,omitempty"` | ||
Raw string `json:"raw"` | ||
IfIndex *int `json:"ifindex,omitempty"` | ||
} | ||
|
||
type QueryResult struct { | ||
Ready *bool `json:"ready,omitempty"` | ||
State *string `json:"state,omitempty"` | ||
Rcode *int `json:"rcode,omitempty"` | ||
Errno *int `json:"errno,omitempty"` | ||
Question *[]ResourceKey `json:"question,omitempty"` | ||
CollectedQuestions *[]ResourceKey `json:"collectedQuestions,omitempty"` | ||
Answer *[]Answer `json:"answer,omitempty"` | ||
} |
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