Feature/systemd query events (#1728)

* [service] Subscribe to systemd-resolver events

* [service] Add disabled state to the resolver

* [service] Add ETW DNS event listener

* [service] DNS listener refactoring

* [service] Add windows core dll project

* [service] DNSListener refactoring, small bugfixes

* [service] Change dns bypass rule

* [service] Update gitignore

* [service] Remove shim from integration module

* [service] Add DNS packet analyzer

* [service] Add self-check in dns monitor

* [service] Fix go linter errors

* [CI] Add github workflow for the windows core dll

* [service] Minor fixes to the dns monitor
This commit is contained in:
Vladimir Stoilov
2024-11-27 17:10:47 +02:00
committed by GitHub
parent 943b9b7859
commit 1a1bc14804
41 changed files with 1668 additions and 51 deletions

View File

@@ -181,4 +181,5 @@ func New(instance instance) (*Compat, error) {
type instance interface {
NetEnv() *netenv.NetEnv
Resolver() *resolver.ResolverModule
}

View File

@@ -158,6 +158,12 @@ func selfcheck(ctx context.Context) (issue *systemIssue, err error) {
// Step 3: Have the nameserver respond with random data in the answer section.
// Check if the resolver is enabled
if module.instance.Resolver().IsDisabled() {
// There is no control over the response, there is nothing more that can be checked.
return nil, nil
}
// Wait for the reply from the resolver.
select {
case err := <-dnsCheckLookupError:

View File

@@ -43,8 +43,24 @@ 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() {
// 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
}
}
// Block bypass attempts using an (encrypted) DNS server.
switch {
case looksLikeOutgoingDNSRequest(conn) && module.instance.Resolver().IsDisabled():
// Allow. Packet will be analyzed and blocked if its not a dns request, before sent.
conn.Inspecting = true
return endpoints.NoMatch, "", nil
case conn.Entity.Port == 53:
return endpoints.Denied,
"blocked DNS query, manual dns setup required",
@@ -62,3 +78,17 @@ func PreventBypassing(ctx context.Context, conn *network.Connection) (endpoints.
return endpoints.NoMatch, "", nil
}
func looksLikeOutgoingDNSRequest(conn *network.Connection) bool {
// Outbound on remote port 53, UDP.
if conn.Inbound {
return false
}
if conn.Entity.Port != 53 {
return false
}
if conn.IPProtocol != packet.UDP {
return false
}
return true
}

View File

@@ -287,6 +287,30 @@ func UpdateIPsAndCNAMEs(q *resolver.Query, rrCache *resolver.RRCache, conn *netw
}
}
// Create new record for this IP.
record := resolver.ResolvedDomain{
Domain: q.FQDN,
Resolver: rrCache.Resolver,
DNSRequestContext: rrCache.ToDNSRequestContext(),
Expires: rrCache.Expires,
}
// Process CNAMEs
record.AddCNAMEs(cnames)
// Link connection with cnames.
if conn.Type == network.DNSRequest {
conn.Entity.CNAME = record.CNAMEs
}
SaveIPsInCache(ips, profileID, record)
}
// formatRR is a friendlier alternative to miekg/dns.RR.String().
func formatRR(rr dns.RR) string {
return strings.ReplaceAll(rr.String(), "\t", " ")
}
// SaveIPsInCache saves the provided ips in the dns cashe assoseted with the record Domain and CNAMEs.
func SaveIPsInCache(ips []net.IP, profileID string, record resolver.ResolvedDomain) {
// Package IPs and CNAMEs into IPInfo structs.
for _, ip := range ips {
// Never save domain attributions for localhost IPs.
@@ -294,31 +318,6 @@ func UpdateIPsAndCNAMEs(q *resolver.Query, rrCache *resolver.RRCache, conn *netw
continue
}
// Create new record for this IP.
record := resolver.ResolvedDomain{
Domain: q.FQDN,
Resolver: rrCache.Resolver,
DNSRequestContext: rrCache.ToDNSRequestContext(),
Expires: rrCache.Expires,
}
// Resolve all CNAMEs in the correct order and add the to the record - up to max 50 layers.
domain := q.FQDN
for range 50 {
nextDomain, isCNAME := cnames[domain]
if !isCNAME || nextDomain == domain {
break
}
record.CNAMEs = append(record.CNAMEs, nextDomain)
domain = nextDomain
}
// Update the entity to include the CNAMEs of the query response.
conn.Entity.CNAME = record.CNAMEs
// Check if there is an existing record for this DNS response.
// Else create a new one.
ipString := ip.String()
info, err := resolver.GetIPInfo(profileID, ipString)
if err != nil {
@@ -341,8 +340,3 @@ func UpdateIPsAndCNAMEs(q *resolver.Query, rrCache *resolver.RRCache, conn *netw
}
}
}
// formatRR is a friendlier alternative to miekg/dns.RR.String().
func formatRR(rr dns.RR) string {
return strings.ReplaceAll(rr.String(), "\t", " ")
}

View File

@@ -0,0 +1,99 @@
//go:build windows
// +build windows
package dnsmonitor
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
})
// 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 {
l.shutdownMutex.Lock()
defer l.shutdownMutex.Unlock()
if l.shutdownGuard.Swap(true) {
return nil
}
err := l.i.DestroySession(l.state)
if err != nil {
return err
}
l.state = 0
return nil
}

