-
-
Notifications
You must be signed in to change notification settings - Fork 305
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
base: develop
Are you sure you want to change the base?
Changes from all commits
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 | ||||
---|---|---|---|---|---|---|
@@ -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", | ||||||
} | ||||||
|
||||||
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 | ||||||
} | ||||||
|
||||||
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) | ||||||
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. 🛠️ Refactor suggestion Wrap errors using When returning errors with Apply this diff to refactor: -return fmt.Errorf("failed to issue Varlink call: %v", err)
+return fmt.Errorf("failed to issue Varlink call: %w", err) 📝 Committable suggestion
Suggested change
🧰 Tools🪛 GitHub Check: Linter[failure] 51-51:
Comment on lines
+48
to
+51
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. Use Type assertions on errors may fail on wrapped errors. Use Apply this diff to fix the issue: -if varlinkErr, ok := err.(*varlink.Error); ok {
+var varlinkErr *varlink.Error
+if errors.As(err, &varlinkErr) {
🧰 Tools🪛 GitHub Check: Linter[failure] 48-48: [failure] 51-51: |
||||||
} | ||||||
} | ||||||
|
||||||
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) | ||||||
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. 🛠️ Refactor suggestion Wrap errors using Again, use Apply this diff to refactor: -return fmt.Errorf("failed to receive Varlink reply: %v", err)
+return fmt.Errorf("failed to receive Varlink reply: %w", err) 📝 Committable suggestion
Suggested change
🧰 Tools🪛 GitHub Check: Linter[failure] 63-63:
Comment on lines
+60
to
+63
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. Use Similar to the previous case, use Apply this diff to fix the issue: -if varlinkErr, ok := err.(*varlink.Error); ok {
+var varlinkErr *varlink.Error
+if errors.As(err, &varlinkErr) {
🧰 Tools🪛 GitHub Check: Linter[failure] 60-60: [failure] 63-63: |
||||||
} | ||||||
} | ||||||
|
||||||
// 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 | ||||||
} | ||||||
|
||||||
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 { | ||
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"` | ||
} | ||
Comment on lines
+11
to
+63
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. 🛠️ Refactor suggestion Improve documentation and consider separating unused fields. The current approach of commenting out unused fields makes the code harder to maintain and understand. Consider either:
+// ResourceRecord represents a DNS resource record with its associated data.
+// Currently, only a subset of DNS record types are supported, focusing on
+// name resolution and addressing. Additional fields for other DNS record
+// types are planned for future implementation.
type ResourceRecord struct {
Key ResourceKey `json:"key"`
+ // Name is the canonical name for CNAME records
Name *string `json:"name,omitempty"`
+ // Address contains the IP address for A/AAAA records
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"`
- // ... (other commented fields)
} Create a new file (e.g., // future_types.go
// FutureResourceRecord contains fields for additional DNS record types
// that may be implemented in the future.
type FutureResourceRecord struct {
Priority *int `json:"priority,omitempty"`
Weight *int `json:"weight,omitempty"`
Port *int `json:"port,omitempty"`
// ... (other fields)
} |
||
|
||
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.
Return error instead of
nil
on connection failureIn the
Start()
method, when failing to establish a varlink connection, the error is logged butnil
is returned. Consider returning the error to allow the caller to handle it appropriately.Apply this diff to fix the issue:
📝 Committable suggestion