diff --git a/.github/workflows/windows-dll.yml b/.github/workflows/windows-dll.yml new file mode 100644 index 000000000..9b2fd7231 --- /dev/null +++ b/.github/workflows/windows-dll.yml @@ -0,0 +1,41 @@ +name: Windows Portmaster Core DLL + +on: + push: + paths: + - 'windows_core_dll/**' + branches: + - master + - develop + + pull_request: + paths: + - 'windows_core_dll/**' + branches: + - master + - develop + workflow_dispatch: + +jobs: + build: + name: Build + runs-on: windows-latest + steps: + - name: Checkout Repository + uses: actions/checkout@v4 + - name: Add msbuild to PATH + uses: microsoft/setup-msbuild@v2 + - name: Build DLL + run: msbuild windows_core_dll\windows_core_dll.sln -t:rebuild -property:Configuration=Release + - name: Verify DLL + shell: powershell + run: | + if (!(Test-Path "windows_core_dll/x64/Release/portmaster-core.dll")) { + Write-Error "DLL build failed: portmaster-core.dll not found" + exit 1 + } + - name: Upload artifacts + uses: actions/upload-artifact@v4 + with: + name: portmaster-core-dll + path: windows_core_dll/x64/Release/portmaster-core.dll \ No newline at end of file diff --git a/.gitignore b/.gitignore index e0a6550a9..03d8b25f6 100644 --- a/.gitignore +++ b/.gitignore @@ -52,3 +52,4 @@ go.work.sum # Kext releases windows_kext/release/kext_release_*.zip +windows_core_dll/.vs/windows_core_dll diff --git a/go.mod b/go.mod index 80685cce6..91d1dec1f 100644 --- a/go.mod +++ b/go.mod @@ -58,6 +58,7 @@ require ( github.com/tidwall/gjson v1.18.0 github.com/tidwall/sjson v1.2.5 github.com/umahmood/haversine v0.0.0-20151105152445-808ab04add26 + github.com/varlink/go v0.4.0 github.com/vincent-petithory/dataurl v1.0.0 go.etcd.io/bbolt v1.3.11 golang.org/x/exp v0.0.0-20241108190413-2d47ceb2692f @@ -91,6 +92,7 @@ require ( github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/josharian/native v1.1.0 // indirect github.com/klauspost/cpuid/v2 v2.2.9 // indirect + github.com/maruel/panicparse/v2 v2.3.1 // indirect github.com/mdlayher/netlink v1.7.2 // indirect github.com/mdlayher/socket v0.5.1 // indirect github.com/mitchellh/reflectwalk v1.0.2 // indirect diff --git a/go.sum b/go.sum index 4a0cac3fc..05a93f40b 100644 --- a/go.sum +++ b/go.sum @@ -311,6 +311,8 @@ github.com/valyala/fastrand v1.1.0 h1:f+5HkLW4rsgzdNoleUOB69hyT9IlD2ZQh9GyDMfb5G github.com/valyala/fastrand v1.1.0/go.mod h1:HWqCzkrkg6QXT8V2EXWvXCoow7vLwOFN002oeRzjapQ= github.com/valyala/histogram v1.2.0 h1:wyYGAZZt3CpwUiIb9AU/Zbllg1llXyrtApRS815OLoQ= github.com/valyala/histogram v1.2.0/go.mod h1:Hb4kBwb4UxsaNbbbh+RRz8ZR6pdodR57tzWUS3BUzXY= +github.com/varlink/go v0.4.0 h1:+/BQoUO9eJK/+MTSHwFcJch7TMsb6N6Dqp6g0qaXXRo= +github.com/varlink/go v0.4.0/go.mod h1:DKg9Y2ctoNkesREGAEak58l+jOC6JU2aqZvUYs5DynU= github.com/vincent-petithory/dataurl v1.0.0 h1:cXw+kPto8NLuJtlMsI152irrVw9fRDX8AbShPRpg2CI= github.com/vincent-petithory/dataurl v1.0.0/go.mod h1:FHafX5vmDzyP+1CQATJn7WFKc9CvnvxyvZy6I1MrG/U= github.com/vmihailenco/msgpack/v5 v5.3.5/go.mod h1:7xyJ9e+0+9SaZT0Wt1RGleJXzli6Q/V5KbhBonMG9jc= diff --git a/service/compat/module.go b/service/compat/module.go index 5ac97b511..e6781c9b8 100644 --- a/service/compat/module.go +++ b/service/compat/module.go @@ -181,4 +181,5 @@ func New(instance instance) (*Compat, error) { type instance interface { NetEnv() *netenv.NetEnv + Resolver() *resolver.ResolverModule } diff --git a/service/compat/selfcheck.go b/service/compat/selfcheck.go index 27efd4881..0bbef4e4c 100644 --- a/service/compat/selfcheck.go +++ b/service/compat/selfcheck.go @@ -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: diff --git a/service/firewall/bypassing.go b/service/firewall/bypassing.go index 415fc6c88..4fe9a1191 100644 --- a/service/firewall/bypassing.go +++ b/service/firewall/bypassing.go @@ -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 +} diff --git a/service/firewall/dns.go b/service/firewall/dns.go index 9b1a55e5a..e34347089 100644 --- a/service/firewall/dns.go +++ b/service/firewall/dns.go @@ -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", " ") -} diff --git a/service/firewall/interception/dnsmonitor/etwlink_windows.go b/service/firewall/interception/dnsmonitor/etwlink_windows.go new file mode 100644 index 000000000..d014bbab1 --- /dev/null +++ b/service/firewall/interception/dnsmonitor/etwlink_windows.go @@ -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 +} diff --git a/service/firewall/interception/dnsmonitor/eventlistener.go b/service/firewall/interception/dnsmonitor/eventlistener.go new file mode 100644 index 000000000..911130c96 --- /dev/null +++ b/service/firewall/interception/dnsmonitor/eventlistener.go @@ -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 +} diff --git a/service/firewall/interception/dnsmonitor/eventlistener_linux.go b/service/firewall/interception/dnsmonitor/eventlistener_linux.go new file mode 100644 index 000000000..d987a0823 --- /dev/null +++ b/service/firewall/interception/dnsmonitor/eventlistener_linux.go @@ -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) +} diff --git a/service/firewall/interception/dnsmonitor/eventlistener_windows.go b/service/firewall/interception/dnsmonitor/eventlistener_windows.go new file mode 100644 index 000000000..b6a39fd8d --- /dev/null +++ b/service/firewall/interception/dnsmonitor/eventlistener_windows.go @@ -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) +} diff --git a/service/firewall/interception/dnsmonitor/module.go b/service/firewall/interception/dnsmonitor/module.go new file mode 100644 index 000000000..eed8be119 --- /dev/null +++ b/service/firewall/interception/dnsmonitor/module.go @@ -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) + } + } +} diff --git a/service/firewall/interception/dnsmonitor/varlinktypes.go b/service/firewall/interception/dnsmonitor/varlinktypes.go new file mode 100644 index 000000000..3021ac187 --- /dev/null +++ b/service/firewall/interception/dnsmonitor/varlinktypes.go @@ -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"` +} diff --git a/service/firewall/interception/nfq/nfq.go b/service/firewall/interception/nfq/nfq.go index 22a5b3908..397d18570 100644 --- a/service/firewall/interception/nfq/nfq.go +++ b/service/firewall/interception/nfq/nfq.go @@ -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 diff --git a/service/firewall/interception/windowskext/packet.go b/service/firewall/interception/windowskext/packet.go index 5942d7d91..9145926c5 100644 --- a/service/firewall/interception/windowskext/packet.go +++ b/service/firewall/interception/windowskext/packet.go @@ -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 diff --git a/service/firewall/interception/windowskext2/handler.go b/service/firewall/interception/windowskext2/handler.go index 57f74c714..d144fa637 100644 --- a/service/firewall/interception/windowskext2/handler.go +++ b/service/firewall/interception/windowskext2/handler.go @@ -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() diff --git a/service/firewall/interception/windowskext2/packet.go b/service/firewall/interception/windowskext2/packet.go index 52a7a2a79..00d950366 100644 --- a/service/firewall/interception/windowskext2/packet.go +++ b/service/firewall/interception/windowskext2/packet.go @@ -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) diff --git a/service/firewall/module.go b/service/firewall/module.go index 131d4cacb..2ac87de2c 100644 --- a/service/firewall/module.go +++ b/service/firewall/module.go @@ -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 } diff --git a/service/firewall/packet_handler.go b/service/firewall/packet_handler.go index a290182f8..c0e59cb74 100644 --- a/service/firewall/packet_handler.go +++ b/service/firewall/packet_handler.go @@ -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() diff --git a/service/instance.go b/service/instance.go index ad6e9dab9..ee4826053 100644 --- a/service/instance.go +++ b/service/instance.go @@ -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 diff --git a/service/integration/etw_windows.go b/service/integration/etw_windows.go new file mode 100644 index 000000000..eac3ad8f4 --- /dev/null +++ b/service/integration/etw_windows.go @@ -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 +} diff --git a/service/integration/integration.go b/service/integration/integration.go new file mode 100644 index 000000000..2189b1524 --- /dev/null +++ b/service/integration/integration.go @@ -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 +} diff --git a/service/integration/integration_windows.go b/service/integration/integration_windows.go new file mode 100644 index 000000000..786d6da63 --- /dev/null +++ b/service/integration/integration_windows.go @@ -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 +} diff --git a/service/integration/module.go b/service/integration/module.go new file mode 100644 index 000000000..0e43798ae --- /dev/null +++ b/service/integration/module.go @@ -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 +} diff --git a/service/network/connection.go b/service/network/connection.go index 7ea96400d..b3dd70fcc 100644 --- a/service/network/connection.go +++ b/service/network/connection.go @@ -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 +} diff --git a/service/network/module.go b/service/network/module.go index 4cab1cb15..eb9b452d1 100644 --- a/service/network/module.go +++ b/service/network/module.go @@ -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 } diff --git a/service/network/packet/parse.go b/service/network/packet/parse.go index 562546af3..adfc69d90 100644 --- a/service/network/packet/parse.go +++ b/service/network/packet/parse.go @@ -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() } diff --git a/service/resolver/ipinfo.go b/service/resolver/ipinfo.go index 89cf9297c..32cc0cc3b 100644 --- a/service/resolver/ipinfo.go +++ b/service/resolver/ipinfo.go @@ -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 { diff --git a/service/resolver/main.go b/service/resolver/main.go index 8a43d12be..107d5fc8c 100644 --- a/service/resolver/main.go +++ b/service/resolver/main.go @@ -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) { diff --git a/service/resolver/resolver.go b/service/resolver/resolver.go index 1a1a12f45..35d71329b 100644 --- a/service/resolver/resolver.go +++ b/service/resolver/resolver.go @@ -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 diff --git a/service/resolver/resolvers.go b/service/resolver/resolvers.go index c5609a017..45876b4e4 100644 --- a/service/resolver/resolvers.go +++ b/service/resolver/resolvers.go @@ -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) diff --git a/windows_core_dll/build.ps1 b/windows_core_dll/build.ps1 new file mode 100644 index 000000000..d58f45edc --- /dev/null +++ b/windows_core_dll/build.ps1 @@ -0,0 +1,2 @@ +msbuild .\windows_core_dll.sln /p:Configuration=Release +ls .\x64\Release\portmaster-core.dll \ No newline at end of file diff --git a/windows_core_dll/dllmain.cpp b/windows_core_dll/dllmain.cpp new file mode 100644 index 000000000..cc0efaac5 --- /dev/null +++ b/windows_core_dll/dllmain.cpp @@ -0,0 +1,197 @@ +// dllmain.cpp : Defines the entry point for the DLL application. +#include "pch.h" + +#pragma comment(lib, "tdh.lib") + +// GUID of the DNS log provider +static const GUID DNS_CLIENT_PROVIDER_GUID = { + 0x1C95126E, + 0x7EEA, + 0x49A9, + {0xA3, 0xFE, 0xA3, 0x78, 0xB0, 0x3D, 0xDB, 0x4D} }; + +// GUID of the event session. This should be unique for the application. +static const GUID PORTMASTER_ETW_SESSION_GUID = { + 0x0211d070, + 0xc3b2, + 0x4609, + {0x92, 0xf5, 0x28, 0xe7, 0x18, 0xb2, 0x3b, 0x18} }; + +// Name of the session. This is visble when user queries all ETW sessions. +// (example `logman query -ets`) +#define LOGSESSION_NAME L"PortmasterDNSEventListener" + +// Fuction type of the callback that will be called on each event. +typedef uint64_t(*GoEventRecordCallback)(wchar_t* domain, wchar_t* result); + +// Holds the state of the ETW Session. +struct ETWSessionState { + TRACEHANDLE SessionTraceHandle; + EVENT_TRACE_PROPERTIES* SessionProperties; + TRACEHANDLE sessionHandle; + GoEventRecordCallback callback; +}; + +// getPropertyValue reads a property from the event. +static bool getPropertyValue(PEVENT_RECORD evt, LPWSTR prop, PBYTE* pData) { + // Describe the data that needs to be retrieved from the event. + PROPERTY_DATA_DESCRIPTOR DataDescriptor; + ZeroMemory(&DataDescriptor, sizeof(DataDescriptor)); + DataDescriptor.PropertyName = (ULONGLONG)(prop); + DataDescriptor.ArrayIndex = 0; + + DWORD PropertySize = 0; + // Check if the data is avaliable and what is the size of it. + DWORD status = + TdhGetPropertySize(evt, 0, NULL, 1, &DataDescriptor, &PropertySize); + if (ERROR_SUCCESS != status) { + return false; + } + + // Allocate memory for the data. + *pData = (PBYTE)malloc(PropertySize); + if (NULL == *pData) { + return false; + } + + // Get the data. + status = + TdhGetProperty(evt, 0, NULL, 1, &DataDescriptor, PropertySize, *pData); + if (ERROR_SUCCESS != status) { + if (*pData) { + free(*pData); + *pData = NULL; + } + return false; + } + + return true; +} + +// EventRecordCallback is a callback called on each event. +static void WINAPI EventRecordCallback(PEVENT_RECORD eventRecord) { + PBYTE resultValue = NULL; + PBYTE domainValue = NULL; + + getPropertyValue(eventRecord, (LPWSTR)L"QueryResults", &resultValue); + getPropertyValue(eventRecord, (LPWSTR)L"QueryName", &domainValue); + + ETWSessionState* state = (ETWSessionState*)eventRecord->UserContext; + + if (resultValue != NULL && domainValue != NULL) { + state->callback((wchar_t*)domainValue, (wchar_t*)resultValue); + } + + free(resultValue); + free(domainValue); +} + +extern "C" { + // PM_ETWCreateState allocates memory for the state and initializes the config for the session. PM_ETWDestroySession must be called to avoid leaks. + // callback must be set to a valid function pointer. + __declspec(dllexport) ETWSessionState* PM_ETWCreateState(GoEventRecordCallback callback) { + // Create trace session properties. + ULONG BufferSize = sizeof(EVENT_TRACE_PROPERTIES) + sizeof(LOGSESSION_NAME); + EVENT_TRACE_PROPERTIES* SessionProperties = + (EVENT_TRACE_PROPERTIES*)calloc(1, BufferSize); + SessionProperties->Wnode.BufferSize = BufferSize; + SessionProperties->Wnode.Flags = WNODE_FLAG_TRACED_GUID; + SessionProperties->Wnode.ClientContext = 1; // QPC clock resolution + SessionProperties->Wnode.Guid = PORTMASTER_ETW_SESSION_GUID; + SessionProperties->LogFileMode = EVENT_TRACE_REAL_TIME_MODE; + SessionProperties->MaximumFileSize = 1; // MB + SessionProperties->LoggerNameOffset = sizeof(EVENT_TRACE_PROPERTIES); + + // Create state + ETWSessionState* state = (ETWSessionState*)calloc(1, sizeof(ETWSessionState)); + state->SessionProperties = SessionProperties; + state->callback = callback; + return state; + } + + // PM_ETWInitializeSession initializes the session. + __declspec(dllexport) uint32_t PM_ETWInitializeSession(ETWSessionState* state) { + return StartTrace(&state->SessionTraceHandle, LOGSESSION_NAME, + state->SessionProperties); + } + + // PM_ETWStartTrace subscribes to the dns events and start listening. The function blocks while the listener is running. + // Call PM_ETWStopTrace to stop the listener. + __declspec(dllexport) uint32_t PM_ETWStartTrace(ETWSessionState* state) { + ULONG status = + EnableTraceEx2(state->SessionTraceHandle, (LPCGUID)&DNS_CLIENT_PROVIDER_GUID, + EVENT_CONTROL_CODE_ENABLE_PROVIDER, + TRACE_LEVEL_INFORMATION, 0, 0, 0, NULL); + + if (status != ERROR_SUCCESS) { + return status; + } + + EVENT_TRACE_LOGFILE trace = { 0 }; + + trace.LoggerName = (LPWSTR)(LOGSESSION_NAME); + trace.ProcessTraceMode = + PROCESS_TRACE_MODE_REAL_TIME | PROCESS_TRACE_MODE_EVENT_RECORD; + trace.EventRecordCallback = EventRecordCallback; + trace.Context = state; + + state->sessionHandle = OpenTrace(&trace); + if (state->sessionHandle == INVALID_PROCESSTRACE_HANDLE) { + return 1; + } + + status = ProcessTrace(&state->sessionHandle, 1, NULL, NULL); + if (status != ERROR_SUCCESS) { + return 1; + } + + return ERROR_SUCCESS; + } + + // PM_ETWFlushTrace flushes the event buffer. + __declspec(dllexport) uint32_t PM_ETWFlushTrace(ETWSessionState* state) { + return ControlTrace(state->SessionTraceHandle, LOGSESSION_NAME, + state->SessionProperties, EVENT_TRACE_CONTROL_FLUSH); + } + + // PM_ETWFlushTrace stops the listener. + __declspec(dllexport) uint32_t PM_ETWStopTrace(ETWSessionState* state) { + return ControlTrace(state->SessionTraceHandle, LOGSESSION_NAME, state->SessionProperties, + EVENT_TRACE_CONTROL_STOP); + } + + // PM_ETWFlushTrace Closes the session and frees resourses. + __declspec(dllexport) uint32_t PM_ETWDestroySession(ETWSessionState* state) { + if (state == NULL) { + return 1; + } + uint32_t status = CloseTrace(state->sessionHandle); + + // Free memory. + free(state->SessionProperties); + free(state); + return status; + } + + // PM_ETWStopOldSession removes old session with the same name if they exist. + // It returns success(0) only if its able to delete the old session. + __declspec(dllexport) ULONG PM_ETWStopOldSession() { + ULONG status = ERROR_SUCCESS; + TRACEHANDLE sessionHandle = 0; + + // Create trace session properties + size_t bufferSize = sizeof(EVENT_TRACE_PROPERTIES) + sizeof(LOGSESSION_NAME); + EVENT_TRACE_PROPERTIES* sessionProperties = (EVENT_TRACE_PROPERTIES*)calloc(1, bufferSize); + sessionProperties->Wnode.BufferSize = (ULONG)bufferSize; + sessionProperties->Wnode.Flags = WNODE_FLAG_TRACED_GUID; + sessionProperties->Wnode.ClientContext = 1; // QPC clock resolution + sessionProperties->Wnode.Guid = PORTMASTER_ETW_SESSION_GUID; + sessionProperties->LoggerNameOffset = sizeof(EVENT_TRACE_PROPERTIES); + + // Use Control trace will stop the session which will trigger a delete. + status = ControlTrace(NULL, LOGSESSION_NAME, sessionProperties, EVENT_TRACE_CONTROL_STOP); + + free(sessionProperties); + return status; + } +} \ No newline at end of file diff --git a/windows_core_dll/framework.h b/windows_core_dll/framework.h new file mode 100644 index 000000000..a9744f820 --- /dev/null +++ b/windows_core_dll/framework.h @@ -0,0 +1,5 @@ +#pragma once + +#define WIN32_LEAN_AND_MEAN // Exclude rarely-used stuff from Windows headers +// Windows Header Files +#include diff --git a/windows_core_dll/pch.cpp b/windows_core_dll/pch.cpp new file mode 100644 index 000000000..91c22df2a --- /dev/null +++ b/windows_core_dll/pch.cpp @@ -0,0 +1,5 @@ +// pch.cpp: source file corresponding to the pre-compiled header + +#include "pch.h" + +// When you are using pre-compiled headers, this source file is necessary for compilation to succeed. diff --git a/windows_core_dll/pch.h b/windows_core_dll/pch.h new file mode 100644 index 000000000..e5658cac9 --- /dev/null +++ b/windows_core_dll/pch.h @@ -0,0 +1,22 @@ +// pch.h: This is a precompiled header file. +// Files listed below are compiled only once, improving build performance for future builds. +// This also affects IntelliSense performance, including code completion and many code browsing features. +// However, files listed here are ALL re-compiled if any one of them is updated between builds. +// Do not add files here that you will be updating frequently as this negates the performance advantage. + +#ifndef PCH_H +#define PCH_H + +// add headers that you want to pre-compile here +#include "framework.h" + +#include +#include +#include +#include +#include +#include +#include + + +#endif //PCH_H diff --git a/windows_core_dll/windows_core_dll.sln b/windows_core_dll/windows_core_dll.sln new file mode 100644 index 000000000..73d46ca7a --- /dev/null +++ b/windows_core_dll/windows_core_dll.sln @@ -0,0 +1,31 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.11.35222.181 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "windows_core_dll", "windows_core_dll.vcxproj", "{6F3C7EAF-8511-4822-AAF0-1086D27E4DA9}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|x64 = Debug|x64 + Debug|x86 = Debug|x86 + Release|x64 = Release|x64 + Release|x86 = Release|x86 + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {6F3C7EAF-8511-4822-AAF0-1086D27E4DA9}.Debug|x64.ActiveCfg = Debug|x64 + {6F3C7EAF-8511-4822-AAF0-1086D27E4DA9}.Debug|x64.Build.0 = Debug|x64 + {6F3C7EAF-8511-4822-AAF0-1086D27E4DA9}.Debug|x86.ActiveCfg = Debug|Win32 + {6F3C7EAF-8511-4822-AAF0-1086D27E4DA9}.Debug|x86.Build.0 = Debug|Win32 + {6F3C7EAF-8511-4822-AAF0-1086D27E4DA9}.Release|x64.ActiveCfg = Release|x64 + {6F3C7EAF-8511-4822-AAF0-1086D27E4DA9}.Release|x64.Build.0 = Release|x64 + {6F3C7EAF-8511-4822-AAF0-1086D27E4DA9}.Release|x86.ActiveCfg = Release|Win32 + {6F3C7EAF-8511-4822-AAF0-1086D27E4DA9}.Release|x86.Build.0 = Release|Win32 + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {8E60106D-49DF-49C7-AC08-02775342FEAE} + EndGlobalSection +EndGlobal diff --git a/windows_core_dll/windows_core_dll.vcxproj b/windows_core_dll/windows_core_dll.vcxproj new file mode 100644 index 000000000..cf6d65f12 --- /dev/null +++ b/windows_core_dll/windows_core_dll.vcxproj @@ -0,0 +1,158 @@ + + + + + Debug + Win32 + + + Release + Win32 + + + Debug + x64 + + + Release + x64 + + + + 17.0 + Win32Proj + {6f3c7eaf-8511-4822-aaf0-1086d27e4da9} + windowscoredll + 10.0 + portmaster-core + + + + DynamicLibrary + true + v143 + Unicode + + + DynamicLibrary + false + v143 + true + Unicode + + + DynamicLibrary + true + v143 + Unicode + + + DynamicLibrary + false + v143 + true + Unicode + + + + + + + + + + + + + + + + + + + + + + Level3 + true + WIN32;_DEBUG;WINDOWSCOREDLL_EXPORTS;_WINDOWS;_USRDLL;%(PreprocessorDefinitions) + true + Use + pch.h + + + Windows + true + false + + + + + Level3 + true + true + true + WIN32;NDEBUG;WINDOWSCOREDLL_EXPORTS;_WINDOWS;_USRDLL;%(PreprocessorDefinitions) + true + Use + pch.h + + + Windows + true + true + true + false + + + + + Level3 + true + _DEBUG;WINDOWSCOREDLL_EXPORTS;_WINDOWS;_USRDLL;%(PreprocessorDefinitions) + true + Use + pch.h + + + Windows + true + false + + + + + Level3 + true + true + true + NDEBUG;WINDOWSCOREDLL_EXPORTS;_WINDOWS;_USRDLL;%(PreprocessorDefinitions) + true + Use + pch.h + + + Windows + true + true + true + false + + + + + + + + + + Create + Create + Create + Create + + + + + + \ No newline at end of file diff --git a/windows_core_dll/windows_core_dll.vcxproj.filters b/windows_core_dll/windows_core_dll.vcxproj.filters new file mode 100644 index 000000000..f99bb483f --- /dev/null +++ b/windows_core_dll/windows_core_dll.vcxproj.filters @@ -0,0 +1,33 @@ + + + + + {4FC737F1-C7A5-4376-A066-2A32D752A2FF} + cpp;c;cc;cxx;c++;cppm;ixx;def;odl;idl;hpj;bat;asm;asmx + + + {93995380-89BD-4b04-88EB-625FBE52EBFB} + h;hh;hpp;hxx;h++;hm;inl;inc;ipp;xsd + + + {67DA6AB6-F800-4c08-8B7A-83BB121AAD01} + rc;ico;cur;bmp;dlg;rc2;rct;bin;rgs;gif;jpg;jpeg;jpe;resx;tiff;tif;png;wav;mfcribbon-ms + + + + + Header Files + + + Header Files + + + + + Source Files + + + Source Files + + + \ No newline at end of file diff --git a/windows_core_dll/windows_core_dll.vcxproj.user b/windows_core_dll/windows_core_dll.vcxproj.user new file mode 100644 index 000000000..0f14913f3 --- /dev/null +++ b/windows_core_dll/windows_core_dll.vcxproj.user @@ -0,0 +1,4 @@ + + + + \ No newline at end of file