View File

@@ -0,0 +1,19 @@
//go:build !linux && !windows
// +build !linux,!windows
package dnsmonitor
type Listener struct{}
func newListener(_ *DNSMonitor) (*Listener, error) {
return &Listener{}, nil
}
func (l *Listener) flush() error {
// Nothing to flush
return nil
}
func (l *Listener) stop() error {
return nil
}

View File

@@ -0,0 +1,144 @@
//go:build linux
// +build linux
package dnsmonitor
import (
"errors"
"fmt"
"net"
"os"
"github.com/miekg/dns"
"github.com/safing/portmaster/base/log"
"github.com/safing/portmaster/service/mgr"
"github.com/safing/portmaster/service/resolver"
"github.com/varlink/go/varlink"
)
type Listener struct {
varlinkConn *varlink.Connection
}
func newListener(module *DNSMonitor) (*Listener, error) {
// Set source of the resolver.
ResolverInfo.Source = resolver.ServerSourceSystemd
// Check if the system has systemd-resolver.
_, err := os.Stat("/run/systemd/resolve/io.systemd.Resolve.Monitor")
if err != nil {
return nil, fmt.Errorf("system does not support systemd resolver monitor")
}
listener := &Listener{}
restartAttempts := 0
module.mgr.Go("systemd-resolver-event-listener", func(w *mgr.WorkerCtx) error {
// Abort initialization if the connection failed after too many tries.
if restartAttempts > 10 {
return nil
}
restartAttempts += 1
// Initialize varlink connection
varlinkConn, err := varlink.NewConnection(module.mgr.Ctx(), "unix:/run/systemd/resolve/io.systemd.Resolve.Monitor")
if err != nil {
return fmt.Errorf("failed to connect to systemd-resolver varlink service: %w", err)
}
defer func() {
if varlinkConn != nil {
err = varlinkConn.Close()
if err != nil {
log.Errorf("dnsmonitor: failed to close varlink connection: %s", err)
}
}
}()
listener.varlinkConn = varlinkConn
// 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{}
// 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 {
break
}
// Ignore if there is no question.
if queryResult.Question == nil || len(*queryResult.Question) == 0 {
continue
}
// Protmaster self check
domain := (*queryResult.Question)[0].Name
if processIfSelfCheckDomain(dns.Fqdn(domain)) {
// Not need to process result.
continue
}
if queryResult.Rcode != nil {
continue // Ignore DNS errors
}
listener.processAnswer(domain, &queryResult)
}
return nil
})
return listener, nil
}
func (l *Listener) flush() error {
// Nothing to flush
return nil
}
func (l *Listener) stop() error {
return nil
}
func (l *Listener) processAnswer(domain string, 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.Answer == nil {
return
}
// 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)
}

View File

@@ -0,0 +1,103 @@
//go:build windows
// +build windows
package dnsmonitor
import (
"fmt"
"net"
"strconv"
"strings"
"github.com/miekg/dns"
"github.com/safing/portmaster/service/mgr"
"github.com/safing/portmaster/service/resolver"
)
type Listener struct {
etw *ETWSession
}
func newListener(module *DNSMonitor) (*Listener, error) {
// Set source of the resolver.
ResolverInfo.Source = resolver.ServerSourceETW
listener := &Listener{}
var err error
// Initialize new dns event session.
listener.etw, err = NewSession(module.instance.OSIntegration().GetETWInterface(), listener.processEvent)
if err != nil {
return nil, err
}
// Start listening for events.
module.mgr.Go("etw-dns-event-listener", func(w *mgr.WorkerCtx) error {
return listener.etw.StartTrace()
})
return listener, nil
}
func (l *Listener) flush() error {
return l.etw.FlushTrace()
}
func (l *Listener) stop() error {
if l == nil {
return fmt.Errorf("listener is nil")
}
if l.etw == nil {
return fmt.Errorf("invalid etw session")
}
// Stop and destroy trace. Destroy should be called even if stop fails for some reason.
err := l.etw.StopTrace()
err2 := l.etw.DestroySession()
if err != nil {
return fmt.Errorf("StopTrace failed: %w", err)
}
if err2 != nil {
return fmt.Errorf("DestroySession failed: %w", err2)
}
return nil
}
func (l *Listener) processEvent(domain string, result string) {
if processIfSelfCheckDomain(dns.Fqdn(domain)) {
// Not need to process result.
return
}
// Ignore empty results
if len(result) == 0 {
return
}
cnames := make(map[string]string)
ips := []net.IP{}
resultArray := strings.Split(result, ";")
for _, r := range resultArray {
// For results other than IP addresses, the string starts with "type:"
if strings.HasPrefix(r, "type:") {
dnsValueArray := strings.Split(r, " ")
if len(dnsValueArray) < 3 {
continue
}
// Ignore everything except CNAME records
if value, err := strconv.ParseInt(dnsValueArray[1], 10, 16); err == nil && value == int64(dns.TypeCNAME) {
cnames[domain] = dnsValueArray[2]
}
} else {
// If the event doesn't start with "type:", it's an IP address
ip := net.ParseIP(r)
if ip != nil {
ips = append(ips, ip)
}
}
}
saveDomain(domain, ips, cnames)
}

