Skip to content

Commit

Permalink
[service] Subscribe to systemd-resolver events
Browse files Browse the repository at this point in the history
  • Loading branch information
vlabo committed Nov 4, 2024
1 parent 145f5e6 commit 9d26cd2
Show file tree
Hide file tree
Showing 3 changed files with 262 additions and 0 deletions.
171 changes: 171 additions & 0 deletions service/firewall/interception/dnslistener/module.go
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)
}
}

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)
}
}

// 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{}
79 changes: 79 additions & 0 deletions service/firewall/interception/dnslistener/varlinktypes.go
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"`
}

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"`
}
12 changes: 12 additions & 0 deletions service/instance.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import (
"github.com/safing/portmaster/service/core/base"
"github.com/safing/portmaster/service/firewall"
"github.com/safing/portmaster/service/firewall/interception"
"github.com/safing/portmaster/service/firewall/interception/dnslistener"
"github.com/safing/portmaster/service/intel/customlists"
"github.com/safing/portmaster/service/intel/filterlists"
"github.com/safing/portmaster/service/intel/geoip"
Expand Down Expand Up @@ -74,6 +75,7 @@ type Instance struct {
firewall *firewall.Firewall
filterLists *filterlists.FilterLists
interception *interception.Interception
dnslistener *dnslistener.DNSListener
customlist *customlists.CustomList
status *status.Status
broadcasts *broadcasts.Broadcasts
Expand Down Expand Up @@ -187,6 +189,10 @@ func New(svcCfg *ServiceConfig) (*Instance, error) { //nolint:maintidx
if err != nil {
return instance, fmt.Errorf("create interception module: %w", err)
}
instance.dnslistener, err = dnslistener.New(instance)
if err != nil {
return instance, fmt.Errorf("create dns-listener module: %w", err)
}
instance.customlist, err = customlists.New(instance)
if err != nil {
return instance, fmt.Errorf("create customlist module: %w", err)
Expand Down Expand Up @@ -288,6 +294,7 @@ func New(svcCfg *ServiceConfig) (*Instance, error) { //nolint:maintidx
instance.filterLists,
instance.customlist,
instance.interception,
instance.dnslistener,

instance.compat,
instance.status,
Expand Down Expand Up @@ -463,6 +470,11 @@ func (i *Instance) Interception() *interception.Interception {
return i.interception
}

// DNSListener returns the dns-listener module.
func (i *Instance) DNSListener() *dnslistener.DNSListener {
return i.dnslistener
}

// CustomList returns the customlist module.
func (i *Instance) CustomList() *customlists.CustomList {
return i.customlist
Expand Down

0 comments on commit 9d26cd2

Please sign in to comment.