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:
@@ -181,4 +181,5 @@ func New(instance instance) (*Compat, error) {
|
||||
|
||||
type instance interface {
|
||||
NetEnv() *netenv.NetEnv
|
||||
Resolver() *resolver.ResolverModule
|
||||
}
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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", " ")
|
||||
}
|
||||
|
||||
99
service/firewall/interception/dnsmonitor/etwlink_windows.go
Normal file
99
service/firewall/interception/dnsmonitor/etwlink_windows.go
Normal 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
|
||||
}
|
||||
19
service/firewall/interception/dnsmonitor/eventlistener.go
Normal file
19
service/firewall/interception/dnsmonitor/eventlistener.go
Normal 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
|
||||
}
|
||||
144
service/firewall/interception/dnsmonitor/eventlistener_linux.go
Normal file
144
service/firewall/interception/dnsmonitor/eventlistener_linux.go
Normal 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)
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
138
service/firewall/interception/dnsmonitor/module.go
Normal file
138
service/firewall/interception/dnsmonitor/module.go
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
83
service/firewall/interception/dnsmonitor/varlinktypes.go
Normal file
83
service/firewall/interception/dnsmonitor/varlinktypes.go
Normal 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"`
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
|
||||
114
service/integration/etw_windows.go
Normal file
114
service/integration/etw_windows.go
Normal 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
|
||||
}
|
||||
16
service/integration/integration.go
Normal file
16
service/integration/integration.go
Normal 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
|
||||
}
|
||||
52
service/integration/integration_windows.go
Normal file
52
service/integration/integration_windows.go
Normal 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
|
||||
}
|
||||
49
service/integration/module.go
Normal file
49
service/integration/module.go
Normal 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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user