View File

@@ -0,0 +1,138 @@
package dnsmonitor
import (
"errors"
"net"
"strings"
"github.com/miekg/dns"
"github.com/safing/portmaster/base/database"
"github.com/safing/portmaster/base/log"
"github.com/safing/portmaster/service/compat"
"github.com/safing/portmaster/service/integration"
"github.com/safing/portmaster/service/mgr"
"github.com/safing/portmaster/service/network/netutils"
"github.com/safing/portmaster/service/resolver"
)
var ResolverInfo = resolver.ResolverInfo{
Name: "SystemResolver",
Type: resolver.ServerTypeMonitor,
}
type DNSMonitor struct {
instance instance
mgr *mgr.Manager
listener *Listener
}
// Manager returns the module manager.
func (dl *DNSMonitor) Manager() *mgr.Manager {
return dl.mgr
}
// Start starts the module.
func (dl *DNSMonitor) Start() error {
// Initialize dns event listener
var err error
dl.listener, err = newListener(dl)
if err != nil {
log.Errorf("dnsmonitor: failed to start dns listener: %s", err)
}
return nil
}
// Stop stops the module.
func (dl *DNSMonitor) Stop() error {
if dl.listener != nil {
err := dl.listener.stop()
if err != nil {
log.Errorf("dnsmonitor: failed to close listener: %s", err)
}
}
return nil
}
// Flush flushes the buffer forcing all events to be processed.
func (dl *DNSMonitor) Flush() error {
return dl.listener.flush()
}
func saveDomain(domain string, ips []net.IP, cnames map[string]string) {
fqdn := dns.Fqdn(domain)
// Create new record for this IP.
record := resolver.ResolvedDomain{
Domain: fqdn,
Resolver: &ResolverInfo,
DNSRequestContext: &resolver.DNSRequestContext{},
Expires: 0,
}
// Process cnames
record.AddCNAMEs(cnames)
// Add to cache
saveIPsInCache(ips, resolver.IPInfoProfileScopeGlobal, record)
}
func New(instance instance) (*DNSMonitor, error) {
// Initialize module
m := mgr.New("DNSMonitor")
module := &DNSMonitor{
mgr: m,
instance: instance,
}
return module, nil
}
type instance interface {
OSIntegration() *integration.OSIntegration
}
func processIfSelfCheckDomain(fqdn string) bool {
// Check for compat check dns request.
if strings.HasSuffix(fqdn, compat.DNSCheckInternalDomainScope) {
subdomain := strings.TrimSuffix(fqdn, compat.DNSCheckInternalDomainScope)
_ = compat.SubmitDNSCheckDomain(subdomain)
log.Infof("dnsmonitor: self-check domain received")
// No need to parse the answer.
return true
}
return false
}
// saveIPsInCache saves the provided ips in the dns cashe assoseted with the record Domain and CNAMEs.
func saveIPsInCache(ips []net.IP, profileID string, record resolver.ResolvedDomain) {
// Package IPs and CNAMEs into IPInfo structs.
for _, ip := range ips {
// Never save domain attributions for localhost IPs.
if netutils.GetIPScope(ip) == netutils.HostLocal {
continue
}
ipString := ip.String()
info, err := resolver.GetIPInfo(profileID, ipString)
if err != nil {
if !errors.Is(err, database.ErrNotFound) {
log.Errorf("dnsmonitor: failed to search for IP info record: %s", err)
}
info = &resolver.IPInfo{
IP: ipString,
ProfileID: profileID,
}
}
// 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("dnsmonitor: failed to save IP info record: %s", err)
}
}
}

View File

@@ -0,0 +1,83 @@
//go:build linux
// +build linux
package dnsmonitor
// List of struct that define the systemd-resolver varlink dns event protocol.
// Source: `sudo varlinkctl introspect /run/systemd/resolve/io.systemd.Resolve.Monitor io.systemd.Resolve.Monitor`
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"`
}

View File

@@ -188,7 +188,7 @@ func (q *Queue) packetHandler(ctx context.Context) func(nfqueue.Attribute) int {
return 0
}
if err := pmpacket.Parse(*attrs.Payload, &pkt.Base); err != nil {
if err := pmpacket.ParseLayer3(*attrs.Payload, &pkt.Base); err != nil {
log.Warningf("nfqueue: failed to parse payload: %s", err)
_ = pkt.Drop()
return 0

View File

@@ -59,7 +59,7 @@ func (pkt *Packet) LoadPacketData() error {
return packet.ErrFailedToLoadPayload
}
err = packet.Parse(payload, &pkt.Base)
err = packet.ParseLayer3(payload, &pkt.Base)
if err != nil {
log.Tracer(pkt.Ctx()).Warningf("windowskext: failed to parse payload: %s", err)
return packet.ErrFailedToLoadPayload

View File

@@ -55,6 +55,7 @@ func Handler(ctx context.Context, packets chan packet.Packet, bandwidthUpdate ch
newPacket := &Packet{
verdictRequest: conn.ID,
payload: conn.Payload,
payloadLayer: conn.PayloadLayer,
verdictSet: abool.NewBool(false),
}
info := newPacket.Info()

View File

@@ -4,6 +4,7 @@
package windowskext
import (
"fmt"
"sync"
"github.com/tevino/abool"
@@ -19,6 +20,7 @@ type Packet struct {
verdictRequest uint64
payload []byte
payloadLayer uint8
verdictSet *abool.AtomicBool
payloadLoaded bool
@@ -51,7 +53,15 @@ func (pkt *Packet) LoadPacketData() error {
pkt.payloadLoaded = true
if len(pkt.payload) > 0 {
err := packet.Parse(pkt.payload, &pkt.Base)
var err error
switch pkt.payloadLayer {
case 3:
err = packet.ParseLayer3(pkt.payload, &pkt.Base)
case 4:
err = packet.ParseLayer4(pkt.payload, &pkt.Base)
default:
err = fmt.Errorf("unsupported payload layer: %d", pkt.payloadLayer)
}
if err != nil {
log.Tracef("payload: %#v", pkt.payload)
log.Tracer(pkt.Ctx()).Warningf("windowskext: failed to parse payload: %s", err)

View File

@@ -16,6 +16,7 @@ import (
"github.com/safing/portmaster/service/netquery"
"github.com/safing/portmaster/service/network"
"github.com/safing/portmaster/service/profile"
"github.com/safing/portmaster/service/resolver"
"github.com/safing/portmaster/spn/access"
"github.com/safing/portmaster/spn/captain"
)
@@ -34,8 +35,7 @@ func (ss *stringSliceFlag) Set(value string) error {
var allowedClients stringSliceFlag
type Firewall struct {
mgr *mgr.Manager
mgr *mgr.Manager
instance instance
}
@@ -165,4 +165,5 @@ type instance interface {
Access() *access.Access
Network() *network.Network
NetQuery() *netquery.NetQuery
Resolver() *resolver.ResolverModule
}

View File

@@ -6,10 +6,12 @@ import (
"fmt"
"net"
"os"
"strings"
"sync/atomic"
"time"
"github.com/google/gopacket/layers"
"github.com/miekg/dns"
"github.com/tevino/abool"
"github.com/safing/portmaster/base/log"
@@ -23,6 +25,7 @@ import (
"github.com/safing/portmaster/service/network/netutils"
"github.com/safing/portmaster/service/network/packet"
"github.com/safing/portmaster/service/process"
"github.com/safing/portmaster/service/resolver"
"github.com/safing/portmaster/spn/access"
)
@@ -444,8 +447,9 @@ func filterHandler(conn *network.Connection, pkt packet.Packet) {
filterConnection = false
log.Tracer(pkt.Ctx()).Infof("filter: granting own pre-authenticated connection %s", conn)
// Redirect outbound DNS packets if enabled,
// Redirect outbound DNS packets if enabled,
case dnsQueryInterception() &&
!module.instance.Resolver().IsDisabled() &&
pkt.IsOutbound() &&
pkt.Info().DstPort == 53 &&
// that don't match the address of our nameserver,
@@ -478,11 +482,13 @@ func filterHandler(conn *network.Connection, pkt packet.Packet) {
// Decide how to continue handling connection.
switch {
case conn.Inspecting && looksLikeOutgoingDNSRequest(conn):
inspectDNSPacket(conn, pkt)
conn.UpdateFirewallHandler(inspectDNSPacket)
case conn.Inspecting:
log.Tracer(pkt.Ctx()).Trace("filter: start inspecting")
conn.UpdateFirewallHandler(inspectAndVerdictHandler)
inspectAndVerdictHandler(conn, pkt)
default:
conn.StopFirewallHandler()
verdictHandler(conn, pkt)
@@ -506,7 +512,7 @@ func FilterConnection(ctx context.Context, conn *network.Connection, pkt packet.
}
// TODO: Enable inspection framework again.
conn.Inspecting = false
// conn.Inspecting = false
// TODO: Quick fix for the SPN.
// Use inspection framework for proper encryption detection.
@@ -580,6 +586,98 @@ func inspectAndVerdictHandler(conn *network.Connection, pkt packet.Packet) {
issueVerdict(conn, pkt, 0, true)
}
func inspectDNSPacket(conn *network.Connection, pkt packet.Packet) {
// Ignore info-only packets in this handler.
if pkt.InfoOnly() {
return
}
dnsPacket := new(dns.Msg)
err := pkt.LoadPacketData()
if err != nil {
_ = pkt.Block()
log.Errorf("filter: failed to load packet payload: %s", err)
return
}
// Parse and block invalid packets.
err = dnsPacket.Unpack(pkt.Payload())
if err != nil {
err = pkt.PermanentBlock()
if err != nil {
log.Errorf("filter: failed to block packet: %s", err)
}
_ = conn.SetVerdict(network.VerdictBlock, "none DNS data on DNS port", "", nil)
conn.VerdictPermanent = true
conn.Save()
return
}
// Packet was parsed.
// Allow it but only after the answer was added to the cache.
defer func() {
err = pkt.Accept()
if err != nil {
log.Errorf("filter: failed to accept dns packet: %s", err)
}
}()
// Check if packet has a question.
if len(dnsPacket.Question) == 0 {
return
}
// Read create structs with the needed data.
question := dnsPacket.Question[0]
fqdn := dns.Fqdn(question.Name)
// Check for compat check dns request.
if strings.HasSuffix(fqdn, compat.DNSCheckInternalDomainScope) {
subdomain := strings.TrimSuffix(fqdn, compat.DNSCheckInternalDomainScope)
_ = compat.SubmitDNSCheckDomain(subdomain)
log.Infof("packet_handler: self-check domain received")
// No need to parse the answer.
return
}
// Check if there is an answer.
if len(dnsPacket.Answer) == 0 {
return
}
resolverInfo := &resolver.ResolverInfo{
Name: "DNSRequestObserver",
Type: resolver.ServerTypeFirewall,
Source: resolver.ServerSourceFirewall,
IP: conn.Entity.IP,
Domain: conn.Entity.Domain,
IPScope: conn.Entity.IPScope,
}
rrCache := &resolver.RRCache{
Domain: fqdn,
Question: dns.Type(question.Qtype),
RCode: dnsPacket.Rcode,
Answer: dnsPacket.Answer,
Ns: dnsPacket.Ns,
Extra: dnsPacket.Extra,
Resolver: resolverInfo,
}
query := &resolver.Query{
FQDN: fqdn,
QType: dns.Type(question.Qtype),
NoCaching: false,
IgnoreFailing: false,
LocalResolversOnly: false,
ICANNSpace: false,
DomainRoot: "",
}
// Save to cache
UpdateIPsAndCNAMEs(query, rrCache, conn)
}
func icmpFilterHandler(conn *network.Connection, pkt packet.Packet) {
// Load packet data.
err := pkt.LoadPacketData()

View File

@@ -19,6 +19,8 @@ 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/dnsmonitor"
"github.com/safing/portmaster/service/integration"
"github.com/safing/portmaster/service/intel/customlists"
"github.com/safing/portmaster/service/intel/filterlists"
"github.com/safing/portmaster/service/intel/geoip"
@@ -65,6 +67,7 @@ type Instance struct {
core *core.Core
updates *updates.Updates
integration *integration.OSIntegration
geoip *geoip.GeoIP
netenv *netenv.NetEnv
ui *ui.UI
@@ -74,6 +77,7 @@ type Instance struct {
firewall *firewall.Firewall
filterLists *filterlists.FilterLists
interception *interception.Interception
dnsmonitor *dnsmonitor.DNSMonitor
customlist *customlists.CustomList
status *status.Status
broadcasts *broadcasts.Broadcasts
@@ -107,7 +111,6 @@ func New(svcCfg *ServiceConfig) (*Instance, error) { //nolint:maintidx
instance.ctx, instance.cancelCtx = context.WithCancel(context.Background())
var err error
// Base modules
instance.base, err = base.New(instance)
if err != nil {
@@ -151,6 +154,10 @@ func New(svcCfg *ServiceConfig) (*Instance, error) { //nolint:maintidx
if err != nil {
return instance, fmt.Errorf("create updates module: %w", err)
}
instance.integration, err = integration.New(instance)
if err != nil {
return instance, fmt.Errorf("create integration module: %w", err)
}
instance.geoip, err = geoip.New(instance)
if err != nil {
return instance, fmt.Errorf("create customlist module: %w", err)
@@ -187,6 +194,10 @@ func New(svcCfg *ServiceConfig) (*Instance, error) { //nolint:maintidx
if err != nil {
return instance, fmt.Errorf("create interception module: %w", err)
}
instance.dnsmonitor, err = dnsmonitor.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)
@@ -275,6 +286,7 @@ func New(svcCfg *ServiceConfig) (*Instance, error) { //nolint:maintidx
instance.core,
instance.updates,
instance.integration,
instance.geoip,
instance.netenv,
@@ -288,6 +300,7 @@ func New(svcCfg *ServiceConfig) (*Instance, error) { //nolint:maintidx
instance.filterLists,
instance.customlist,
instance.interception,
instance.dnsmonitor,
instance.compat,
instance.status,
@@ -378,6 +391,11 @@ func (i *Instance) Updates() *updates.Updates {
return i.updates
}
// OSIntegration returns the integration module.
func (i *Instance) OSIntegration() *integration.OSIntegration {
return i.integration
}
// GeoIP returns the geoip module.
func (i *Instance) GeoIP() *geoip.GeoIP {
return i.geoip
@@ -463,6 +481,11 @@ func (i *Instance) Interception() *interception.Interception {
return i.interception
}
// DNSMonitor returns the dns-listener module.
func (i *Instance) DNSMonitor() *dnsmonitor.DNSMonitor {
return i.dnsmonitor
}
// CustomList returns the customlist module.
func (i *Instance) CustomList() *customlists.CustomList {
return i.customlist

View File

@@ -0,0 +1,114 @@
//go:build windows
// +build windows
package integration
import (
"fmt"
"golang.org/x/sys/windows"
)
type ETWFunctions struct {
createState *windows.Proc
initializeSession *windows.Proc
startTrace *windows.Proc
flushTrace *windows.Proc
stopTrace *windows.Proc
destroySession *windows.Proc
stopOldSession *windows.Proc
}
func initializeETW(dll *windows.DLL) (ETWFunctions, error) {
var functions ETWFunctions
var err error
functions.createState, err = dll.FindProc("PM_ETWCreateState")
if err != nil {
return functions, fmt.Errorf("failed to load function PM_ETWCreateState: %q", err)
}
functions.initializeSession, err = dll.FindProc("PM_ETWInitializeSession")
if err != nil {
return functions, fmt.Errorf("failed to load function PM_ETWInitializeSession: %q", err)
}
functions.startTrace, err = dll.FindProc("PM_ETWStartTrace")
if err != nil {
return functions, fmt.Errorf("failed to load function PM_ETWStartTrace: %q", err)
}
functions.flushTrace, err = dll.FindProc("PM_ETWFlushTrace")
if err != nil {
return functions, fmt.Errorf("failed to load function PM_ETWFlushTrace: %q", err)
}
functions.stopTrace, err = dll.FindProc("PM_ETWStopTrace")
if err != nil {
return functions, fmt.Errorf("failed to load function PM_ETWStopTrace: %q", err)
}
functions.destroySession, err = dll.FindProc("PM_ETWDestroySession")
if err != nil {
return functions, fmt.Errorf("failed to load function PM_ETWDestroySession: %q", err)
}
functions.stopOldSession, err = dll.FindProc("PM_ETWStopOldSession")
if err != nil {
return functions, fmt.Errorf("failed to load function PM_ETWDestroySession: %q", err)
}
return functions, nil
}
// CreateState calls the dll createState C function.
func (etw ETWFunctions) CreateState(callback uintptr) uintptr {
state, _, _ := etw.createState.Call(callback)
return state
}
// InitializeSession calls the dll initializeSession C function.
func (etw ETWFunctions) InitializeSession(state uintptr) error {
rc, _, _ := etw.initializeSession.Call(state)
if rc != 0 {
return fmt.Errorf("failed with status code: %d", rc)
}
return nil
}
// StartTrace calls the dll startTrace C function.
func (etw ETWFunctions) StartTrace(state uintptr) error {
rc, _, _ := etw.startTrace.Call(state)
if rc != 0 {
return fmt.Errorf("failed with status code: %d", rc)
}
return nil
}
// FlushTrace calls the dll flushTrace C function.
func (etw ETWFunctions) FlushTrace(state uintptr) error {
rc, _, _ := etw.flushTrace.Call(state)
if rc != 0 {
return fmt.Errorf("failed with status code: %d", rc)
}
return nil
}
// StopTrace calls the dll stopTrace C function.
func (etw ETWFunctions) StopTrace(state uintptr) error {
rc, _, _ := etw.stopTrace.Call(state)
if rc != 0 {
return fmt.Errorf("failed with status code: %d", rc)
}
return nil
}
// DestroySession calls the dll destroySession C function.
func (etw ETWFunctions) DestroySession(state uintptr) error {
rc, _, _ := etw.destroySession.Call(state)
if rc != 0 {
return fmt.Errorf("failed with status code: %d", rc)
}
return nil
}
// StopOldSession calls the dll stopOldSession C function.
func (etw ETWFunctions) StopOldSession() error {
rc, _, _ := etw.stopOldSession.Call()
if rc != 0 {
return fmt.Errorf("failed with status code: %d", rc)
}
return nil
}

View File

@@ -0,0 +1,16 @@
//go:build !windows
// +build !windows
package integration
type OSSpecific struct{}
// Initialize is empty on any OS different then Windows.
func (i *OSIntegration) Initialize() error {
return nil
}
// CleanUp releases any resourses allocated during initializaion.
func (i *OSIntegration) CleanUp() error {
return nil
}

View File

@@ -0,0 +1,52 @@
//go:build windows
// +build windows
package integration
import (
"fmt"
"github.com/safing/portmaster/service/updates"
"golang.org/x/sys/windows"
)
type OSSpecific struct {
dll *windows.DLL
etwFunctions ETWFunctions
}
// Initialize loads the dll and finds all the needed functions from it.
func (i *OSIntegration) Initialize() error {
// Find path to the dll.
file, err := updates.GetFile("portmaster-core.dll")
if err != nil {
return err
}
// Load the DLL.
i.os.dll, err = windows.LoadDLL(file.Path())
if err != nil {
return fmt.Errorf("failed to load dll: %q", err)
}
// Enumerate all needed dll functions.
i.os.etwFunctions, err = initializeETW(i.os.dll)
if err != nil {
return err
}
return nil
}
// CleanUp releases any resourses allocated during initializaion.
func (i *OSIntegration) CleanUp() error {
if i.os.dll != nil {
return i.os.dll.Release()
}
return nil
}
// GetETWInterface return struct containing all the ETW related functions.
func (i *OSIntegration) GetETWInterface() ETWFunctions {
return i.os.etwFunctions
}

View File

@@ -0,0 +1,49 @@
package integration
import (
"github.com/safing/portmaster/service/mgr"
"github.com/safing/portmaster/service/updates"
)
// OSIntegration module provides special integration with the OS.
type OSIntegration struct {
m *mgr.Manager
states *mgr.StateMgr
//nolint:unused
os OSSpecific
instance instance
}
// New returns a new OSIntegration module.
func New(instance instance) (*OSIntegration, error) {
m := mgr.New("OSIntegration")
module := &OSIntegration{
m: m,
states: m.NewStateMgr(),
instance: instance,
}
return module, nil
}
// Manager returns the module manager.
func (i *OSIntegration) Manager() *mgr.Manager {
return i.m
}
// Start starts the module.
func (i *OSIntegration) Start() error {
return i.Initialize()
}
// Stop stops the module.
func (i *OSIntegration) Stop() error {
return i.CleanUp()
}
type instance interface {
Updates() *updates.Updates
}

View File

@@ -5,6 +5,7 @@ import (
"errors"
"fmt"
"net"
"runtime"
"sync"
"sync/atomic"
"time"
@@ -18,6 +19,7 @@ import (
"github.com/safing/portmaster/service/netenv"
"github.com/safing/portmaster/service/network/netutils"
"github.com/safing/portmaster/service/network/packet"
"github.com/safing/portmaster/service/network/reference"
"github.com/safing/portmaster/service/process"
_ "github.com/safing/portmaster/service/process/tags"
"github.com/safing/portmaster/service/resolver"
@@ -542,6 +544,23 @@ func (conn *Connection) GatherConnectionInfo(pkt packet.Packet) (err error) {
// Try again with the global scope, in case DNS went through the system resolver.
ipinfo, err = resolver.GetIPInfo(resolver.IPInfoProfileScopeGlobal, pkt.Info().RemoteIP().String())
}
if runtime.GOOS == "windows" && err != nil {
// On windows domains may come with delay.
if module.instance.Resolver().IsDisabled() && conn.shouldWaitForDomain() {
// Flush the dns listener buffer and try again.
for i := range 4 {
_ = module.instance.DNSMonitor().Flush()
ipinfo, err = resolver.GetIPInfo(resolver.IPInfoProfileScopeGlobal, pkt.Info().RemoteIP().String())
if err == nil {
log.Tracer(pkt.Ctx()).Debugf("network: found domain from dnsmonitor after %d tries", i+1)
break
}
time.Sleep(5 * time.Millisecond)
}
}
}
if err == nil {
lastResolvedDomain := ipinfo.MostRecentDomain()
if lastResolvedDomain != nil {
@@ -869,3 +888,17 @@ func (conn *Connection) String() string {
return fmt.Sprintf("%s -> %s", conn.process, conn.Entity.IP)
}
}
func (conn *Connection) shouldWaitForDomain() bool {
// Should wait for Global Unicast, outgoing and not ICMP connections
switch {
case conn.Entity.IPScope != netutils.Global:
return false
case conn.Inbound:
return false
case reference.IsICMP(conn.Entity.Protocol):
return false
}
return true
}

View File

@@ -9,10 +9,12 @@ import (
"sync/atomic"
"github.com/safing/portmaster/base/log"
"github.com/safing/portmaster/service/firewall/interception/dnsmonitor"
"github.com/safing/portmaster/service/mgr"
"github.com/safing/portmaster/service/netenv"
"github.com/safing/portmaster/service/network/state"
"github.com/safing/portmaster/service/profile"
"github.com/safing/portmaster/service/resolver"
)
// Events.
@@ -188,4 +190,6 @@ func New(instance instance) (*Network, error) {
type instance interface {
Profile() *profile.ProfileModule
Resolver() *resolver.ResolverModule
DNSMonitor() *dnsmonitor.DNSMonitor
}

View File

@@ -106,11 +106,12 @@ func checkError(packet gopacket.Packet, info *Info) error {
return nil
}
// Parse parses an IP packet and saves the information in the given packet object.
func Parse(packetData []byte, pktBase *Base) (err error) {
// ParseLayer3 parses an IP packet and saves the information in the given packet object.
func ParseLayer3(packetData []byte, pktBase *Base) (err error) {
if len(packetData) == 0 {
return errors.New("empty packet")
}
pktBase.layer3Data = packetData
ipVersion := packetData[0] >> 4
@@ -155,6 +156,62 @@ func Parse(packetData []byte, pktBase *Base) (err error) {
return nil
}
// ParseLayer4 parses an layer 4 packet and saves the information in the given packet object.
func ParseLayer4(packetData []byte, pktBase *Base) (err error) {
if len(packetData) == 0 {
return errors.New("empty packet")
}
var layer gopacket.LayerType
switch pktBase.info.Protocol {
case ICMP:
layer = layers.LayerTypeICMPv4
case IGMP:
layer = layers.LayerTypeIGMP
case TCP:
layer = layers.LayerTypeTCP
case UDP:
layer = layers.LayerTypeUDP
case ICMPv6:
layer = layers.LayerTypeICMPv6
case UDPLite:
return fmt.Errorf("UDPLite not supported")
case RAW:
return fmt.Errorf("RAW protocol not supported")
case AnyHostInternalProtocol61:
return fmt.Errorf("AnyHostInternalProtocol61 protocol not supported")
default:
return fmt.Errorf("protocol not supported")
}
packet := gopacket.NewPacket(packetData, layer, gopacket.DecodeOptions{
Lazy: true,
NoCopy: true,
})
availableDecoders := []func(gopacket.Packet, *Info) error{
parseTCP,
parseUDP,
// parseUDPLite, // We don't yet support udplite.
parseICMPv4,
parseICMPv6,
parseIGMP,
checkError,
}
for _, dec := range availableDecoders {
if err := dec(packet, pktBase.Info()); err != nil {
return err
}
}
pktBase.layers = packet
if transport := packet.TransportLayer(); transport != nil {
pktBase.layer5Data = transport.LayerPayload()
}
return nil
}
func init() {
genIPProtocolFromLayerType()
}

View File

@@ -52,6 +52,27 @@ type ResolvedDomain struct {
Expires int64
}
// AddCNAMEs adds all cnames from the map related to its set Domain.
func (resolved *ResolvedDomain) AddCNAMEs(cnames map[string]string) {
// Resolve all CNAMEs in the correct order and add the to the record - up to max 50 layers.
domain := resolved.Domain
domainLoop:
for range 50 {
nextDomain, isCNAME := cnames[domain]
switch {
case !isCNAME:
break domainLoop
case nextDomain == resolved.Domain:
break domainLoop
case nextDomain == domain:
break domainLoop
}
resolved.CNAMEs = append(resolved.CNAMEs, nextDomain)
domain = nextDomain
}
}
// String returns a string representation of ResolvedDomain including
// the CNAME chain. It implements fmt.Stringer.
func (resolved *ResolvedDomain) String() string {

View File

@@ -29,6 +29,8 @@ type ResolverModule struct { //nolint
failingResolverWorkerMgr *mgr.WorkerMgr
suggestUsingStaleCacheTask *mgr.WorkerMgr
isDisabled atomic.Bool
states *mgr.StateMgr
}
@@ -52,6 +54,10 @@ func (rm *ResolverModule) Stop() error {
return nil
}
func (rm *ResolverModule) IsDisabled() bool {
return rm.isDisabled.Load()
}
func prep() error {
// Set DNS test connectivity function for the online status check
netenv.DNSTestQueryFunc = func(ctx context.Context, fdqn string) (ips []net.IP, ok bool, err error) {

View File

@@ -17,17 +17,22 @@ import (
// DNS Resolver Attributes.
const (
ServerTypeDNS = "dns"
ServerTypeTCP = "tcp"
ServerTypeDoT = "dot"
ServerTypeDoH = "doh"
ServerTypeMDNS = "mdns"
ServerTypeEnv = "env"
ServerTypeDNS = "dns"
ServerTypeTCP = "tcp"
ServerTypeDoT = "dot"
ServerTypeDoH = "doh"
ServerTypeMDNS = "mdns"
ServerTypeEnv = "env"
ServerTypeMonitor = "monitor"
ServerTypeFirewall = "firewall"
ServerSourceConfigured = "config"
ServerSourceOperatingSystem = "system"
ServerSourceMDNS = "mdns"
ServerSourceEnv = "env"
ServerSourceETW = "etw"
ServerSourceSystemd = "systemd"
ServerSourceFirewall = "firewall"
)
// DNS resolver scheme aliases.
@@ -82,11 +87,11 @@ type ResolverInfo struct { //nolint:golint,maligned // TODO
Name string
// Type describes the type of the resolver.
// Possible values include dns, tcp, dot, doh, mdns, env.
// Possible values include dns, tcp, dot, doh, mdns, env, monitor, firewall.
Type string
// Source describes where the resolver configuration came from.
// Possible values include config, system, mdns, env.
// Possible values include config, system, mdns, env, etw, systemd, firewall.
Source string
// IP is the IP address of the resolver

View File

@@ -388,7 +388,6 @@ func loadResolvers() {
// Resolve module error about missing resolvers.
module.states.Remove(missingResolversErrorID)
// Check if settings were changed and clear name cache when they did.
newResolverConfig := configuredNameServers()
if len(currentResolverConfig) > 0 &&
@@ -399,6 +398,14 @@ func loadResolvers() {
return err
})
}
// If no resolvers are configure set the disabled state. So other modules knows that the users does not want to use Portmaster resolver.
if len(newResolverConfig) == 0 {
module.isDisabled.Store(true)
} else {
module.isDisabled.Store(false)
}
currentResolverConfig = newResolverConfig
newResolvers := append(
@@ -431,7 +438,7 @@ func loadResolvers() {
// save resolvers
globalResolvers = newResolvers
// assing resolvers to scopes
// assign resolvers to scopes
setScopedResolvers(globalResolvers)
// set active resolvers (for cache validation)