Revamp intel and nameserver packages

This commit is contained in:
Daniel 2019-10-25 13:35:02 +02:00
parent 5799d2559b
commit 25b1d59663
18 changed files with 1675 additions and 1060 deletions

View file

@ -13,7 +13,7 @@ var (
localAddrFactory func(network string) net.Addr localAddrFactory func(network string) net.Addr
) )
// SetLocalAddrFactory supplied the intel package with a function to set local addresses for connections. // SetLocalAddrFactory supplies the intel package with a function to get permitted local addresses for connections.
func SetLocalAddrFactory(laf func(network string) net.Addr) { func SetLocalAddrFactory(laf func(network string) net.Addr) {
if localAddrFactory == nil { if localAddrFactory == nil {
localAddrFactory = laf localAddrFactory = laf
@ -36,11 +36,9 @@ type clientManager struct {
ttl time.Duration // force refresh of connection to reduce traceability ttl time.Duration // force refresh of connection to reduce traceability
} }
// ref: https://godoc.org/github.com/miekg/dns#Client func newDNSClientManager(_ *Resolver) *clientManager {
func newDNSClientManager(resolver *Resolver) *clientManager {
return &clientManager{ return &clientManager{
// ttl: 1 * time.Minute, ttl: 0, // new client for every request, as we need to randomize the port
factory: func() *dns.Client { factory: func() *dns.Client {
return &dns.Client{ return &dns.Client{
Timeout: 5 * time.Second, Timeout: 5 * time.Second,
@ -52,15 +50,16 @@ func newDNSClientManager(resolver *Resolver) *clientManager {
} }
} }
func newTCPClientManager(resolver *Resolver) *clientManager { func newTCPClientManager(_ *Resolver) *clientManager {
return &clientManager{ return &clientManager{
// ttl: 5 * time.Minute, ttl: 0, // TODO: build a custom client that can reuse connections to some degree (performance / privacy tradeoff)
factory: func() *dns.Client { factory: func() *dns.Client {
return &dns.Client{ return &dns.Client{
Net: "tcp", Net: "tcp",
Timeout: 5 * time.Second, Timeout: 5 * time.Second,
Dialer: &net.Dialer{ Dialer: &net.Dialer{
LocalAddr: getLocalAddr("tcp"), LocalAddr: getLocalAddr("tcp"),
KeepAlive: 15 * time.Second,
}, },
} }
}, },
@ -69,19 +68,19 @@ func newTCPClientManager(resolver *Resolver) *clientManager {
func newTLSClientManager(resolver *Resolver) *clientManager { func newTLSClientManager(resolver *Resolver) *clientManager {
return &clientManager{ return &clientManager{
// ttl: 5 * time.Minute, ttl: 0, // TODO: build a custom client that can reuse connections to some degree (performance / privacy tradeoff)
factory: func() *dns.Client { factory: func() *dns.Client {
return &dns.Client{ return &dns.Client{
Net: "tcp-tls", Net: "tcp-tls",
TLSConfig: &tls.Config{ TLSConfig: &tls.Config{
MinVersion: tls.VersionTLS12, MinVersion: tls.VersionTLS12,
ServerName: resolver.VerifyDomain, ServerName: resolver.VerifyDomain,
// TODO: use custom random // TODO: use portbase rng
// Rand: io.Reader,
}, },
Timeout: 5 * time.Second, Timeout: 5 * time.Second,
Dialer: &net.Dialer{ Dialer: &net.Dialer{
LocalAddr: getLocalAddr("tcp"), LocalAddr: getLocalAddr("tcp"),
KeepAlive: 15 * time.Second,
}, },
} }
}, },
@ -90,18 +89,18 @@ func newTLSClientManager(resolver *Resolver) *clientManager {
func newHTTPSClientManager(resolver *Resolver) *clientManager { func newHTTPSClientManager(resolver *Resolver) *clientManager {
return &clientManager{ return &clientManager{
// ttl: 5 * time.Minute, ttl: 0, // TODO: build a custom client that can reuse connections to some degree (performance / privacy tradeoff)
factory: func() *dns.Client { factory: func() *dns.Client {
new := &dns.Client{ new := &dns.Client{
Net: "https", Net: "https",
TLSConfig: &tls.Config{ TLSConfig: &tls.Config{
MinVersion: tls.VersionTLS12, MinVersion: tls.VersionTLS12,
// TODO: use custom random // TODO: use portbase rng
// Rand: io.Reader,
}, },
Timeout: 5 * time.Second, Timeout: 5 * time.Second,
Dialer: &net.Dialer{ Dialer: &net.Dialer{
LocalAddr: getLocalAddr("tcp"), LocalAddr: getLocalAddr("tcp"),
KeepAlive: 15 * time.Second,
}, },
} }
if resolver.VerifyDomain != "" { if resolver.VerifyDomain != "" {

View file

@ -1,6 +1,9 @@
package intel package intel
import ( import (
"fmt"
"strings"
"github.com/safing/portbase/config" "github.com/safing/portbase/config"
"github.com/safing/portmaster/status" "github.com/safing/portmaster/status"
) )
@ -8,13 +11,18 @@ import (
var ( var (
configuredNameServers config.StringArrayOption configuredNameServers config.StringArrayOption
defaultNameServers = []string{ defaultNameServers = []string{
// "dot://9.9.9.9:853?verify=dns.quad9.net&", // Quad9
// "dot|149.112.112.112:853|dns.quad9.net", // Quad9
// "dot://[2620:fe::fe]:853?verify=dns.quad9.net&name=Quad9" // Quad9
// "dot://[2620:fe::9]:853?verify=dns.quad9.net&name=Quad9" // Quad9
"dot|1.1.1.1:853|cloudflare-dns.com", // Cloudflare
"dot|1.0.0.1:853|cloudflare-dns.com", // Cloudflare
"dns|9.9.9.9:53", // Quad9
"dns|149.112.112.112:53", // Quad9
"dns|1.1.1.1:53", // Cloudflare "dns|1.1.1.1:53", // Cloudflare
"dns|1.0.0.1:53", // Cloudflare "dns|1.0.0.1:53", // Cloudflare
"dns|9.9.9.9:53", // Quad9 // "doh|cloudflare-dns.com/dns-query", // DoH still experimental
"tls|1.1.1.1:853|cloudflare-dns.com", // Cloudflare
"tls|1.0.0.1:853|cloudflare-dns.com", // Cloudflare
"tls|9.9.9.9:853|dns.quad9.net", // Quad9
// "https|cloudflare-dns.com/dns-query", // HTTPS still experimental
} }
nameserverRetryRate config.IntOption nameserverRetryRate config.IntOption
@ -22,15 +30,17 @@ var (
doNotUseAssignedNameservers status.SecurityLevelOption doNotUseAssignedNameservers status.SecurityLevelOption
doNotUseInsecureProtocols status.SecurityLevelOption doNotUseInsecureProtocols status.SecurityLevelOption
doNotResolveSpecialDomains status.SecurityLevelOption doNotResolveSpecialDomains status.SecurityLevelOption
doNotResolveTestDomains status.SecurityLevelOption
) )
func prep() error { func prepConfig() error {
err := config.Register(&config.Option{ err := config.Register(&config.Option{
Name: "Nameservers (DNS)", Name: "Nameservers (DNS)",
Key: "intel/nameservers", Key: "intel/nameservers",
Description: "Nameserver to use for resolving DNS requests.", Description: "Nameserver to use for resolving DNS requests.",
ExpertiseLevel: config.ExpertiseLevelExpert,
OptType: config.OptTypeStringArray, OptType: config.OptTypeStringArray,
ExpertiseLevel: config.ExpertiseLevelExpert,
ReleaseLevel: config.ReleaseLevelStable,
DefaultValue: defaultNameServers, DefaultValue: defaultNameServers,
ValidationRegex: "^(dns|tcp|tls|https)|[a-z0-9\\.|-]+$", ValidationRegex: "^(dns|tcp|tls|https)|[a-z0-9\\.|-]+$",
}) })
@ -43,8 +53,9 @@ func prep() error {
Name: "Nameserver Retry Rate", Name: "Nameserver Retry Rate",
Key: "intel/nameserverRetryRate", Key: "intel/nameserverRetryRate",
Description: "Rate at which to retry failed nameservers, in seconds.", Description: "Rate at which to retry failed nameservers, in seconds.",
ExpertiseLevel: config.ExpertiseLevelExpert,
OptType: config.OptTypeInt, OptType: config.OptTypeInt,
ExpertiseLevel: config.ExpertiseLevelExpert,
ReleaseLevel: config.ReleaseLevelStable,
DefaultValue: 600, DefaultValue: 600,
}) })
if err != nil { if err != nil {
@ -56,8 +67,9 @@ func prep() error {
Name: "Do not use Multicast DNS", Name: "Do not use Multicast DNS",
Key: "intel/doNotUseMulticastDNS", Key: "intel/doNotUseMulticastDNS",
Description: "Multicast DNS queries other devices in the local network", Description: "Multicast DNS queries other devices in the local network",
ExpertiseLevel: config.ExpertiseLevelExpert,
OptType: config.OptTypeInt, OptType: config.OptTypeInt,
ExpertiseLevel: config.ExpertiseLevelExpert,
ReleaseLevel: config.ReleaseLevelStable,
ExternalOptType: "security level", ExternalOptType: "security level",
DefaultValue: 6, DefaultValue: 6,
ValidationRegex: "^(7|6|4)$", ValidationRegex: "^(7|6|4)$",
@ -71,8 +83,9 @@ func prep() error {
Name: "Do not use assigned Nameservers", Name: "Do not use assigned Nameservers",
Key: "intel/doNotUseAssignedNameservers", Key: "intel/doNotUseAssignedNameservers",
Description: "that were acquired by the network (dhcp) or system", Description: "that were acquired by the network (dhcp) or system",
ExpertiseLevel: config.ExpertiseLevelExpert,
OptType: config.OptTypeInt, OptType: config.OptTypeInt,
ExpertiseLevel: config.ExpertiseLevelExpert,
ReleaseLevel: config.ReleaseLevelStable,
ExternalOptType: "security level", ExternalOptType: "security level",
DefaultValue: 4, DefaultValue: 4,
ValidationRegex: "^(7|6|4)$", ValidationRegex: "^(7|6|4)$",
@ -86,8 +99,9 @@ func prep() error {
Name: "Do not resolve insecurely", Name: "Do not resolve insecurely",
Key: "intel/doNotUseInsecureProtocols", Key: "intel/doNotUseInsecureProtocols",
Description: "Do not resolve domains with insecure protocols, ie. plain DNS", Description: "Do not resolve domains with insecure protocols, ie. plain DNS",
ExpertiseLevel: config.ExpertiseLevelExpert,
OptType: config.OptTypeInt, OptType: config.OptTypeInt,
ExpertiseLevel: config.ExpertiseLevelExpert,
ReleaseLevel: config.ReleaseLevelStable,
ExternalOptType: "security level", ExternalOptType: "security level",
DefaultValue: 4, DefaultValue: 4,
ValidationRegex: "^(7|6|4)$", ValidationRegex: "^(7|6|4)$",
@ -100,11 +114,12 @@ func prep() error {
err = config.Register(&config.Option{ err = config.Register(&config.Option{
Name: "Do not resolve special domains", Name: "Do not resolve special domains",
Key: "intel/doNotResolveSpecialDomains", Key: "intel/doNotResolveSpecialDomains",
Description: "Do not resolve special (top level) domains: example, example.com, example.net, example.org, invalid, test, onion. (RFC6761, RFC7686)", Description: fmt.Sprintf("Do not resolve the special top level domains %s", formatScopeList(specialServiceScopes)),
ExpertiseLevel: config.ExpertiseLevelExpert,
OptType: config.OptTypeInt, OptType: config.OptTypeInt,
ExpertiseLevel: config.ExpertiseLevelExpert,
ReleaseLevel: config.ReleaseLevelStable,
ExternalOptType: "security level", ExternalOptType: "security level",
DefaultValue: 6, DefaultValue: 7,
ValidationRegex: "^(7|6|4)$", ValidationRegex: "^(7|6|4)$",
}) })
if err != nil { if err != nil {
@ -112,5 +127,29 @@ func prep() error {
} }
doNotResolveSpecialDomains = status.ConfigIsActiveConcurrent("intel/doNotResolveSpecialDomains") doNotResolveSpecialDomains = status.ConfigIsActiveConcurrent("intel/doNotResolveSpecialDomains")
err = config.Register(&config.Option{
Name: "Do not resolve test domains",
Key: "intel/doNotResolveTestDomains",
Description: fmt.Sprintf("Do not resolve the special testing top level domains %s", formatScopeList(localTestScopes)),
OptType: config.OptTypeInt,
ExpertiseLevel: config.ExpertiseLevelExpert,
ReleaseLevel: config.ReleaseLevelStable,
ExternalOptType: "security level",
DefaultValue: 6,
ValidationRegex: "^(7|6|4)$",
})
if err != nil {
return err
}
doNotResolveTestDomains = status.ConfigIsActiveConcurrent("intel/doNotResolveTestDomains")
return nil return nil
} }
func formatScopeList(list []string) string {
formatted := make([]string, 0, len(list))
for _, domain := range list {
formatted = append(formatted, strings.Trim(domain, "."))
}
return strings.Join(formatted, ", ")
}

View file

@ -1,11 +1,13 @@
package intel package intel
import ( import (
"context"
"fmt" "fmt"
"sync" "sync"
"github.com/safing/portbase/database" "github.com/safing/portbase/database"
"github.com/safing/portbase/database/record" "github.com/safing/portbase/database/record"
"github.com/safing/portbase/log"
) )
var ( var (
@ -61,6 +63,13 @@ func (intel *Intel) Save() error {
} }
// GetIntel fetches intelligence data for the given domain. // GetIntel fetches intelligence data for the given domain.
func GetIntel(domain string) (*Intel, error) { func GetIntel(ctx context.Context, q *Query) (*Intel, error) {
return &Intel{Domain: domain}, nil // sanity check
if q == nil || !q.check() {
return nil, ErrInvalid
}
log.Tracer(ctx).Trace("intel: getting intel")
// TODO
return &Intel{Domain: q.FQDN}, nil
} }

View file

@ -2,8 +2,7 @@ package intel
import ( import (
"context" "context"
"time"
"github.com/miekg/dns"
"github.com/safing/portbase/log" "github.com/safing/portbase/log"
"github.com/safing/portbase/modules" "github.com/safing/portbase/modules"
@ -12,30 +11,41 @@ import (
_ "github.com/safing/portmaster/core" _ "github.com/safing/portmaster/core"
) )
var (
module *modules.Module
)
func init() { func init() {
modules.Register("intel", prep, start, nil, "core") module = modules.Register("intel", prep, start, nil, "core", "network")
}
func prep() error {
return prepConfig()
} }
func start() error { func start() error {
// load resolvers from config and environment // load resolvers from config and environment
loadResolvers(false) loadResolvers()
go listenToMDNS() err := module.RegisterEventHook(
"network",
"network changed",
"update nameservers",
func(_ context.Context, _ interface{}) error {
loadResolvers()
log.Debug("intel: reloaded nameservers due to network change")
return nil
},
)
if err != nil {
return err
}
module.StartServiceWorker(
"mdns handler",
5*time.Second,
listenToMDNS,
)
return nil return nil
} }
// GetIntelAndRRs returns intel and DNS resource records for the given domain.
func GetIntelAndRRs(ctx context.Context, domain string, qtype dns.Type, securityLevel uint8) (intel *Intel, rrs *RRCache) {
log.Tracer(ctx).Trace("intel: getting intel")
intel, err := GetIntel(domain)
if err != nil {
log.Tracer(ctx).Warningf("intel: failed to get intel: %s", err)
log.Errorf("intel: failed to get intel: %s", err)
intel = nil
}
log.Tracer(ctx).Tracef("intel: getting records")
rrs = Resolve(ctx, domain, qtype, securityLevel)
return
}

View file

@ -4,34 +4,29 @@ import (
"os" "os"
"testing" "testing"
"github.com/safing/portbase/database/dbmodule" "github.com/safing/portmaster/core"
"github.com/safing/portbase/log"
"github.com/safing/portbase/modules"
) )
func TestMain(m *testing.M) { func TestMain(m *testing.M) {
// setup // setup
testDir := os.TempDir() tmpDir, err := core.InitForTesting()
dbmodule.SetDatabaseLocation(testDir)
err := modules.Start()
if err != nil { if err != nil {
if err == modules.ErrCleanExit { panic(err)
os.Exit(0) }
} else {
err = modules.Shutdown() // setup package
err = prep()
if err != nil { if err != nil {
log.Shutdown() panic(err)
}
os.Exit(1)
}
} }
loadResolvers()
// run tests // run tests
rv := m.Run() rv := m.Run()
// teardown // teardown
modules.Shutdown() core.StopTesting()
os.RemoveAll(testDir) _ = os.RemoveAll(tmpDir)
// exit with test run return value // exit with test run return value
os.Exit(rv) os.Exit(rv)

View file

@ -25,13 +25,33 @@ var (
unicast4Conn *net.UDPConn unicast4Conn *net.UDPConn
unicast6Conn *net.UDPConn unicast6Conn *net.UDPConn
questions = make(map[uint16]savedQuestion) questions = make(map[uint16]*savedQuestion)
questionsLock sync.Mutex questionsLock sync.Mutex
mDNSResolver = &Resolver{
Server: ServerSourceMDNS,
ServerType: ServerTypeDNS,
Source: ServerSourceMDNS,
Conn: &mDNSResolverConn{},
}
) )
type mDNSResolverConn struct{}
func (mrc *mDNSResolverConn) Query(ctx context.Context, q *Query) (*RRCache, error) {
return queryMulticastDNS(ctx, q)
}
func (mrc *mDNSResolverConn) MarkFailed() {}
func (mrc *mDNSResolverConn) LastFail() time.Time {
return time.Time{}
}
type savedQuestion struct { type savedQuestion struct {
question dns.Question question dns.Question
expires int64 expires time.Time
response chan *RRCache
} }
func indexOfRR(entry *dns.RR_Header, list *[]dns.RR) int { func indexOfRR(entry *dns.RR_Header, list *[]dns.RR) int {
@ -43,16 +63,23 @@ func indexOfRR(entry *dns.RR_Header, list *[]dns.RR) int {
return -1 return -1
} }
func listenToMDNS() { //nolint:gocyclo,gocognit // TODO: make simpler
func listenToMDNS(ctx context.Context) error {
var err error var err error
messages := make(chan *dns.Msg) messages := make(chan *dns.Msg)
// TODO: init and start every listener in its own service worker
// this will make the more resilient and actually able to restart
multicast4Conn, err = net.ListenMulticastUDP("udp4", nil, &net.UDPAddr{IP: net.IPv4(224, 0, 0, 251), Port: 5353}) multicast4Conn, err = net.ListenMulticastUDP("udp4", nil, &net.UDPAddr{IP: net.IPv4(224, 0, 0, 251), Port: 5353})
if err != nil { if err != nil {
// TODO: retry after some time // TODO: retry after some time
log.Warningf("intel(mdns): failed to create udp4 listen multicast socket: %s", err) log.Warningf("intel(mdns): failed to create udp4 listen multicast socket: %s", err)
} else { } else {
go listenForDNSPackets(multicast4Conn, messages) module.StartServiceWorker("mdns udp4 multicast listener", 0, func(ctx context.Context) error {
return listenForDNSPackets(multicast4Conn, messages)
})
defer multicast4Conn.Close()
} }
multicast6Conn, err = net.ListenMulticastUDP("udp6", nil, &net.UDPAddr{IP: net.IP([]byte{0xff, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xfb}), Port: 5353}) multicast6Conn, err = net.ListenMulticastUDP("udp6", nil, &net.UDPAddr{IP: net.IP([]byte{0xff, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xfb}), Port: 5353})
@ -60,7 +87,10 @@ func listenToMDNS() {
// TODO: retry after some time // TODO: retry after some time
log.Warningf("intel(mdns): failed to create udp6 listen multicast socket: %s", err) log.Warningf("intel(mdns): failed to create udp6 listen multicast socket: %s", err)
} else { } else {
go listenForDNSPackets(multicast6Conn, messages) module.StartServiceWorker("mdns udp6 multicast listener", 0, func(ctx context.Context) error {
return listenForDNSPackets(multicast6Conn, messages)
})
defer multicast6Conn.Close()
} }
unicast4Conn, err = net.ListenUDP("udp4", &net.UDPAddr{IP: net.IPv4zero, Port: 0}) unicast4Conn, err = net.ListenUDP("udp4", &net.UDPAddr{IP: net.IPv4zero, Port: 0})
@ -68,7 +98,10 @@ func listenToMDNS() {
// TODO: retry after some time // TODO: retry after some time
log.Warningf("intel(mdns): failed to create udp4 listen socket: %s", err) log.Warningf("intel(mdns): failed to create udp4 listen socket: %s", err)
} else { } else {
go listenForDNSPackets(unicast4Conn, messages) module.StartServiceWorker("mdns udp4 unicast listener", 0, func(ctx context.Context) error {
return listenForDNSPackets(unicast4Conn, messages)
})
defer unicast4Conn.Close()
} }
unicast6Conn, err = net.ListenUDP("udp6", &net.UDPAddr{IP: net.IPv6zero, Port: 0}) unicast6Conn, err = net.ListenUDP("udp6", &net.UDPAddr{IP: net.IPv6zero, Port: 0})
@ -76,14 +109,32 @@ func listenToMDNS() {
// TODO: retry after some time // TODO: retry after some time
log.Warningf("intel(mdns): failed to create udp6 listen socket: %s", err) log.Warningf("intel(mdns): failed to create udp6 listen socket: %s", err)
} else { } else {
go listenForDNSPackets(unicast6Conn, messages) module.StartServiceWorker("mdns udp6 unicast listener", 0, func(ctx context.Context) error {
return listenForDNSPackets(unicast6Conn, messages)
})
defer unicast6Conn.Close()
} }
// start message handler
module.StartServiceWorker("mdns message handler", 0, func(ctx context.Context) error {
return handleMDNSMessages(ctx, messages)
})
// wait for shutdown
<-module.Ctx.Done()
return nil
}
//nolint:gocyclo,gocognit // TODO
func handleMDNSMessages(ctx context.Context, messages chan *dns.Msg) error {
for { for {
select { select {
case <-ctx.Done():
return nil
case message := <-messages: case message := <-messages:
// log.Tracef("intel: got net mdns message: %s", message) // log.Tracef("intel: got net mdns message: %s", message)
var err error
var question *dns.Question var question *dns.Question
var saveFullRequest bool var saveFullRequest bool
scavengedRecords := make(map[string]dns.RR) scavengedRecords := make(map[string]dns.RR)
@ -91,7 +142,7 @@ func listenToMDNS() {
// save every received response // save every received response
// if previous save was less than 2 seconds ago, add to response, else replace // if previous save was less than 2 seconds ago, add to response, else replace
// pick out A and AAAA records and save seperately // pick out A and AAAA records and save separately
// continue if not response // continue if not response
if !message.Response { if !message.Response {
@ -111,24 +162,28 @@ func listenToMDNS() {
continue continue
} }
// get question, some servers do not reply with question // return saved question
if len(message.Question) == 0 {
questionsLock.Lock() questionsLock.Lock()
savedQ, ok := questions[message.MsgHdr.Id] savedQ := questions[message.MsgHdr.Id]
questionsLock.Unlock() questionsLock.Unlock()
if ok {
question = &savedQ.question // get question, some servers do not reply with question
} if len(message.Question) > 0 {
} else {
question = &message.Question[0] question = &message.Question[0]
// if questions do not match, disregard saved question
if savedQ != nil && message.Question[0].String() != savedQ.question.String() {
savedQ = nil
}
} else if savedQ != nil {
question = &savedQ.question
} }
if question != nil { if question != nil {
// continue if class is not INTERNET // continue if class is not INTERNET
if question.Qclass != dns.ClassINET && question.Qclass != DNSClassMulticast { if question.Qclass != dns.ClassINET && question.Qclass != DNSClassMulticast {
// log.Tracef("intel: mdns question is not of class INET, ignoring")
continue continue
} }
// mark request to be saved
saveFullRequest = true saveFullRequest = true
} }
@ -144,7 +199,7 @@ func listenToMDNS() {
} }
for _, entry := range message.Answer { for _, entry := range message.Answer {
if strings.HasSuffix(entry.Header().Name, ".local.") || domainInScopes(entry.Header().Name, localReverseScopes) { if strings.HasSuffix(entry.Header().Name, ".local.") || domainInScope(entry.Header().Name, localReverseScopes) {
if saveFullRequest { if saveFullRequest {
k := indexOfRR(entry.Header(), &rrCache.Answer) k := indexOfRR(entry.Header(), &rrCache.Answer)
if k == -1 { if k == -1 {
@ -166,7 +221,7 @@ func listenToMDNS() {
} }
} }
for _, entry := range message.Ns { for _, entry := range message.Ns {
if strings.HasSuffix(entry.Header().Name, ".local.") || domainInScopes(entry.Header().Name, localReverseScopes) { if strings.HasSuffix(entry.Header().Name, ".local.") || domainInScope(entry.Header().Name, localReverseScopes) {
if saveFullRequest { if saveFullRequest {
k := indexOfRR(entry.Header(), &rrCache.Ns) k := indexOfRR(entry.Header(), &rrCache.Ns)
if k == -1 { if k == -1 {
@ -188,7 +243,7 @@ func listenToMDNS() {
} }
} }
for _, entry := range message.Extra { for _, entry := range message.Extra {
if strings.HasSuffix(entry.Header().Name, ".local.") || domainInScopes(entry.Header().Name, localReverseScopes) { if strings.HasSuffix(entry.Header().Name, ".local.") || domainInScope(entry.Header().Name, localReverseScopes) {
if saveFullRequest { if saveFullRequest {
k := indexOfRR(entry.Header(), &rrCache.Extra) k := indexOfRR(entry.Header(), &rrCache.Extra)
if k == -1 { if k == -1 {
@ -213,7 +268,19 @@ func listenToMDNS() {
var questionID string var questionID string
if saveFullRequest { if saveFullRequest {
rrCache.Clean(60) rrCache.Clean(60)
rrCache.Save() err := rrCache.Save()
if err != nil {
log.Warningf("intel: failed to cache RR %s: %s", rrCache.Domain, err)
}
// return finished response
if savedQ != nil {
select {
case savedQ.response <- rrCache:
default:
}
}
questionID = fmt.Sprintf("%s%s", question.Name, dns.Type(question.Qtype).String()) questionID = fmt.Sprintf("%s%s", question.Name, dns.Type(question.Qtype).String())
} }
@ -227,107 +294,122 @@ func listenToMDNS() {
Answer: []dns.RR{v}, Answer: []dns.RR{v},
} }
rrCache.Clean(60) rrCache.Clean(60)
rrCache.Save() err := rrCache.Save()
if err != nil {
log.Warningf("intel: failed to cache RR %s: %s", rrCache.Domain, err)
}
// log.Tracef("intel: mdns scavenged %s", k) // log.Tracef("intel: mdns scavenged %s", k)
} }
} }
cleanSavedQuestions() cleanSavedQuestions()
} }
} }
func listenForDNSPackets(conn *net.UDPConn, messages chan *dns.Msg) { func listenForDNSPackets(conn *net.UDPConn, messages chan *dns.Msg) error {
buf := make([]byte, 65536) buf := make([]byte, 65536)
for { for {
// log.Tracef("debug: listening...")
n, err := conn.Read(buf) n, err := conn.Read(buf)
// n, _, err := conn.ReadFrom(buf)
// n, _, err := conn.ReadFromUDP(buf)
if err != nil { if err != nil {
// log.Tracef("intel: failed to read packet: %s", err) if module.ShutdownInProgress() {
continue return nil
}
log.Debugf("intel: failed to read packet: %s", err)
return err
} }
// log.Tracef("debug: read something...")
message := new(dns.Msg) message := new(dns.Msg)
if err = message.Unpack(buf[:n]); err != nil { if err = message.Unpack(buf[:n]); err != nil {
// log.Tracef("intel: failed to unpack message: %s", err) log.Debugf("intel: failed to unpack message: %s", err)
continue continue
} }
// log.Tracef("debug: parsed message...")
messages <- message messages <- message
} }
} }
func queryMulticastDNS(ctx context.Context, fqdn string, qtype dns.Type) (*RRCache, error) { func queryMulticastDNS(ctx context.Context, q *Query) (*RRCache, error) {
log.Tracer(ctx).Trace("intel: resolving with mDNS") // check for active connections
q := new(dns.Msg)
q.SetQuestion(fqdn, uint16(qtype))
// request unicast response
// q.Question[0].Qclass |= 1 << 15
q.RecursionDesired = false
saveQuestion(q)
questionsLock.Lock()
defer questionsLock.Unlock()
questions[q.MsgHdr.Id] = savedQuestion{
question: q.Question[0],
expires: time.Now().Add(10 * time.Second).Unix(),
}
buf, err := q.Pack()
if err != nil {
return nil, fmt.Errorf("failed to pack query: %s", err)
}
if unicast4Conn == nil && unicast6Conn == nil { if unicast4Conn == nil && unicast6Conn == nil {
return nil, errors.New("unicast mdns connections not initialized") return nil, errors.New("unicast mdns connections not initialized")
} }
if unicast4Conn != nil && uint16(qtype) != dns.TypeAAAA {
unicast4Conn.SetWriteDeadline(time.Now().Add(1 * time.Second)) // trace log
log.Tracer(ctx).Trace("intel: resolving with mDNS")
// create query
dnsQuery := new(dns.Msg)
dnsQuery.SetQuestion(q.FQDN, uint16(q.QType))
// request unicast response
// q.Question[0].Qclass |= 1 << 15
dnsQuery.RecursionDesired = false
// create response channel
response := make(chan *RRCache)
// save question
questionsLock.Lock()
defer questionsLock.Unlock()
questions[dnsQuery.MsgHdr.Id] = &savedQuestion{
question: dnsQuery.Question[0],
expires: time.Now().Add(10 * time.Second),
response: response,
}
// pack qeury
buf, err := dnsQuery.Pack()
if err != nil {
return nil, fmt.Errorf("failed to pack query: %s", err)
}
// send queries
if unicast4Conn != nil && uint16(q.QType) != dns.TypeAAAA {
err = unicast4Conn.SetWriteDeadline(time.Now().Add(1 * time.Second))
if err != nil {
return nil, fmt.Errorf("failed to configure query (set timout): %s", err)
}
_, err = unicast4Conn.WriteToUDP(buf, &net.UDPAddr{IP: net.IPv4(224, 0, 0, 251), Port: 5353}) _, err = unicast4Conn.WriteToUDP(buf, &net.UDPAddr{IP: net.IPv4(224, 0, 0, 251), Port: 5353})
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to send query: %s", err) return nil, fmt.Errorf("failed to send query: %s", err)
} }
} }
if unicast6Conn != nil && uint16(qtype) != dns.TypeA { if unicast6Conn != nil && uint16(q.QType) != dns.TypeA {
unicast6Conn.SetWriteDeadline(time.Now().Add(1 * time.Second)) err = unicast6Conn.SetWriteDeadline(time.Now().Add(1 * time.Second))
if err != nil {
return nil, fmt.Errorf("failed to configure query (set timout): %s", err)
}
_, err = unicast6Conn.WriteToUDP(buf, &net.UDPAddr{IP: net.IP([]byte{0xff, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xfb}), Port: 5353}) _, err = unicast6Conn.WriteToUDP(buf, &net.UDPAddr{IP: net.IP([]byte{0xff, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xfb}), Port: 5353})
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to send query: %s", err) return nil, fmt.Errorf("failed to send query: %s", err)
} }
} }
time.Sleep(1 * time.Second) // wait for response or timeout
select {
rrCache, err := GetRRCache(fqdn, qtype) case rrCache := <-response:
if err == nil { if rrCache != nil {
return rrCache, nil
}
case <-time.After(1 * time.Second):
// check cache again
rrCache, err := GetRRCache(q.FQDN, q.QType)
if err != nil {
return rrCache, nil return rrCache, nil
} }
return nil, nil
} }
func saveQuestion(q *dns.Msg) { return nil, ErrNotFound
questionsLock.Lock()
defer questionsLock.Unlock()
// log.Tracef("intel: saving mdns question id=%d, name=%s", q.MsgHdr.Id, q.Question[0].Name)
questions[q.MsgHdr.Id] = savedQuestion{
question: q.Question[0],
expires: time.Now().Add(10 * time.Second).Unix(),
}
} }
func cleanSavedQuestions() { func cleanSavedQuestions() {
questionsLock.Lock() questionsLock.Lock()
defer questionsLock.Unlock() defer questionsLock.Unlock()
now := time.Now().Unix() now := time.Now()
for k, v := range questions { for msgID, savedQuestion := range questions {
if v.expires < now { if now.After(savedQuestion.expires) {
delete(questions, k) delete(questions, msgID)
} }
} }
} }

View file

@ -2,10 +2,8 @@ package intel
import ( import (
"context" "context"
"errors"
"fmt" "fmt"
"math/rand"
"net"
"strings"
"sync" "sync"
"time" "time"
@ -13,362 +11,261 @@ import (
"github.com/safing/portbase/database" "github.com/safing/portbase/database"
"github.com/safing/portbase/log" "github.com/safing/portbase/log"
"github.com/safing/portmaster/status"
) )
// TODO: make resolver interface for http package var (
mtAsyncResolve = "async resolve"
// special tlds: // basic errors
// localhost. [RFC6761] - respond with 127.0.0.1 and ::1 to A and AAAA queries, else nxdomain // ErrNotFound is a basic error that will match all "not found" errors
ErrNotFound = errors.New("record does not exist")
// ErrBlocked is basic error that will match all "blocked" errors
ErrBlocked = errors.New("query was blocked")
// ErrLocalhost is returned to *.localhost queries
ErrLocalhost = errors.New("query for localhost")
// local. [RFC6762] - resolve if search, else resolve with mdns // detailed errors
// 10.in-addr.arpa. [RFC6761]
// 16.172.in-addr.arpa. [RFC6761]
// 17.172.in-addr.arpa. [RFC6761]
// 18.172.in-addr.arpa. [RFC6761]
// 19.172.in-addr.arpa. [RFC6761]
// 20.172.in-addr.arpa. [RFC6761]
// 21.172.in-addr.arpa. [RFC6761]
// 22.172.in-addr.arpa. [RFC6761]
// 23.172.in-addr.arpa. [RFC6761]
// 24.172.in-addr.arpa. [RFC6761]
// 25.172.in-addr.arpa. [RFC6761]
// 26.172.in-addr.arpa. [RFC6761]
// 27.172.in-addr.arpa. [RFC6761]
// 28.172.in-addr.arpa. [RFC6761]
// 29.172.in-addr.arpa. [RFC6761]
// 30.172.in-addr.arpa. [RFC6761]
// 31.172.in-addr.arpa. [RFC6761]
// 168.192.in-addr.arpa. [RFC6761]
// 254.169.in-addr.arpa. [RFC6762]
// 8.e.f.ip6.arpa. [RFC6762]
// 9.e.f.ip6.arpa. [RFC6762]
// a.e.f.ip6.arpa. [RFC6762]
// b.e.f.ip6.arpa. [RFC6762]
// example. [RFC6761] - resolve if search, else return nxdomain // ErrTestDomainsDisabled wraps ErrBlocked
// example.com. [RFC6761] - resolve if search, else return nxdomain ErrTestDomainsDisabled = fmt.Errorf("%w: test domains disabled", ErrBlocked)
// example.net. [RFC6761] - resolve if search, else return nxdomain // ErrSpecialDomainsDisabled wraps ErrBlocked
// example.org. [RFC6761] - resolve if search, else return nxdomain ErrSpecialDomainsDisabled = fmt.Errorf("%w: special domains disabled", ErrBlocked)
// invalid. [RFC6761] - resolve if search, else return nxdomain // ErrInvalid wraps ErrNotFound
// test. [RFC6761] - resolve if search, else return nxdomain ErrInvalid = fmt.Errorf("%w: invalid request", ErrNotFound)
// onion. [RFC7686] - resolve if search, else return nxdomain // ErrNoCompliance wraps ErrBlocked and is returned when no resolvers were able to comply with the current settings
ErrNoCompliance = fmt.Errorf("%w: no compliant resolvers for this query", ErrBlocked)
)
// resolvers: type Query struct {
// local FQDN string
// global QType dns.Type
// mdns SecurityLevel uint8
NoCaching bool
IgnoreFailing bool
LocalResolversOnly bool
// scopes: // internal
// local-inaddr -> local, mdns dotPrefixedFQDN string
// local -> local scopes, mdns }
// global -> local scopes, global
// special -> local scopes, local // check runs sanity checks and does some initialization. Returns whether the query passed the basic checks.
func (q *Query) check() (ok bool) {
if q.FQDN == "" {
return false
}
// init
q.FQDN = dns.Fqdn(q.FQDN)
if q.FQDN == "." {
q.dotPrefixedFQDN = q.FQDN
} else {
q.dotPrefixedFQDN = "." + q.FQDN
}
return true
}
// Resolve resolves the given query for a domain and type and returns a RRCache object or nil, if the query failed. // Resolve resolves the given query for a domain and type and returns a RRCache object or nil, if the query failed.
func Resolve(ctx context.Context, fqdn string, qtype dns.Type, securityLevel uint8) *RRCache { func Resolve(ctx context.Context, q *Query) (rrCache *RRCache, err error) {
fqdn = dns.Fqdn(fqdn) // sanity check
if q == nil || !q.check() {
return nil, ErrInvalid
}
// use this to time how long it takes resolve this domain // log
// timed := time.Now() log.Tracer(ctx).Tracef("intel: resolving %s%s", q.FQDN, q.QType)
// defer log.Tracef("intel: took %s to get resolve %s%s", time.Now().Sub(timed).String(), fqdn, qtype.String())
// check cache // check query compliance
rrCache, err := GetRRCache(fqdn, qtype) if err = q.checkCompliance(); err != nil {
return nil, err
}
// check the cache
if !q.NoCaching {
rrCache = checkCache(ctx, q)
if rrCache != nil {
rrCache.MixAnswers()
return rrCache, nil
}
// dedupe!
markRequestFinished := deduplicateRequest(ctx, q)
if markRequestFinished == nil {
// we waited for another request, recheck the cache!
rrCache = checkCache(ctx, q)
if rrCache != nil {
rrCache.MixAnswers()
return rrCache, nil
}
// if cache is still empty or non-compliant, go ahead and just query
} else {
// we are the first!
defer markRequestFinished()
}
}
return resolveAndCache(ctx, q)
}
func checkCache(ctx context.Context, q *Query) *RRCache {
rrCache, err := GetRRCache(q.FQDN, q.QType)
// failed to get from cache
if err != nil { if err != nil {
switch err { if err != database.ErrNotFound {
case database.ErrNotFound: log.Tracer(ctx).Warningf("intel: getting RRCache %s%s from database failed: %s", q.FQDN, q.QType.String(), err)
default: log.Warningf("intel: getting RRCache %s%s from database failed: %s", q.FQDN, q.QType.String(), err)
log.Tracer(ctx).Warningf("intel: getting RRCache %s%s from database failed: %s", fqdn, qtype.String(), err)
log.Warningf("intel: getting RRCache %s%s from database failed: %s", fqdn, qtype.String(), err)
} }
return resolveAndCache(ctx, fqdn, qtype, securityLevel) return nil
} }
if rrCache.TTL <= time.Now().Unix() { // get resolver that rrCache was resolved with
log.Tracer(ctx).Tracef("intel: serving from cache, requesting new. TTL=%d, now=%d", rrCache.TTL, time.Now().Unix()) resolver := getResolverByIDWithLocking(rrCache.Server)
// log.Tracef("intel: serving cache, requesting new. TTL=%d, now=%d", rrCache.TTL, time.Now().Unix()) if resolver == nil {
return nil
}
// check compliance of resolver
err = resolver.checkCompliance(ctx, q)
if err != nil {
log.Tracer(ctx).Debugf("intel: cached entry for %s%s does not comply to query parameters: %s", q.FQDN, q.QType.String(), err)
return nil
}
// check if expired
if rrCache.Expired() {
rrCache.Lock()
rrCache.requestingNew = true rrCache.requestingNew = true
go resolveAndCache(nil, fqdn, qtype, securityLevel) rrCache.Unlock()
}
// randomize records to allow dumb clients (who only look at the first record) to reliably connect log.Tracer(ctx).Trace("intel: serving from cache, requesting new")
for i := range rrCache.Answer {
j := rand.Intn(i + 1) // resolve async
rrCache.Answer[i], rrCache.Answer[j] = rrCache.Answer[j], rrCache.Answer[i] module.StartMediumPriorityMicroTask(&mtAsyncResolve, func(ctx context.Context) error {
_, _ = resolveAndCache(ctx, q)
return nil
})
} }
return rrCache return rrCache
} }
func resolveAndCache(ctx context.Context, fqdn string, qtype dns.Type, securityLevel uint8) (rrCache *RRCache) { func deduplicateRequest(ctx context.Context, q *Query) (finishRequest func()) {
log.Tracer(ctx).Tracef("intel: resolving %s%s", fqdn, qtype.String()) // create identifier key
dupKey := fmt.Sprintf("%s%s", q.FQDN, q.QType.String())
// dedup requests
dupKey := fmt.Sprintf("%s%s", fqdn, qtype.String())
dupReqLock.Lock() dupReqLock.Lock()
mutex, requestActive := dupReqMap[dupKey] defer dupReqLock.Unlock()
if !requestActive {
mutex = new(sync.Mutex) // get duplicate request waitgroup
mutex.Lock() wg, requestActive := dupReqMap[dupKey]
dupReqMap[dupKey] = mutex
dupReqLock.Unlock() // someone else is already on it!
} else { if requestActive {
dupReqLock.Unlock() // log that we are waiting
log.Tracer(ctx).Tracef("intel: waiting for duplicate query for %s to complete", dupKey) log.Tracer(ctx).Tracef("intel: waiting for duplicate query for %s to complete", dupKey)
// log.Tracef("intel: waiting for duplicate query for %s to complete", dupKey) // wait
mutex.Lock() wg.Wait()
// wait until duplicate request is finished, then fetch current RRCache and return // done!
mutex.Unlock()
var err error
rrCache, err = GetRRCache(dupKey, qtype)
if err == nil {
return rrCache
}
// must have been nxdomain if we cannot get RRCache
return nil return nil
} }
defer func() {
// we are currently the only one doing a request for this
// create new waitgroup
wg = new(sync.WaitGroup)
// add worker (us!)
wg.Add(1)
// add to registry
dupReqMap[dupKey] = wg
// return function to mark request as finished
return func() {
dupReqLock.Lock() dupReqLock.Lock()
defer dupReqLock.Unlock()
// mark request as done
wg.Done()
// delete from registry
delete(dupReqMap, dupKey) delete(dupReqMap, dupKey)
dupReqLock.Unlock()
mutex.Unlock()
}()
// resolve
rrCache = intelligentResolve(ctx, fqdn, qtype, securityLevel)
if rrCache == nil {
return nil
}
// persist to database
rrCache.Clean(600)
rrCache.Save()
return rrCache
}
func intelligentResolve(ctx context.Context, fqdn string, qtype dns.Type, securityLevel uint8) *RRCache {
// TODO: handle being offline
// TODO: handle multiple network connections
// TODO: handle these in a separate goroutine
// if config.Changed() {
// log.Info("intel: config changed, reloading resolvers")
// loadResolvers(false)
// } else if env.NetworkChanged() {
// log.Info("intel: network changed, reloading resolvers")
// loadResolvers(true)
// }
resolversLock.RLock()
defer resolversLock.RUnlock()
lastFailBoundary := time.Now().Unix() - nameserverRetryRate()
preDottedFqdn := "." + fqdn
// resolve:
// reverse local -> local, mdns
// local -> local scopes, mdns
// special -> local scopes, local
// global -> local scopes, global
// local reverse scope
if domainInScopes(preDottedFqdn, localReverseScopes) {
// try local resolvers
for _, resolver := range localResolvers {
rrCache, ok := tryResolver(ctx, resolver, lastFailBoundary, fqdn, qtype, securityLevel)
if ok && rrCache != nil && !rrCache.IsNXDomain() {
return rrCache
}
}
// check config
if doNotUseMulticastDNS(securityLevel) {
return nil
}
// try mdns
rrCache, err := queryMulticastDNS(ctx, fqdn, qtype)
if err != nil {
log.Tracer(ctx).Warningf("intel: failed to query mdns: %s", err)
log.Errorf("intel: failed to query mdns: %s", err)
}
return rrCache
}
// local scopes
for _, scope := range localScopes {
if strings.HasSuffix(preDottedFqdn, scope.Domain) {
for _, resolver := range scope.Resolvers {
rrCache, ok := tryResolver(ctx, resolver, lastFailBoundary, fqdn, qtype, securityLevel)
if ok && rrCache != nil && !rrCache.IsNXDomain() {
return rrCache
}
}
} }
} }
switch { func resolveAndCache(ctx context.Context, q *Query) (rrCache *RRCache, err error) {
case strings.HasSuffix(preDottedFqdn, ".local."): // get resolvers
// check config resolvers := GetResolversInScope(ctx, q)
if doNotUseMulticastDNS(securityLevel) { if len(resolvers) == 0 {
return nil return nil, ErrNoCompliance
}
// try mdns
rrCache, err := queryMulticastDNS(ctx, fqdn, qtype)
if err != nil {
log.Tracer(ctx).Warningf("intel: failed to query mdns: %s", err)
log.Errorf("intel: failed to query mdns: %s", err)
}
return rrCache
case domainInScopes(preDottedFqdn, specialScopes):
// check config
if doNotResolveSpecialDomains(securityLevel) {
return nil
}
// try local resolvers
for _, resolver := range localResolvers {
rrCache, ok := tryResolver(ctx, resolver, lastFailBoundary, fqdn, qtype, securityLevel)
if ok {
return rrCache
}
}
default:
// try global resolvers
for _, resolver := range globalResolvers {
rrCache, ok := tryResolver(ctx, resolver, lastFailBoundary, fqdn, qtype, securityLevel)
if ok {
return rrCache
}
}
} }
log.Tracer(ctx).Warningf("intel: failed to resolve %s%s: all resolvers failed (or were skipped to fulfill the security level)", fqdn, qtype.String()) // prep
log.Criticalf("intel: failed to resolve %s%s: all resolvers failed (or were skipped to fulfill the security level), resetting servers...", fqdn, qtype.String()) lastFailBoundary := time.Now().Add(
go resetResolverFailStatus() -time.Duration(nameserverRetryRate()) * time.Second,
)
return nil // start resolving
// TODO: check if there would be resolvers available in lower security modes and alert user var i int
} // once with skipping recently failed resolvers, once without
resolveLoop:
func tryResolver(ctx context.Context, resolver *Resolver, lastFailBoundary int64, fqdn string, qtype dns.Type, securityLevel uint8) (*RRCache, bool) { for i = 0; i < 2; i++ {
log.Tracer(ctx).Tracef("intel: resolving with %s", resolver) for _, resolver := range resolvers {
// check if resolver failed recently (on first run)
// skip if not security level denies insecure protocols if i == 0 && resolver.Conn.LastFail().After(lastFailBoundary) {
if doNotUseInsecureProtocols(securityLevel) && resolver.ServerType == "dns" {
log.Tracer(ctx).Tracef("intel: skipping resolver %s, because it isn't allowed to operate on the current security level: %d|%d", resolver, status.ActiveSecurityLevel(), securityLevel)
return nil, false
}
// skip if not security level denies assigned dns servers
if doNotUseAssignedNameservers(securityLevel) && resolver.Source == "dhcp" {
log.Tracer(ctx).Tracef("intel: skipping resolver %s, because assigned nameservers are not allowed on the current security level: %d|%d", resolver, status.ActiveSecurityLevel(), securityLevel)
return nil, false
}
// check if failed recently
if resolver.LastFail() > lastFailBoundary {
log.Tracer(ctx).Tracef("intel: skipping resolver %s, because it failed recently", resolver) log.Tracer(ctx).Tracef("intel: skipping resolver %s, because it failed recently", resolver)
return nil, false
}
// TODO: put SkipFqdnBeforeInit back into !resolver.Initialized.IsSet() as soon as Go1.9 arrives and we can use a custom resolver
// skip resolver if initializing and fqdn is set to skip
if fqdn == resolver.SkipFqdnBeforeInit {
log.Tracer(ctx).Tracef("intel: skipping resolver %s, because %s is set to be skipped before init", resolver, fqdn)
return nil, false
}
// check if resolver is already initialized
if !resolver.Initialized() {
// first should init, others wait
resolver.InitLock.Lock()
if resolver.Initialized() {
// unlock immediately if resolver was initialized
resolver.InitLock.Unlock()
} else {
// initialize and unlock when finished
defer resolver.InitLock.Unlock()
}
// check if previous init failed
if resolver.LastFail() > lastFailBoundary {
return nil, false
}
}
// resolve
rrCache, err := query(ctx, resolver, fqdn, qtype)
if err != nil {
// check if failing is disabled
if resolver.LastFail() == -1 {
log.Tracer(ctx).Tracef("intel: non-failing resolver %s failed, moving to next: %s", resolver, err)
// log.Tracef("intel: non-failing resolver %s failed (%s), moving to next", resolver, err)
return nil, false
}
log.Tracer(ctx).Warningf("intel: resolver %s failed, moving to next: %s", resolver, err)
log.Warningf("intel: resolver %s failed, moving to next: %s", resolver, err)
resolver.Lock()
resolver.failReason = err.Error()
resolver.lastFail = time.Now().Unix()
resolver.initialized = false
resolver.Unlock()
return nil, false
}
resolver.Lock()
resolver.initialized = true
resolver.Unlock()
return rrCache, true
}
func query(ctx context.Context, resolver *Resolver, fqdn string, qtype dns.Type) (*RRCache, error) {
q := new(dns.Msg)
q.SetQuestion(fqdn, uint16(qtype))
var reply *dns.Msg
var err error
for i := 0; i < 3; i++ {
// log query time
// qStart := time.Now()
reply, _, err = resolver.clientManager.getDNSClient().Exchange(q, resolver.ServerAddress)
// log.Tracef("intel: query to %s took %s", resolver.Server, time.Now().Sub(qStart))
// error handling
if err != nil {
log.Tracer(ctx).Tracef("intel: query to %s encountered error: %s", resolver.Server, err)
// TODO: handle special cases
// 1. connect: network is unreachable
// 2. timeout
// temporary error
if nerr, ok := err.(net.Error); ok && nerr.Timeout() {
log.Tracer(ctx).Tracef("intel: retrying to resolve %s%s with %s, error is temporary", fqdn, qtype, resolver.Server)
continue continue
} }
// permanent error // resolve
break rrCache, err = resolver.Conn.Query(ctx, q)
} if err != nil {
// FIXME: check if we are online?
switch {
case errors.Is(err, ErrNotFound):
// NXDomain, or similar
return nil, err
case errors.Is(err, ErrBlocked):
// some resolvers might also block
return nil, err
}
} else {
// no error // no error
break if rrCache == nil {
// defensive: assume NXDomain
return nil, ErrNotFound
}
break resolveLoop
}
}
} }
// tried all resolvers, possibly twice
if i > 1 {
return nil, fmt.Errorf("all %d query-compliant resolvers failed, last error: %s", len(resolvers), err)
}
// check for error
if err != nil { if err != nil {
return nil, err return nil, err
} }
new := &RRCache{ // check for result
Domain: fqdn, if rrCache == nil /* defensive */ {
Question: qtype, return nil, ErrNotFound
Answer: reply.Answer,
Ns: reply.Ns,
Extra: reply.Extra,
Server: resolver.Server,
ServerScope: resolver.ServerIPScope,
} }
// TODO: check if reply.Answer is valid // cache if enabled
return new, nil if !q.NoCaching {
// persist to database
rrCache.Clean(600)
err = rrCache.Save()
if err != nil {
log.Warningf("intel: failed to cache RR for %s%s: %s", q.FQDN, q.QType.String(), err)
}
}
return rrCache, nil
} }

276
intel/resolver-scopes.go Normal file
View file

@ -0,0 +1,276 @@
package intel
import (
"context"
"errors"
"strings"
"github.com/miekg/dns"
"github.com/safing/portbase/log"
)
// special scopes:
// localhost. [RFC6761] - respond with 127.0.0.1 and ::1 to A and AAAA queries, else nxdomain
// local. [RFC6762] - resolve if search, else resolve with mdns
// 10.in-addr.arpa. [RFC6761]
// 16.172.in-addr.arpa. [RFC6761]
// 17.172.in-addr.arpa. [RFC6761]
// 18.172.in-addr.arpa. [RFC6761]
// 19.172.in-addr.arpa. [RFC6761]
// 20.172.in-addr.arpa. [RFC6761]
// 21.172.in-addr.arpa. [RFC6761]
// 22.172.in-addr.arpa. [RFC6761]
// 23.172.in-addr.arpa. [RFC6761]
// 24.172.in-addr.arpa. [RFC6761]
// 25.172.in-addr.arpa. [RFC6761]
// 26.172.in-addr.arpa. [RFC6761]
// 27.172.in-addr.arpa. [RFC6761]
// 28.172.in-addr.arpa. [RFC6761]
// 29.172.in-addr.arpa. [RFC6761]
// 30.172.in-addr.arpa. [RFC6761]
// 31.172.in-addr.arpa. [RFC6761]
// 168.192.in-addr.arpa. [RFC6761]
// 254.169.in-addr.arpa. [RFC6762]
// 8.e.f.ip6.arpa. [RFC6762]
// 9.e.f.ip6.arpa. [RFC6762]
// a.e.f.ip6.arpa. [RFC6762]
// b.e.f.ip6.arpa. [RFC6762]
// example. [RFC6761] - resolve if search, else return nxdomain
// example.com. [RFC6761] - resolve if search, else return nxdomain
// example.net. [RFC6761] - resolve if search, else return nxdomain
// example.org. [RFC6761] - resolve if search, else return nxdomain
// invalid. [RFC6761] - resolve if search, else return nxdomain
// test. [RFC6761] - resolve if search, else return nxdomain
// onion. [RFC7686] - resolve if search, else return nxdomain
// resolvers:
// local
// global
// mdns
var (
// RFC6761 - respond with 127.0.0.1 and ::1 to A and AAAA queries respectively, else nxdomain
localhost = ".localhost."
// RFC6761 - always respond with nxdomain
invalid = ".invalid."
// RFC6762 - resolve locally
local = ".local."
// local reverse dns
localReverseScopes = []string{
".10.in-addr.arpa.", // RFC6761
".16.172.in-addr.arpa.", // RFC6761
".17.172.in-addr.arpa.", // RFC6761
".18.172.in-addr.arpa.", // RFC6761
".19.172.in-addr.arpa.", // RFC6761
".20.172.in-addr.arpa.", // RFC6761
".21.172.in-addr.arpa.", // RFC6761
".22.172.in-addr.arpa.", // RFC6761
".23.172.in-addr.arpa.", // RFC6761
".24.172.in-addr.arpa.", // RFC6761
".25.172.in-addr.arpa.", // RFC6761
".26.172.in-addr.arpa.", // RFC6761
".27.172.in-addr.arpa.", // RFC6761
".28.172.in-addr.arpa.", // RFC6761
".29.172.in-addr.arpa.", // RFC6761
".30.172.in-addr.arpa.", // RFC6761
".31.172.in-addr.arpa.", // RFC6761
".168.192.in-addr.arpa.", // RFC6761
".254.169.in-addr.arpa.", // RFC6762
".8.e.f.ip6.arpa.", // RFC6762
".9.e.f.ip6.arpa.", // RFC6762
".a.e.f.ip6.arpa.", // RFC6762
".b.e.f.ip6.arpa.", // RFC6762
}
// RFC6761 - only resolve locally
localTestScopes = []string{
".example.",
".example.com.",
".example.net.",
".example.org.",
".test.",
}
// resolve globally - resolving these should be disabled by default
specialServiceScopes = []string{
".onion.", // Tor Hidden Services, RFC7686
".bit.", // Namecoin, https://www.namecoin.org/
}
)
func domainInScope(dotPrefixedFQDN string, scopeList []string) bool {
for _, scope := range scopeList {
if strings.HasSuffix(dotPrefixedFQDN, scope) {
return true
}
}
return false
}
// GetResolversInScope returns all resolvers that are in scope the resolve the given query and options.
func GetResolversInScope(ctx context.Context, q *Query) (selected []*Resolver) {
resolversLock.RLock()
defer resolversLock.RUnlock()
// resolver selection:
// local -> local scopes, mdns
// local-inaddr -> local, mdns
// global -> local scopes, global
// special -> local scopes, local
// check local scopes
for _, scope := range localScopes {
if strings.HasSuffix(q.dotPrefixedFQDN, scope.Domain) {
// scoped resolvers
for _, resolver := range scope.Resolvers {
if err := resolver.checkCompliance(ctx, q); err == nil {
selected = append(selected, resolver)
} else {
log.Tracef("skipping non-compliant resolver: %s", resolver.Server)
}
}
}
}
// if there was a match with a local scope, stop here
if len(selected) > 0 {
// add mdns
if err := mDNSResolver.checkCompliance(ctx, q); err == nil {
selected = append(selected, mDNSResolver)
} else {
log.Tracef("skipping non-compliant resolver: %s", mDNSResolver.Server)
}
return selected
}
// check local reverse scope
if domainInScope(q.dotPrefixedFQDN, localReverseScopes) {
// local resolvers
for _, resolver := range localResolvers {
if err := resolver.checkCompliance(ctx, q); err == nil {
selected = append(selected, resolver)
} else {
log.Tracef("skipping non-compliant resolver: %s", resolver.Server)
}
}
// mdns resolver
if err := mDNSResolver.checkCompliance(ctx, q); err == nil {
selected = append(selected, mDNSResolver)
} else {
log.Tracef("skipping non-compliant resolver: %s", mDNSResolver.Server)
}
return selected
}
// check for .local mdns
if strings.HasSuffix(q.dotPrefixedFQDN, local) {
// add mdns
if err := mDNSResolver.checkCompliance(ctx, q); err == nil {
selected = append(selected, mDNSResolver)
} else {
log.Tracef("skipping non-compliant resolver: %s", mDNSResolver.Server)
}
return selected
}
// check for test scopes
if domainInScope(q.dotPrefixedFQDN, localTestScopes) {
// local resolvers
for _, resolver := range localResolvers {
if err := resolver.checkCompliance(ctx, q); err == nil {
selected = append(selected, resolver)
} else {
log.Tracef("skipping non-compliant resolver: %s", resolver.Server)
}
}
return selected
}
// finally, query globally
for _, resolver := range globalResolvers {
if err := resolver.checkCompliance(ctx, q); err == nil {
selected = append(selected, resolver)
} else {
log.Tracef("skipping non-compliant resolver: %s", resolver.Server)
}
}
return selected
}
var (
errInsecureProtocol = errors.New("insecure protocols disabled")
errAssignedServer = errors.New("assigned (dhcp) nameservers disabled")
errMulticastDNS = errors.New("multicast DNS disabled")
errSkip = errors.New("this fqdn cannot resolved by this resolver")
)
func (q *Query) checkCompliance() error {
// RFC6761 - always respond with nxdomain
if strings.HasSuffix(q.dotPrefixedFQDN, invalid) {
return ErrNotFound
}
// RFC6761 - respond with 127.0.0.1 and ::1 to A and AAAA queries respectively, else nxdomain
if strings.HasSuffix(q.dotPrefixedFQDN, localhost) {
switch uint16(q.QType) {
case dns.TypeA, dns.TypeAAAA:
return ErrLocalhost
default:
return ErrNotFound
}
}
// special TLDs
if doNotResolveSpecialDomains(q.SecurityLevel) &&
domainInScope(q.dotPrefixedFQDN, specialServiceScopes) {
return ErrSpecialDomainsDisabled
}
// testing TLDs
if doNotResolveTestDomains(q.SecurityLevel) &&
domainInScope(q.dotPrefixedFQDN, localTestScopes) {
return ErrTestDomainsDisabled
}
return nil
}
func (resolver *Resolver) checkCompliance(_ context.Context, q *Query) error {
if q.FQDN == resolver.SkipFQDN {
return errSkip
}
if doNotUseInsecureProtocols(q.SecurityLevel) {
switch resolver.ServerType {
case ServerTypeDNS:
return errInsecureProtocol
case ServerTypeTCP:
return errInsecureProtocol
case ServerTypeDoT:
// compliant
case ServerTypeDoH:
// compliant
default:
return errInsecureProtocol
}
}
if doNotUseAssignedNameservers(q.SecurityLevel) {
if resolver.Source == ServerSourceAssigned {
return errAssignedServer
}
}
if doNotUseMulticastDNS(q.SecurityLevel) {
if resolver.Source == ServerSourceMDNS {
return errMulticastDNS
}
}
return nil
}

View file

@ -1,331 +1,151 @@
package intel package intel
import ( import (
"errors" "context"
"fmt"
"net" "net"
"sort"
"strconv"
"strings"
"sync" "sync"
"time"
"github.com/miekg/dns" "github.com/miekg/dns"
"github.com/safing/portbase/log" "github.com/safing/portbase/log"
"github.com/safing/portmaster/network/environment" "github.com/safing/portmaster/network/environment"
"github.com/safing/portmaster/network/netutils" )
const (
ServerTypeDNS = "dns"
ServerTypeTCP = "tcp"
ServerTypeDoT = "dot"
ServerTypeDoH = "doh"
ServerSourceConfigured = "config"
ServerSourceAssigned = "dhcp"
ServerSourceMDNS = "mdns"
) )
// Resolver holds information about an active resolver. // Resolver holds information about an active resolver.
type Resolver struct { type Resolver struct {
sync.Mutex // Server config url (and ID)
// static
Server string Server string
// Parsed config
ServerType string ServerType string
ServerAddress string ServerAddress string
ServerIP net.IP ServerIP net.IP
ServerIPScope int8 ServerIPScope int8
ServerPort uint16 ServerPort uint16
// Special Options
VerifyDomain string VerifyDomain string
Search []string
SkipFQDN string
Source string Source string
clientManager *clientManager
Search *[]string // logic interface
SkipFqdnBeforeInit string Conn ResolverConn
InitLock sync.Mutex
// must be locked
initialized bool
lastFail int64
failReason string
fails int
expires int64
// TODO: add Expiration (for server got from DHCP / ICMPv6)
} }
// Initialized returns the internal initialized value while locking the Resolver. // String returns the URL representation of the resolver.
func (r *Resolver) Initialized() bool { func (resolver *Resolver) String() string {
r.Lock() return resolver.Server
defer r.Unlock() }
return r.initialized
// ResolverConn is an interface to implement different types of query backends.
type ResolverConn interface {
Query(ctx context.Context, q *Query) (*RRCache, error)
MarkFailed()
LastFail() time.Time
}
// BasicResolverConn implements ResolverConn for standard dns clients.
type BasicResolverConn struct {
sync.Mutex // for lastFail
resolver *Resolver
clientManager *clientManager
lastFail time.Time
}
// MarkFailed marks the resolver as failed.
func (brc *BasicResolverConn) MarkFailed() {
if !environment.Online() {
// don't mark failed if we are offline
return
}
brc.Lock()
defer brc.Unlock()
brc.lastFail = time.Now()
} }
// LastFail returns the internal lastfail value while locking the Resolver. // LastFail returns the internal lastfail value while locking the Resolver.
func (r *Resolver) LastFail() int64 { func (brc *BasicResolverConn) LastFail() time.Time {
r.Lock() brc.Lock()
defer r.Unlock() defer brc.Unlock()
return r.lastFail return brc.lastFail
} }
// FailReason returns the internal failreason value while locking the Resolver. func (brc *BasicResolverConn) Query(ctx context.Context, q *Query) (*RRCache, error) {
func (r *Resolver) FailReason() string { // convenience
r.Lock() resolver := brc.resolver
defer r.Unlock()
return r.failReason
}
// Fails returns the internal fails value while locking the Resolver. // create query
func (r *Resolver) Fails() int { dnsQuery := new(dns.Msg)
r.Lock() dnsQuery.SetQuestion(q.FQDN, uint16(q.QType))
defer r.Unlock()
return r.fails
}
// Expires returns the internal expires value while locking the Resolver. // start
func (r *Resolver) Expires() int64 { var reply *dns.Msg
r.Lock() var err error
defer r.Unlock() for i := 0; i < 3; i++ {
return r.expires
}
func (r *Resolver) String() string { // log query time
return r.Server // qStart := time.Now()
} reply, _, err = brc.clientManager.getDNSClient().Exchange(dnsQuery, resolver.ServerAddress)
// log.Tracef("intel: query to %s took %s", resolver.Server, time.Now().Sub(qStart))
// Scope defines a domain scope and which resolvers can resolve it. // error handling
type Scope struct { if err != nil {
Domain string log.Tracer(ctx).Tracef("intel: query to %s encountered error: %s", resolver.Server, err)
Resolvers []*Resolver
}
var ( // TODO: handle special cases
globalResolvers []*Resolver // all resolvers // 1. connect: network is unreachable
localResolvers []*Resolver // all resolvers that are in site-local or link-local IP ranges // 2. timeout
localScopes []*Scope // list of scopes with a list of local resolvers that can resolve the scope
resolversLock sync.RWMutex
env = environment.NewInterface() // hint network environment at failed connection
environment.ReportFailedConnection()
dupReqMap = make(map[string]*sync.Mutex) // temporary error
dupReqLock sync.Mutex if nerr, ok := err.(net.Error); ok && nerr.Timeout() {
) log.Tracer(ctx).Tracef("intel: retrying to resolve %s%s with %s, error is temporary", q.FQDN, q.QType, resolver.Server)
func indexOfResolver(server string, list []*Resolver) int {
for k, v := range list {
if v.Server == server {
return k
}
}
return -1
}
func indexOfScope(domain string, list []*Scope) int {
for k, v := range list {
if v.Domain == domain {
return k
}
}
return -1
}
func parseAddress(server string) (net.IP, uint16, error) {
delimiter := strings.LastIndex(server, ":")
if delimiter < 0 {
return nil, 0, errors.New("port missing")
}
ip := net.ParseIP(strings.Trim(server[:delimiter], "[]"))
if ip == nil {
return nil, 0, errors.New("invalid IP address")
}
port, err := strconv.Atoi(server[delimiter+1:])
if err != nil || port < 1 || port > 65536 {
return nil, 0, errors.New("invalid port")
}
return ip, uint16(port), nil
}
func urlFormatAddress(ip net.IP, port uint16) string {
var address string
if ipv4 := ip.To4(); ipv4 != nil {
address = fmt.Sprintf("%s:%d", ipv4.String(), port)
} else {
address = fmt.Sprintf("[%s]:%d", ip.String(), port)
}
return address
}
func loadResolvers(resetResolvers bool) {
// TODO: what happens when a lot of processes want to reload at once? we do not need to run this multiple times in a short time frame.
resolversLock.Lock()
defer resolversLock.Unlock()
var newResolvers []*Resolver
configuredServersLoop:
for _, server := range configuredNameServers() {
key := indexOfResolver(server, newResolvers)
if key >= 0 {
continue configuredServersLoop
}
key = indexOfResolver(server, globalResolvers)
if resetResolvers || key == -1 {
parts := strings.Split(server, "|")
if len(parts) < 2 {
log.Warningf("intel: nameserver format invalid: %s", server)
continue configuredServersLoop
}
ip, port, err := parseAddress(parts[1])
if err != nil && strings.ToLower(parts[0]) != "https" {
log.Warningf("intel: nameserver (%s) address invalid: %s", server, err)
continue configuredServersLoop
}
new := &Resolver{
Server: server,
ServerType: strings.ToLower(parts[0]),
ServerAddress: parts[1],
ServerIP: ip,
ServerIPScope: netutils.ClassifyIP(ip),
ServerPort: port,
Source: "config",
}
switch new.ServerType {
case "dns":
new.clientManager = newDNSClientManager(new)
case "tcp":
new.clientManager = newTCPClientManager(new)
case "tls":
if len(parts) < 3 {
log.Warningf("intel: nameserver missing verification domain as third parameter: %s", server)
continue configuredServersLoop
}
new.VerifyDomain = parts[2]
new.clientManager = newTLSClientManager(new)
case "https":
new.SkipFqdnBeforeInit = dns.Fqdn(strings.Split(parts[1], ":")[0])
if len(parts) > 2 {
new.VerifyDomain = parts[2]
}
new.clientManager = newHTTPSClientManager(new)
default:
log.Warningf("intel: nameserver (%s) type invalid: %s", server, parts[0])
continue configuredServersLoop
}
newResolvers = append(newResolvers, new)
} else {
newResolvers = append(newResolvers, globalResolvers[key])
}
}
// add local resolvers
assignedNameservers := environment.Nameservers()
assignedServersLoop:
for _, nameserver := range assignedNameservers {
server := fmt.Sprintf("dns|%s", urlFormatAddress(nameserver.IP, 53))
key := indexOfResolver(server, newResolvers)
if key >= 0 {
continue assignedServersLoop
}
key = indexOfResolver(server, globalResolvers)
if resetResolvers || key == -1 {
new := &Resolver{
Server: server,
ServerType: "dns",
ServerAddress: urlFormatAddress(nameserver.IP, 53),
ServerIP: nameserver.IP,
ServerIPScope: netutils.ClassifyIP(nameserver.IP),
ServerPort: 53,
Source: "dhcp",
}
new.clientManager = newDNSClientManager(new)
if netutils.IPIsLAN(nameserver.IP) && len(nameserver.Search) > 0 {
// only allow searches for local resolvers
var newSearch []string
for _, value := range nameserver.Search {
newSearch = append(newSearch, fmt.Sprintf(".%s.", strings.Trim(value, ".")))
}
new.Search = &newSearch
}
newResolvers = append(newResolvers, new)
} else {
newResolvers = append(newResolvers, globalResolvers[key])
}
}
// save resolvers
globalResolvers = newResolvers
if len(globalResolvers) == 0 {
log.Criticalf("intel: no (valid) dns servers found in configuration and system")
}
// make list with local resolvers
localResolvers = make([]*Resolver, 0)
for _, resolver := range globalResolvers {
if resolver.ServerIP != nil && netutils.IPIsLAN(resolver.ServerIP) {
localResolvers = append(localResolvers, resolver)
}
}
// add resolvers to every scope the cover
localScopes = make([]*Scope, 0)
for _, resolver := range globalResolvers {
if resolver.Search != nil {
// add resolver to custom searches
for _, search := range *resolver.Search {
if search == "." {
continue continue
} }
key := indexOfScope(search, localScopes)
if key == -1 { // permanent error
localScopes = append(localScopes, &Scope{ break
Domain: search,
Resolvers: []*Resolver{resolver},
})
} else {
localScopes[key].Resolvers = append(localScopes[key].Resolvers, resolver)
}
} }
} // no error
break
} }
// sort scopes by length if err != nil {
sort.Slice(localScopes, return nil, err
func(i, j int) bool { // FIXME: mark as failed
return len(localScopes[i].Domain) > len(localScopes[j].Domain)
},
)
log.Trace("intel: loaded global resolvers:")
for _, resolver := range globalResolvers {
log.Tracef("intel: %s", resolver.Server)
}
log.Trace("intel: loaded local resolvers:")
for _, resolver := range localResolvers {
log.Tracef("intel: %s", resolver.Server)
}
log.Trace("intel: loaded scopes:")
for _, scope := range localScopes {
var scopeServers []string
for _, resolver := range scope.Resolvers {
scopeServers = append(scopeServers, resolver.Server)
}
log.Tracef("intel: %s: %s", scope.Domain, strings.Join(scopeServers, ", "))
} }
// hint network environment at successful connection
environment.ReportSuccessfulConnection()
new := &RRCache{
Domain: q.FQDN,
Question: q.QType,
Answer: reply.Answer,
Ns: reply.Ns,
Extra: reply.Extra,
Server: resolver.Server,
ServerScope: resolver.ServerIPScope,
} }
// resetResolverFailStatus resets all resolver failures. // TODO: check if reply.Answer is valid
func resetResolverFailStatus() { return new, nil
resolversLock.Lock()
defer resolversLock.Unlock()
log.Tracef("old: %+v %+v, ", globalResolvers, localResolvers)
for _, resolver := range append(globalResolvers, localResolvers...) {
resolver.Lock()
resolver.failReason = ""
resolver.lastFail = 0
resolver.Unlock()
}
log.Tracef("new: %+v %+v, ", globalResolvers, localResolvers)
} }

357
intel/resolvers.go Normal file
View file

@ -0,0 +1,357 @@
package intel
import (
"errors"
"fmt"
"net"
"sort"
"strconv"
"strings"
"sync"
"golang.org/x/net/publicsuffix"
"github.com/miekg/dns"
"github.com/safing/portbase/log"
"github.com/safing/portmaster/network/environment"
"github.com/safing/portmaster/network/netutils"
)
// Scope defines a domain scope and which resolvers can resolve it.
type Scope struct {
Domain string
Resolvers []*Resolver
}
var (
globalResolvers []*Resolver // all (global) resolvers
localResolvers []*Resolver // all resolvers that are in site-local or link-local IP ranges
localScopes []*Scope // list of scopes with a list of local resolvers that can resolve the scope
allResolvers map[string]*Resolver // lookup map of all resolvers
resolversLock sync.RWMutex
dupReqMap = make(map[string]*sync.WaitGroup)
dupReqLock sync.Mutex
)
func indexOfResolver(server string, list []*Resolver) int {
for k, v := range list {
if v.Server == server {
return k
}
}
return -1
}
func indexOfScope(domain string, list []*Scope) int {
for k, v := range list {
if v.Domain == domain {
return k
}
}
return -1
}
func getResolverByIDWithLocking(server string) *Resolver {
resolversLock.Lock()
defer resolversLock.Unlock()
resolver, ok := allResolvers[server]
if ok {
return resolver
}
return nil
}
func parseAddress(server string) (net.IP, uint16, error) {
delimiter := strings.LastIndex(server, ":")
if delimiter < 0 {
return nil, 0, errors.New("port missing")
}
ip := net.ParseIP(strings.Trim(server[:delimiter], "[]"))
if ip == nil {
return nil, 0, errors.New("invalid IP address")
}
port, err := strconv.Atoi(server[delimiter+1:])
if err != nil || port < 1 || port > 65536 {
return nil, 0, errors.New("invalid port")
}
return ip, uint16(port), nil
}
func urlFormatAddress(ip net.IP, port uint16) string {
var address string
if ipv4 := ip.To4(); ipv4 != nil {
address = fmt.Sprintf("%s:%d", ipv4.String(), port)
} else {
address = fmt.Sprintf("[%s]:%d", ip.String(), port)
}
return address
}
//nolint:gocyclo,gocognit
func loadResolvers() {
// TODO: what happens when a lot of processes want to reload at once? we do not need to run this multiple times in a short time frame.
resolversLock.Lock()
defer resolversLock.Unlock()
var newResolvers []*Resolver
configuredServersLoop:
for _, server := range configuredNameServers() {
key := indexOfResolver(server, newResolvers)
if key >= 0 {
continue configuredServersLoop
}
key = indexOfResolver(server, globalResolvers)
if key == -1 {
parts := strings.Split(server, "|")
if len(parts) < 2 {
log.Warningf("intel: nameserver format invalid: %s", server)
continue configuredServersLoop
}
var ipScope int8
ip, port, err := parseAddress(parts[1])
if err == nil {
ipScope = netutils.ClassifyIP(ip)
if ipScope == netutils.HostLocal {
log.Warningf(`intel: cannot use configured localhost nameserver "%s"`, server)
continue configuredServersLoop
}
} else {
if strings.ToLower(parts[0]) == "doh" {
ipScope = netutils.Global
} else {
log.Warningf("intel: nameserver (%s) address invalid: %s", server, err)
continue configuredServersLoop
}
}
// create new structs
newConn := &BasicResolverConn{}
new := &Resolver{
Server: server,
ServerType: strings.ToLower(parts[0]),
ServerAddress: parts[1],
ServerIP: ip,
ServerIPScope: ipScope,
ServerPort: port,
Source: "config",
Conn: newConn,
}
// refer back
newConn.resolver = new
switch new.ServerType {
case "dns":
newConn.clientManager = newDNSClientManager(new)
case "tcp":
newConn.clientManager = newTCPClientManager(new)
case "dot":
if len(parts) < 3 {
log.Warningf("intel: nameserver missing verification domain as third parameter: %s", server)
continue configuredServersLoop
}
new.VerifyDomain = parts[2]
newConn.clientManager = newTLSClientManager(new)
case "doh":
new.SkipFQDN = dns.Fqdn(strings.Split(parts[1], ":")[0])
if len(parts) > 2 {
new.VerifyDomain = parts[2]
}
newConn.clientManager = newHTTPSClientManager(new)
default:
log.Warningf("intel: nameserver (%s) type invalid: %s", server, parts[0])
continue configuredServersLoop
}
newResolvers = append(newResolvers, new)
} else {
newResolvers = append(newResolvers, globalResolvers[key])
}
}
// add local resolvers
assignedNameservers := environment.Nameservers()
assignedServersLoop:
for _, nameserver := range assignedNameservers {
server := fmt.Sprintf("dns|%s", urlFormatAddress(nameserver.IP, 53))
key := indexOfResolver(server, newResolvers)
if key >= 0 {
continue assignedServersLoop
}
key = indexOfResolver(server, globalResolvers)
if key == -1 {
ipScope := netutils.ClassifyIP(nameserver.IP)
if ipScope == netutils.HostLocal {
log.Infof(`intel: cannot use assigned localhost nameserver at %s`, nameserver.IP)
continue assignedServersLoop
}
// create new structs
newConn := &BasicResolverConn{}
new := &Resolver{
Server: server,
ServerType: "dns",
ServerAddress: urlFormatAddress(nameserver.IP, 53),
ServerIP: nameserver.IP,
ServerIPScope: ipScope,
ServerPort: 53,
Source: "dhcp",
Conn: newConn,
}
// refer back
newConn.resolver = new
// add client manager
newConn.clientManager = newDNSClientManager(new)
if netutils.IPIsLAN(nameserver.IP) && len(nameserver.Search) > 0 {
// only allow searches for local resolvers
for _, value := range nameserver.Search {
trimmedDomain := strings.Trim(value, ".")
if checkSearchScope(trimmedDomain) {
new.Search = append(new.Search, fmt.Sprintf(".%s.", strings.Trim(value, ".")))
}
}
// cap to mitigate exploitation via malicious local resolver
if len(new.Search) > 100 {
new.Search = new.Search[:100]
}
}
newResolvers = append(newResolvers, new)
} else {
newResolvers = append(newResolvers, globalResolvers[key])
}
}
// save resolvers
globalResolvers = newResolvers
if len(globalResolvers) == 0 {
log.Criticalf("intel: no (valid) dns servers found in configuration and system")
}
// make list with local resolvers
localResolvers = make([]*Resolver, 0)
for _, resolver := range globalResolvers {
if resolver.ServerIP != nil && netutils.IPIsLAN(resolver.ServerIP) {
localResolvers = append(localResolvers, resolver)
}
}
// add resolvers to every scope the cover
localScopes = make([]*Scope, 0)
for _, resolver := range globalResolvers {
if resolver.Search != nil {
// add resolver to custom searches
for _, search := range resolver.Search {
if search == "." {
continue
}
key := indexOfScope(search, localScopes)
if key == -1 {
localScopes = append(localScopes, &Scope{
Domain: search,
Resolvers: []*Resolver{resolver},
})
} else {
localScopes[key].Resolvers = append(localScopes[key].Resolvers, resolver)
}
}
}
}
// sort scopes by length
sort.Slice(localScopes,
func(i, j int) bool {
return len(localScopes[i].Domain) > len(localScopes[j].Domain)
},
)
// log global resolvers
if len(globalResolvers) > 0 {
log.Trace("intel: loaded global resolvers:")
for _, resolver := range globalResolvers {
log.Tracef("intel: %s", resolver.Server)
}
} else {
log.Warning("intel: no global resolvers loaded")
}
// log local resolvers
if len(localResolvers) > 0 {
log.Trace("intel: loaded local resolvers:")
for _, resolver := range localResolvers {
log.Tracef("intel: %s", resolver.Server)
}
} else {
log.Info("intel: no local resolvers loaded")
}
// log scopes
if len(localScopes) > 0 {
log.Trace("intel: loaded scopes:")
for _, scope := range localScopes {
var scopeServers []string
for _, resolver := range scope.Resolvers {
scopeServers = append(scopeServers, resolver.Server)
}
log.Tracef("intel: %s: %s", scope.Domain, strings.Join(scopeServers, ", "))
}
} else {
log.Info("intel: no scopes loaded")
}
// alert if no resolvers are loaded
if len(globalResolvers) == 0 && len(localResolvers) == 0 {
log.Critical("intel: no resolvers loaded!")
}
}
func checkSearchScope(searchDomain string) (ok bool) {
// sanity check
if len(searchDomain) == 0 ||
searchDomain[0] == '.' ||
searchDomain[len(searchDomain)-1] == '.' {
return false
}
// add more subdomains to use official publicsuffix package for our cause
searchDomain = "*.*.*.*.*." + searchDomain
// get suffix
suffix, icann := publicsuffix.PublicSuffix(searchDomain)
// sanity check
if len(suffix) == 0 {
return false
}
// inexistent (custom) tlds are okay
// this will include special service domains! (.onion, .bit, ...)
if !icann && !strings.Contains(suffix, ".") {
return true
}
// check if suffix is a special service domain (may be handled fully by local nameserver)
if domainInScope("."+suffix+".", specialServiceScopes) {
return true
}
// build eTLD+1
split := len(searchDomain) - len(suffix) - 1
eTLDplus1 := searchDomain[1+strings.LastIndex(searchDomain[:split], "."):]
// scope check
//nolint:gosimple // want comment
if strings.Contains(eTLDplus1, "*") {
// oops, search domain is too high up the hierarchy
return false
}
return true
}

36
intel/resolvers_test.go Normal file
View file

@ -0,0 +1,36 @@
package intel
import "testing"
func TestCheckResolverSearchScope(t *testing.T) {
test := func(t *testing.T, domain string, expectedResult bool) {
if checkSearchScope(domain) != expectedResult {
if expectedResult {
t.Errorf("domain %s failed scope test", domain)
} else {
t.Errorf("domain %s should fail scope test", domain)
}
}
}
// should fail (invalid)
test(t, ".", false)
test(t, ".com.", false)
test(t, "com.", false)
test(t, ".com", false)
// should succeed
test(t, "a.com", true)
test(t, "b.a.com", true)
test(t, "c.b.a.com", true)
test(t, "onion", true)
test(t, "a.onion", true)
test(t, "b.a.onion", true)
test(t, "c.b.a.onion", true)
test(t, "bit", true)
test(t, "a.bit", true)
test(t, "b.a.bit", true)
test(t, "c.b.a.bit", true)
}

View file

@ -1,26 +1,32 @@
package intel package intel
import ( import (
"errors" "context"
"fmt"
"strings" "strings"
"github.com/safing/portbase/log"
"github.com/miekg/dns" "github.com/miekg/dns"
"github.com/safing/portbase/log"
) )
// ResolveIPAndValidate finds (reverse DNS), validates (forward DNS) and returns the domain name assigned to the given IP. // ResolveIPAndValidate finds (reverse DNS), validates (forward DNS) and returns the domain name assigned to the given IP.
func ResolveIPAndValidate(ip string, securityLevel uint8) (domain string, err error) { func ResolveIPAndValidate(ctx context.Context, ip string, securityLevel uint8) (domain string, err error) {
// get reversed DNS address // get reversed DNS address
rQ, err := dns.ReverseAddr(ip) reverseIP, err := dns.ReverseAddr(ip)
if err != nil { if err != nil {
log.Tracef("intel: failed to get reverse address of %s: %s", ip, err) log.Tracef("intel: failed to get reverse address of %s: %s", ip, err)
return "", err return "", ErrInvalid
} }
// get PTR record // get PTR record
rrCache := Resolve(nil, rQ, dns.Type(dns.TypePTR), securityLevel) q := &Query{
if rrCache == nil { FQDN: reverseIP,
return "", errors.New("querying for PTR record failed (may be NXDomain)") QType: dns.Type(dns.TypePTR),
SecurityLevel: securityLevel,
}
rrCache, err := Resolve(ctx, q)
if err != nil || rrCache == nil {
return "", fmt.Errorf("failed to resolve %s%s: %w", q.FQDN, q.QType, err)
} }
// get result from record // get result from record
@ -35,23 +41,27 @@ func ResolveIPAndValidate(ip string, securityLevel uint8) (domain string, err er
// check for nxDomain // check for nxDomain
if ptrName == "" { if ptrName == "" {
return "", errors.New("no PTR record for IP (nxDomain)") return "", fmt.Errorf("%w: %s%s", ErrNotFound, q.FQDN, q.QType)
} }
log.Infof("ptrName: %s", ptrName)
// get forward record // get forward record
if strings.Contains(ip, ":") { q = &Query{
rrCache = Resolve(nil, ptrName, dns.Type(dns.TypeAAAA), securityLevel) FQDN: ptrName,
} else { SecurityLevel: securityLevel,
rrCache = Resolve(nil, ptrName, dns.Type(dns.TypeA), securityLevel)
} }
if rrCache == nil { // IPv4/6 switch
return "", errors.New("querying for A/AAAA record failed (may be NXDomain)") if strings.Contains(ip, ":") {
q.QType = dns.Type(dns.TypeAAAA)
} else {
q.QType = dns.Type(dns.TypeA)
}
// resolve
rrCache, err = Resolve(ctx, q)
if err != nil || rrCache == nil {
return "", fmt.Errorf("failed to resolve %s%s: %w", q.FQDN, q.QType, err)
} }
// check for matching A/AAAA record // check for matching A/AAAA record
log.Infof("rr: %s", rrCache)
for _, rr := range rrCache.Answer { for _, rr := range rrCache.Answer {
switch v := rr.(type) { switch v := rr.(type) {
case *dns.A: case *dns.A:
@ -68,5 +78,5 @@ func ResolveIPAndValidate(ip string, securityLevel uint8) (domain string, err er
} }
// no match // no match
return "", errors.New("validation failed") return "", ErrBlocked
} }

View file

@ -1,10 +1,19 @@
package intel package intel
import "testing" import (
"context"
"testing"
"github.com/safing/portbase/log"
)
func testReverse(t *testing.T, ip, result, expectedErr string) { func testReverse(t *testing.T, ip, result, expectedErr string) {
domain, err := ResolveIPAndValidate(ip, 0) ctx, tracer := log.AddTracer(context.Background())
defer tracer.Submit()
domain, err := ResolveIPAndValidate(ctx, ip, 0)
if err != nil { if err != nil {
tracer.Warning(err.Error())
if expectedErr == "" || err.Error() != expectedErr { if expectedErr == "" || err.Error() != expectedErr {
t.Errorf("reverse-validating %s: unexpected error: %s", ip, err) t.Errorf("reverse-validating %s: unexpected error: %s", ip, err)
} }
@ -18,11 +27,11 @@ func testReverse(t *testing.T, ip, result, expectedErr string) {
func TestResolveIPAndValidate(t *testing.T) { func TestResolveIPAndValidate(t *testing.T) {
testReverse(t, "198.41.0.4", "a.root-servers.net.", "") testReverse(t, "198.41.0.4", "a.root-servers.net.", "")
testReverse(t, "9.9.9.9", "dns.quad9.net.", "") // testReverse(t, "9.9.9.9", "dns.quad9.net.", "") // started resolving to dns9.quad9.net.
testReverse(t, "2620:fe::fe", "dns.quad9.net.", "") testReverse(t, "2620:fe::fe", "dns.quad9.net.", "")
testReverse(t, "1.1.1.1", "one.one.one.one.", "") testReverse(t, "1.1.1.1", "one.one.one.one.", "")
testReverse(t, "2606:4700:4700::1111", "one.one.one.one.", "") testReverse(t, "2606:4700:4700::1111", "one.one.one.one.", "")
testReverse(t, "93.184.216.34", "example.com.", "no PTR record for IP (nxDomain)") testReverse(t, "93.184.216.34", "example.com.", "record does not exist: 34.216.184.93.in-addr.arpa.PTR")
testReverse(t, "185.199.109.153", "sites.github.io.", "no PTR record for IP (nxDomain)") testReverse(t, "185.199.109.153", "sites.github.io.", "record does not exist: 153.109.199.185.in-addr.arpa.PTR")
} }

View file

@ -2,40 +2,62 @@ package intel
import ( import (
"fmt" "fmt"
"math/rand"
"net" "net"
"sync"
"time" "time"
"github.com/miekg/dns" "github.com/miekg/dns"
) )
// RRCache is used to cache DNS data // RRCache is used to cache DNS data
//nolint:maligned // TODO
type RRCache struct { type RRCache struct {
Domain string sync.Mutex
Question dns.Type
Answer []dns.RR Domain string // constant
Ns []dns.RR Question dns.Type // constant
Extra []dns.RR
TTL int64
Server string Answer []dns.RR // might be mixed
ServerScope int8 Ns []dns.RR // constant
Extra []dns.RR // constant
TTL int64 // constant
updated int64 Server string // constant
servedFromCache bool ServerScope int8 // constant
requestingNew bool
Filtered bool servedFromCache bool // mutable
FilteredEntries []string requestingNew bool // mutable
Filtered bool // mutable
FilteredEntries []string // mutable
updated int64 // mutable
}
// Expired returns whether the record has expired.
func (rrCache *RRCache) Expired() bool {
return rrCache.TTL <= time.Now().Unix()
}
// MixAnswers randomizes the answer records to allow dumb clients (who only look at the first record) to reliably connect.
func (rrCache *RRCache) MixAnswers() {
rrCache.Lock()
defer rrCache.Unlock()
for i := range rrCache.Answer {
j := rand.Intn(i + 1)
rrCache.Answer[i], rrCache.Answer[j] = rrCache.Answer[j], rrCache.Answer[i]
}
} }
// Clean sets all TTLs to 17 and sets cache expiry with specified minimum. // Clean sets all TTLs to 17 and sets cache expiry with specified minimum.
func (m *RRCache) Clean(minExpires uint32) { func (rrCache *RRCache) Clean(minExpires uint32) {
var lowestTTL uint32 = 0xFFFFFFFF var lowestTTL uint32 = 0xFFFFFFFF
var header *dns.RR_Header var header *dns.RR_Header
// set TTLs to 17 // set TTLs to 17
// TODO: double append? is there something more elegant? // TODO: double append? is there something more elegant?
for _, rr := range append(m.Answer, append(m.Ns, m.Extra...)...) { for _, rr := range append(rrCache.Answer, append(rrCache.Ns, rrCache.Extra...)...) {
header = rr.Header() header = rr.Header()
if lowestTTL > header.Ttl { if lowestTTL > header.Ttl {
lowestTTL = header.Ttl lowestTTL = header.Ttl
@ -49,12 +71,12 @@ func (m *RRCache) Clean(minExpires uint32) {
} }
// log.Tracef("lowest TTL is %d", lowestTTL) // log.Tracef("lowest TTL is %d", lowestTTL)
m.TTL = time.Now().Unix() + int64(lowestTTL) rrCache.TTL = time.Now().Unix() + int64(lowestTTL)
} }
// ExportAllARecords return of a list of all A and AAAA IP addresses. // ExportAllARecords return of a list of all A and AAAA IP addresses.
func (m *RRCache) ExportAllARecords() (ips []net.IP) { func (rrCache *RRCache) ExportAllARecords() (ips []net.IP) {
for _, rr := range m.Answer { for _, rr := range rrCache.Answer {
if rr.Header().Class != dns.ClassINET { if rr.Header().Class != dns.ClassINET {
continue continue
} }
@ -76,23 +98,23 @@ func (m *RRCache) ExportAllARecords() (ips []net.IP) {
} }
// ToNameRecord converts the RRCache to a NameRecord for cleaner persistence. // ToNameRecord converts the RRCache to a NameRecord for cleaner persistence.
func (m *RRCache) ToNameRecord() *NameRecord { func (rrCache *RRCache) ToNameRecord() *NameRecord {
new := &NameRecord{ new := &NameRecord{
Domain: m.Domain, Domain: rrCache.Domain,
Question: m.Question.String(), Question: rrCache.Question.String(),
TTL: m.TTL, TTL: rrCache.TTL,
Server: m.Server, Server: rrCache.Server,
ServerScope: m.ServerScope, ServerScope: rrCache.ServerScope,
} }
// stringify RR entries // stringify RR entries
for _, entry := range m.Answer { for _, entry := range rrCache.Answer {
new.Answer = append(new.Answer, entry.String()) new.Answer = append(new.Answer, entry.String())
} }
for _, entry := range m.Ns { for _, entry := range rrCache.Ns {
new.Ns = append(new.Ns, entry.String()) new.Ns = append(new.Ns, entry.String())
} }
for _, entry := range m.Extra { for _, entry := range rrCache.Extra {
new.Extra = append(new.Extra, entry.String()) new.Extra = append(new.Extra, entry.String())
} }
@ -100,8 +122,8 @@ func (m *RRCache) ToNameRecord() *NameRecord {
} }
// Save saves the RRCache to the database as a NameRecord. // Save saves the RRCache to the database as a NameRecord.
func (m *RRCache) Save() error { func (rrCache *RRCache) Save() error {
return m.ToNameRecord().Save() return rrCache.ToNameRecord().Save()
} }
// GetRRCache tries to load the corresponding NameRecord from the database and convert it. // GetRRCache tries to load the corresponding NameRecord from the database and convert it.
@ -143,25 +165,25 @@ func GetRRCache(domain string, question dns.Type) (*RRCache, error) {
} }
// ServedFromCache marks the RRCache as served from cache. // ServedFromCache marks the RRCache as served from cache.
func (m *RRCache) ServedFromCache() bool { func (rrCache *RRCache) ServedFromCache() bool {
return m.servedFromCache return rrCache.servedFromCache
} }
// RequestingNew informs that it has expired and new RRs are being fetched. // RequestingNew informs that it has expired and new RRs are being fetched.
func (m *RRCache) RequestingNew() bool { func (rrCache *RRCache) RequestingNew() bool {
return m.requestingNew return rrCache.requestingNew
} }
// Flags formats ServedFromCache and RequestingNew to a condensed, flag-like format. // Flags formats ServedFromCache and RequestingNew to a condensed, flag-like format.
func (m *RRCache) Flags() string { func (rrCache *RRCache) Flags() string {
var s string var s string
if m.servedFromCache { if rrCache.servedFromCache {
s += "C" s += "C"
} }
if m.requestingNew { if rrCache.requestingNew {
s += "R" s += "R"
} }
if m.Filtered { if rrCache.Filtered {
s += "F" s += "F"
} }
@ -172,27 +194,27 @@ func (m *RRCache) Flags() string {
} }
// IsNXDomain returnes whether the result is nxdomain. // IsNXDomain returnes whether the result is nxdomain.
func (m *RRCache) IsNXDomain() bool { func (rrCache *RRCache) IsNXDomain() bool {
return len(m.Answer) == 0 return len(rrCache.Answer) == 0
} }
// ShallowCopy returns a shallow copy of the cache. slices are not copied, but referenced. // ShallowCopy returns a shallow copy of the cache. slices are not copied, but referenced.
func (m *RRCache) ShallowCopy() *RRCache { func (rrCache *RRCache) ShallowCopy() *RRCache {
return &RRCache{ return &RRCache{
Domain: m.Domain, Domain: rrCache.Domain,
Question: m.Question, Question: rrCache.Question,
Answer: m.Answer, Answer: rrCache.Answer,
Ns: m.Ns, Ns: rrCache.Ns,
Extra: m.Extra, Extra: rrCache.Extra,
TTL: m.TTL, TTL: rrCache.TTL,
Server: m.Server, Server: rrCache.Server,
ServerScope: m.ServerScope, ServerScope: rrCache.ServerScope,
updated: m.updated, updated: rrCache.updated,
servedFromCache: m.servedFromCache, servedFromCache: rrCache.servedFromCache,
requestingNew: m.requestingNew, requestingNew: rrCache.requestingNew,
Filtered: m.Filtered, Filtered: rrCache.Filtered,
FilteredEntries: m.FilteredEntries, FilteredEntries: rrCache.FilteredEntries,
} }
} }

View file

@ -1,51 +0,0 @@
package intel
import "strings"
var (
localReverseScopes = []string{
".10.in-addr.arpa.",
".16.172.in-addr.arpa.",
".17.172.in-addr.arpa.",
".18.172.in-addr.arpa.",
".19.172.in-addr.arpa.",
".20.172.in-addr.arpa.",
".21.172.in-addr.arpa.",
".22.172.in-addr.arpa.",
".23.172.in-addr.arpa.",
".24.172.in-addr.arpa.",
".25.172.in-addr.arpa.",
".26.172.in-addr.arpa.",
".27.172.in-addr.arpa.",
".28.172.in-addr.arpa.",
".29.172.in-addr.arpa.",
".30.172.in-addr.arpa.",
".31.172.in-addr.arpa.",
".168.192.in-addr.arpa.",
".254.169.in-addr.arpa.",
".8.e.f.ip6.arpa.",
".9.e.f.ip6.arpa.",
".a.e.f.ip6.arpa.",
".b.e.f.ip6.arpa.",
}
// RFC6761, RFC7686
specialScopes = []string{
".example.",
".example.com.",
".example.net.",
".example.org.",
".invalid.",
".test.",
".onion.",
}
)
func domainInScopes(fqdn string, list []string) bool {
for _, scope := range list {
if strings.HasSuffix(fqdn, scope) {
return true
}
}
return false
}

View file

@ -3,7 +3,9 @@ package nameserver
import ( import (
"context" "context"
"net" "net"
"runtime" "strings"
"github.com/safing/portmaster/network/environment"
"github.com/miekg/dns" "github.com/miekg/dns"
@ -18,23 +20,20 @@ import (
) )
var ( var (
localhostIPs []dns.RR module *modules.Module
) dnsServer *dns.Server
mtDNSRequest = "dns request"
var ( listenAddress = "0.0.0.0:53"
listenAddress = "127.0.0.1:53" IPv4Localhost = net.IPv4(127, 0, 0, 1)
localhostIP = net.IPv4(127, 0, 0, 1) localhostRRs []dns.RR
) )
func init() { func init() {
modules.Register("nameserver", prep, start, nil, "core", "intel") module = modules.Register("nameserver", initLocalhostRRs, start, stop, "core", "intel", "network")
if runtime.GOOS == "windows" {
listenAddress = "0.0.0.0:53"
}
} }
func prep() error { func initLocalhostRRs() error {
localhostIPv4, err := dns.NewRR("localhost. 17 IN A 127.0.0.1") localhostIPv4, err := dns.NewRR("localhost. 17 IN A 127.0.0.1")
if err != nil { if err != nil {
return err return err
@ -45,153 +44,202 @@ func prep() error {
return err return err
} }
localhostIPs = []dns.RR{localhostIPv4, localhostIPv6} localhostRRs = []dns.RR{localhostIPv4, localhostIPv6}
return nil return nil
} }
func start() error { func start() error {
server := &dns.Server{Addr: listenAddress, Net: "udp"} dnsServer = &dns.Server{Addr: listenAddress, Net: "udp"}
dns.HandleFunc(".", handleRequest) dns.HandleFunc(".", handleRequestAsMicroTask)
go run(server)
module.StartServiceWorker("dns resolver", 0, func(ctx context.Context) error {
err := dnsServer.ListenAndServe()
if err != nil {
// check if we are shutting down
if module.ShutdownInProgress() {
return nil
}
// is something blocking our port?
checkErr := checkForConflictingService()
if checkErr != nil {
return checkErr
}
}
return err
})
return nil return nil
} }
func run(server *dns.Server) { func stop() error {
for { if dnsServer != nil {
err := server.ListenAndServe() return dnsServer.Shutdown()
if err != nil {
log.Errorf("nameserver: server failed: %s", err)
checkForConflictingService(err)
}
} }
return nil
} }
func nxDomain(w dns.ResponseWriter, query *dns.Msg) { func returnNXDomain(w dns.ResponseWriter, query *dns.Msg) {
m := new(dns.Msg) m := new(dns.Msg)
m.SetRcode(query, dns.RcodeNameError) m.SetRcode(query, dns.RcodeNameError)
w.WriteMsg(m) _ = w.WriteMsg(m)
} }
func handleRequest(w dns.ResponseWriter, query *dns.Msg) { func returnServerFailure(w dns.ResponseWriter, query *dns.Msg) {
m := new(dns.Msg)
m.SetRcode(query, dns.RcodeServerFailure)
_ = w.WriteMsg(m)
}
func handleRequestAsMicroTask(w dns.ResponseWriter, query *dns.Msg) {
err := module.RunMicroTask(&mtDNSRequest, func(ctx context.Context) error {
return handleRequest(ctx, w, query)
})
if err != nil {
log.Warningf("intel: failed to handle dns request: %s", err)
}
}
func handleRequest(ctx context.Context, w dns.ResponseWriter, query *dns.Msg) error {
// return with server failure if offline
if environment.GetOnlineStatus() == environment.StatusOffline {
returnServerFailure(w, query)
return nil
}
// only process first question, that's how everyone does it. // only process first question, that's how everyone does it.
question := query.Question[0] question := query.Question[0]
fqdn := dns.Fqdn(question.Name) q := &intel.Query{
qtype := dns.Type(question.Qtype) FQDN: question.Name,
QType: dns.Type(question.Qtype),
}
// check class // check class
if question.Qclass != dns.ClassINET { if question.Qclass != dns.ClassINET {
// we only serve IN records, return nxdomain // we only serve IN records, return nxdomain
nxDomain(w, query) returnNXDomain(w, query)
return return nil
} }
// handle request for localhost // handle request for localhost
if fqdn == "localhost." { if strings.HasSuffix(q.FQDN, "localhost.") {
m := new(dns.Msg) m := new(dns.Msg)
m.SetReply(query) m.SetReply(query)
m.Answer = localhostIPs m.Answer = localhostRRs
w.WriteMsg(m) _ = w.WriteMsg(m)
return nil
} }
// get addresses // get addresses
remoteAddr, ok := w.RemoteAddr().(*net.UDPAddr) remoteAddr, ok := w.RemoteAddr().(*net.UDPAddr)
if !ok { if !ok {
log.Warningf("nameserver: could not get remote address of request for %s%s, ignoring", fqdn, qtype) log.Warningf("nameserver: could not get remote address of request for %s%s, ignoring", q.FQDN, q.QType)
return return nil
} }
if !remoteAddr.IP.Equal(localhostIP) { if !remoteAddr.IP.Equal(IPv4Localhost) {
// if request is not coming from 127.0.0.1, check if it's really local // if request is not coming from 127.0.0.1, check if it's really local
localAddr, ok := w.RemoteAddr().(*net.UDPAddr) localAddr, ok := w.RemoteAddr().(*net.UDPAddr)
if !ok { if !ok {
log.Warningf("nameserver: could not get local address of request for %s%s, ignoring", fqdn, qtype) log.Warningf("nameserver: could not get local address of request for %s%s, ignoring", q.FQDN, q.QType)
return return nil
} }
// ignore external request // ignore external request
if !remoteAddr.IP.Equal(localAddr.IP) { if !remoteAddr.IP.Equal(localAddr.IP) {
log.Warningf("nameserver: external request for %s%s, ignoring", fqdn, qtype) log.Warningf("nameserver: external request for %s%s, ignoring", q.FQDN, q.QType)
return return nil
} }
} }
// check if valid domain name // check if valid domain name
if !netutils.IsValidFqdn(fqdn) { if !netutils.IsValidFqdn(q.FQDN) {
log.Debugf("nameserver: domain name %s is invalid, returning nxdomain", fqdn) log.Debugf("nameserver: domain name %s is invalid, returning nxdomain", q.FQDN)
nxDomain(w, query) returnNXDomain(w, query)
return return nil
} }
// start tracer // start tracer
ctx := log.AddTracer(context.Background()) ctx, tracer := log.AddTracer(ctx)
log.Tracer(ctx).Tracef("nameserver: handling new request for %s%s from %s:%d", fqdn, qtype, remoteAddr.IP, remoteAddr.Port) tracer.Tracef("nameserver: handling new request for %s%s from %s:%d", q.FQDN, q.QType, remoteAddr.IP, remoteAddr.Port)
// TODO: if there are 3 request for the same domain/type in a row, delete all caches of that domain // TODO: if there are 3 request for the same domain/type in a row, delete all caches of that domain
// get connection // get connection
comm, err := network.GetCommunicationByDNSRequest(ctx, remoteAddr.IP, uint16(remoteAddr.Port), fqdn) comm, err := network.GetCommunicationByDNSRequest(ctx, remoteAddr.IP, uint16(remoteAddr.Port), q.FQDN)
if err != nil { if err != nil {
log.ErrorTracef(ctx, "nameserver: could not identify process of %s:%d, returning nxdomain: %s", remoteAddr.IP, remoteAddr.Port, err) tracer.Errorf("nameserver: could not identify process of %s:%d, returning nxdomain: %s", remoteAddr.IP, remoteAddr.Port, err)
nxDomain(w, query) returnNXDomain(w, query)
return return nil
} }
defer func() { defer func() {
go comm.SaveIfNeeded() go comm.SaveIfNeeded()
}() }()
// save security level to query
q.SecurityLevel = comm.Process().ProfileSet().SecurityLevel()
// check for possible DNS tunneling / data transmission // check for possible DNS tunneling / data transmission
// TODO: improve this // TODO: improve this
lms := algs.LmsScoreOfDomain(fqdn) lms := algs.LmsScoreOfDomain(q.FQDN)
// log.Tracef("nameserver: domain %s has lms score of %f", fqdn, lms) // log.Tracef("nameserver: domain %s has lms score of %f", fqdn, lms)
if lms < 10 { if lms < 10 {
log.WarningTracef(ctx, "nameserver: possible data tunnel by %s: %s has lms score of %f, returning nxdomain", comm.Process(), fqdn, lms) tracer.Warningf("nameserver: possible data tunnel by %s: %s has lms score of %f, returning nxdomain", comm.Process(), q.FQDN, lms)
nxDomain(w, query) returnNXDomain(w, query)
return return nil
} }
// check profile before we even get intel and rr // check profile before we even get intel and rr
firewall.DecideOnCommunicationBeforeIntel(comm, fqdn) firewall.DecideOnCommunicationBeforeIntel(comm, q.FQDN)
comm.Lock() comm.Lock()
comm.SaveWhenFinished() comm.SaveWhenFinished()
comm.Unlock() comm.Unlock()
if comm.GetVerdict() == network.VerdictBlock || comm.GetVerdict() == network.VerdictDrop { if comm.GetVerdict() == network.VerdictBlock || comm.GetVerdict() == network.VerdictDrop {
log.InfoTracef(ctx, "nameserver: %s denied before intel, returning nxdomain", comm) tracer.Infof("nameserver: %s denied before intel, returning nxdomain", comm)
nxDomain(w, query) returnNXDomain(w, query)
return return nil
} }
// get intel and RRs // get intel and RRs
domainIntel, rrCache := intel.GetIntelAndRRs(ctx, fqdn, qtype, comm.Process().ProfileSet().SecurityLevel()) rrCache, err := intel.Resolve(ctx, q)
if rrCache == nil { if err != nil {
// TODO: analyze nxdomain requests, malware could be trying DGA-domains // TODO: analyze nxdomain requests, malware could be trying DGA-domains
log.WarningTracef(ctx, "nameserver: %s requested %s%s, is nxdomain", comm.Process(), fqdn, qtype) tracer.Warningf("nameserver: %s requested %s%s: %s", comm.Process(), q.FQDN, q.QType, err)
nxDomain(w, query) returnNXDomain(w, query)
return return nil
} }
// set intel // get current intel
comm.Lock()
domainIntel := comm.Intel
comm.Unlock()
if domainIntel == nil {
// fetch intel
domainIntel, err = intel.GetIntel(ctx, q)
if err != nil {
tracer.Warningf("nameserver: failed to get intel for %s%s: %s", q.FQDN, q.QType, err)
returnNXDomain(w, query)
}
comm.Lock() comm.Lock()
comm.Intel = domainIntel comm.Intel = domainIntel
comm.Unlock() comm.Unlock()
}
// check with intel // check with intel
firewall.DecideOnCommunicationAfterIntel(comm, fqdn, rrCache) firewall.DecideOnCommunicationAfterIntel(comm, q.FQDN, rrCache)
switch comm.GetVerdict() { switch comm.GetVerdict() {
case network.VerdictUndecided, network.VerdictBlock, network.VerdictDrop: case network.VerdictUndecided, network.VerdictBlock, network.VerdictDrop:
log.InfoTracef(ctx, "nameserver: %s denied after intel, returning nxdomain", comm) tracer.Infof("nameserver: %s denied after intel, returning nxdomain", comm)
nxDomain(w, query) returnNXDomain(w, query)
return return nil
} }
// filter DNS response // filter DNS response
rrCache = firewall.FilterDNSResponse(comm, fqdn, rrCache) rrCache = firewall.FilterDNSResponse(comm, q, rrCache)
if rrCache == nil { if rrCache == nil {
log.InfoTracef(ctx, "nameserver: %s implicitly denied by filtering the dns response, returning nxdomain", comm) tracer.Infof("nameserver: %s implicitly denied by filtering the dns response, returning nxdomain", comm)
nxDomain(w, query) returnNXDomain(w, query)
return return nil
} }
// save IP addresses to IPInfo // save IP addresses to IPInfo
@ -202,12 +250,13 @@ func handleRequest(w dns.ResponseWriter, query *dns.Msg) {
if err != nil { if err != nil {
ipInfo = &intel.IPInfo{ ipInfo = &intel.IPInfo{
IP: v.A.String(), IP: v.A.String(),
Domains: []string{fqdn}, Domains: []string{q.FQDN},
} }
ipInfo.Save() _ = ipInfo.Save()
} else { } else {
if ipInfo.AddDomain(fqdn) { added := ipInfo.AddDomain(q.FQDN)
ipInfo.Save() if added {
_ = ipInfo.Save()
} }
} }
case *dns.AAAA: case *dns.AAAA:
@ -215,12 +264,13 @@ func handleRequest(w dns.ResponseWriter, query *dns.Msg) {
if err != nil { if err != nil {
ipInfo = &intel.IPInfo{ ipInfo = &intel.IPInfo{
IP: v.AAAA.String(), IP: v.AAAA.String(),
Domains: []string{fqdn}, Domains: []string{q.FQDN},
} }
ipInfo.Save() _ = ipInfo.Save()
} else { } else {
if ipInfo.AddDomain(fqdn) { added := ipInfo.AddDomain(q.FQDN)
ipInfo.Save() if added {
_ = ipInfo.Save()
} }
} }
} }
@ -232,6 +282,8 @@ func handleRequest(w dns.ResponseWriter, query *dns.Msg) {
m.Answer = rrCache.Answer m.Answer = rrCache.Answer
m.Ns = rrCache.Ns m.Ns = rrCache.Ns
m.Extra = rrCache.Extra m.Extra = rrCache.Extra
w.WriteMsg(m) _ = w.WriteMsg(m)
log.DebugTracef(ctx, "nameserver: returning response %s%s to %s", fqdn, qtype, comm.Process()) tracer.Debugf("nameserver: returning response %s%s to %s", q.FQDN, q.QType, comm.Process())
return nil
} }

View file

@ -3,30 +3,34 @@ package only
import ( import (
"context" "context"
"net" "net"
"time" "strings"
"github.com/safing/portmaster/network/environment"
"github.com/miekg/dns" "github.com/miekg/dns"
"github.com/safing/portbase/log" "github.com/safing/portbase/log"
"github.com/safing/portbase/modules" "github.com/safing/portbase/modules"
"github.com/safing/portmaster/analytics/algs"
"github.com/safing/portmaster/intel" "github.com/safing/portmaster/intel"
"github.com/safing/portmaster/network/netutils" "github.com/safing/portmaster/network/netutils"
"github.com/safing/portmaster/status"
) )
var ( var (
localhostIPs []dns.RR module *modules.Module
dnsServer *dns.Server
mtDNSRequest = "dns request"
listenAddress = "127.0.0.1:53"
IPv4Localhost = net.IPv4(127, 0, 0, 1)
localhostRRs []dns.RR
) )
func init() { func init() {
modules.Register("nameserver", prep, start, nil, "core", "intel") module = modules.Register("nameserver", initLocalhostRRs, start, stop, "core", "intel", "network")
} }
func prep() error { func initLocalhostRRs() error {
intel.SetLocalAddrFactory(func(network string) net.Addr { return nil })
localhostIPv4, err := dns.NewRR("localhost. 17 IN A 127.0.0.1") localhostIPv4, err := dns.NewRR("localhost. 17 IN A 127.0.0.1")
if err != nil { if err != nil {
return err return err
@ -37,107 +41,128 @@ func prep() error {
return err return err
} }
localhostIPs = []dns.RR{localhostIPv4, localhostIPv6} localhostRRs = []dns.RR{localhostIPv4, localhostIPv6}
return nil return nil
} }
func start() error { func start() error {
server := &dns.Server{Addr: "0.0.0.0:53", Net: "udp"} dnsServer = &dns.Server{Addr: listenAddress, Net: "udp"}
dns.HandleFunc(".", handleRequest) dns.HandleFunc(".", handleRequestAsMicroTask)
go run(server)
module.StartServiceWorker("dns resolver", 0, func(ctx context.Context) error {
err := dnsServer.ListenAndServe()
if err != nil {
// check if we are shutting down
if module.ShutdownInProgress() {
return nil
}
}
return err
})
return nil return nil
} }
func run(server *dns.Server) { func stop() error {
for { if dnsServer != nil {
err := server.ListenAndServe() return dnsServer.Shutdown()
if err != nil {
log.Errorf("nameserver: server failed: %s", err)
log.Info("nameserver: restarting server in 10 seconds")
time.Sleep(10 * time.Second)
}
} }
return nil
} }
func nxDomain(w dns.ResponseWriter, query *dns.Msg) { func returnNXDomain(w dns.ResponseWriter, query *dns.Msg) {
m := new(dns.Msg) m := new(dns.Msg)
m.SetRcode(query, dns.RcodeNameError) m.SetRcode(query, dns.RcodeNameError)
w.WriteMsg(m) _ = w.WriteMsg(m)
} }
func handleRequest(w dns.ResponseWriter, query *dns.Msg) { func returnServerFailure(w dns.ResponseWriter, query *dns.Msg) {
m := new(dns.Msg)
m.SetRcode(query, dns.RcodeServerFailure)
_ = w.WriteMsg(m)
}
func handleRequestAsMicroTask(w dns.ResponseWriter, query *dns.Msg) {
err := module.RunMicroTask(&mtDNSRequest, func(ctx context.Context) error {
return handleRequest(ctx, w, query)
})
if err != nil {
log.Warningf("intel: failed to handle dns request: %s", err)
}
}
func handleRequest(ctx context.Context, w dns.ResponseWriter, query *dns.Msg) error {
// return with server failure if offline
if environment.GetOnlineStatus() == environment.StatusOffline {
returnServerFailure(w, query)
return nil
}
// only process first question, that's how everyone does it. // only process first question, that's how everyone does it.
question := query.Question[0] question := query.Question[0]
fqdn := dns.Fqdn(question.Name) q := &intel.Query{
qtype := dns.Type(question.Qtype) FQDN: question.Name,
QType: dns.Type(question.Qtype),
}
// check class // check class
if question.Qclass != dns.ClassINET { if question.Qclass != dns.ClassINET {
// we only serve IN records, return nxdomain // we only serve IN records, return nxdomain
nxDomain(w, query) returnNXDomain(w, query)
return return nil
} }
// handle request for localhost // handle request for localhost
if fqdn == "localhost." { if strings.HasSuffix(q.FQDN, "localhost.") {
m := new(dns.Msg) m := new(dns.Msg)
m.SetReply(query) m.SetReply(query)
m.Answer = localhostIPs m.Answer = localhostRRs
w.WriteMsg(m) _ = w.WriteMsg(m)
return nil
} }
// get addresses // get addresses
remoteAddr, ok := w.RemoteAddr().(*net.UDPAddr) remoteAddr, ok := w.RemoteAddr().(*net.UDPAddr)
if !ok { if !ok {
log.Warningf("nameserver: could not get remote address of request for %s%s, ignoring", fqdn, qtype) log.Warningf("nameserver: could not get remote address of request for %s%s, ignoring", q.FQDN, q.QType)
return return nil
} }
if !remoteAddr.IP.Equal(IPv4Localhost) {
// if request is not coming from 127.0.0.1, check if it's really local
localAddr, ok := w.RemoteAddr().(*net.UDPAddr) localAddr, ok := w.RemoteAddr().(*net.UDPAddr)
if !ok { if !ok {
log.Warningf("nameserver: could not get local address of request for %s%s, ignoring", fqdn, qtype) log.Warningf("nameserver: could not get local address of request for %s%s, ignoring", q.FQDN, q.QType)
return return nil
} }
// ignore external request // ignore external request
if !remoteAddr.IP.Equal(localAddr.IP) { if !remoteAddr.IP.Equal(localAddr.IP) {
log.Warningf("nameserver: external request for %s%s, ignoring", fqdn, qtype) log.Warningf("nameserver: external request for %s%s, ignoring", q.FQDN, q.QType)
return return nil
}
} }
// check if valid domain name // check if valid domain name
if !netutils.IsValidFqdn(fqdn) { if !netutils.IsValidFqdn(q.FQDN) {
log.Debugf("nameserver: domain name %s is invalid, returning nxdomain", fqdn) log.Debugf("nameserver: domain name %s is invalid, returning nxdomain", q.FQDN)
nxDomain(w, query) returnNXDomain(w, query)
return return nil
} }
// start tracer // start tracer
ctx := log.AddTracer(context.Background()) ctx, tracer := log.AddTracer(ctx)
log.Tracer(ctx).Tracef("nameserver: handling new request for %s%s from %s:%d", fqdn, qtype, remoteAddr.IP, remoteAddr.Port) tracer.Tracef("nameserver: handling new request for %s%s from %s:%d", q.FQDN, q.QType, remoteAddr.IP, remoteAddr.Port)
// TODO: if there are 3 request for the same domain/type in a row, delete all caches of that domain // TODO: if there are 3 request for the same domain/type in a row, delete all caches of that domain
// check for possible DNS tunneling / data transmission
// TODO: improve this
lms := algs.LmsScoreOfDomain(fqdn)
// log.Tracef("nameserver: domain %s has lms score of %f", fqdn, lms)
if lms < 10 {
log.WarningTracef(ctx, "nameserver: possible data tunnel by %s:%d: %s has lms score of %f, returning nxdomain", remoteAddr.IP, remoteAddr.Port, fqdn, lms)
nxDomain(w, query)
return
}
// get intel and RRs // get intel and RRs
// start = time.Now() rrCache, err := intel.Resolve(ctx, q)
_, rrCache := intel.GetIntelAndRRs(ctx, fqdn, qtype, status.SecurityLevelDynamic) if err != nil {
// log.Tracef("nameserver: took %s to get intel and RRs", time.Since(start))
if rrCache == nil {
// TODO: analyze nxdomain requests, malware could be trying DGA-domains // TODO: analyze nxdomain requests, malware could be trying DGA-domains
log.WarningTracef(ctx, "nameserver: %s:%d requested %s%s, is nxdomain", remoteAddr.IP, remoteAddr.Port, fqdn, qtype) tracer.Warningf("nameserver: request for %s%s: %s", q.FQDN, q.QType, err)
nxDomain(w, query) returnNXDomain(w, query)
return return nil
} }
// save IP addresses to IPInfo // save IP addresses to IPInfo
@ -148,12 +173,13 @@ func handleRequest(w dns.ResponseWriter, query *dns.Msg) {
if err != nil { if err != nil {
ipInfo = &intel.IPInfo{ ipInfo = &intel.IPInfo{
IP: v.A.String(), IP: v.A.String(),
Domains: []string{fqdn}, Domains: []string{q.FQDN},
} }
ipInfo.Save() _ = ipInfo.Save()
} else { } else {
if ipInfo.AddDomain(fqdn) { added := ipInfo.AddDomain(q.FQDN)
ipInfo.Save() if added {
_ = ipInfo.Save()
} }
} }
case *dns.AAAA: case *dns.AAAA:
@ -161,12 +187,13 @@ func handleRequest(w dns.ResponseWriter, query *dns.Msg) {
if err != nil { if err != nil {
ipInfo = &intel.IPInfo{ ipInfo = &intel.IPInfo{
IP: v.AAAA.String(), IP: v.AAAA.String(),
Domains: []string{fqdn}, Domains: []string{q.FQDN},
} }
ipInfo.Save() _ = ipInfo.Save()
} else { } else {
if ipInfo.AddDomain(fqdn) { added := ipInfo.AddDomain(q.FQDN)
ipInfo.Save() if added {
_ = ipInfo.Save()
} }
} }
} }
@ -178,6 +205,8 @@ func handleRequest(w dns.ResponseWriter, query *dns.Msg) {
m.Answer = rrCache.Answer m.Answer = rrCache.Answer
m.Ns = rrCache.Ns m.Ns = rrCache.Ns
m.Extra = rrCache.Extra m.Extra = rrCache.Extra
w.WriteMsg(m) _ = w.WriteMsg(m)
log.DebugTracef(ctx, "nameserver: returning response %s%s to %s:%d", fqdn, qtype, remoteAddr.IP, remoteAddr.Port) tracer.Debugf("nameserver: returning response %s%s", q.FQDN, q.QType)
return nil
} }

View file

@ -7,20 +7,45 @@ import (
"time" "time"
"github.com/safing/portbase/log" "github.com/safing/portbase/log"
"github.com/safing/portbase/modules"
"github.com/safing/portbase/notifications" "github.com/safing/portbase/notifications"
"github.com/safing/portmaster/network/packet" "github.com/safing/portmaster/network/packet"
"github.com/safing/portmaster/process" "github.com/safing/portmaster/process"
) )
func checkForConflictingService(err error) { var (
pid, err := takeover() otherResolverIPs = []net.IP{
if err != nil || pid == 0 { net.IPv4(127, 0, 0, 1), // default
log.Info("nameserver: restarting server in 10 seconds") net.IPv4(127, 0, 0, 53), // some resolvers on Linux
time.Sleep(10 * time.Second) }
return )
func checkForConflictingService() error {
var pid int
var err error
// check multiple IPs for other resolvers
for _, resolverIP := range otherResolverIPs {
pid, err = takeover(resolverIP)
if err == nil && pid != 0 {
break
}
}
// handle returns
if err != nil {
log.Infof("nameserver: could not stop conflicting service: %s", err)
// leave original service-worker error intact
return nil
}
if pid == 0 {
// no conflicting service identified
return nil
} }
log.Infof("nameserver: stopped conflicting name service with pid %d", pid) // we killed something!
// wait for a short duration for the other service to shut down
time.Sleep(10 * time.Millisecond)
// notify user // notify user
(&notifications.Notification{ (&notifications.Notification{
@ -28,15 +53,14 @@ func checkForConflictingService(err error) {
Message: fmt.Sprintf("Portmaster stopped a conflicting name service (pid %d) to gain required system integration.", pid), Message: fmt.Sprintf("Portmaster stopped a conflicting name service (pid %d) to gain required system integration.", pid),
}).Save() }).Save()
// wait for a short duration for the other service to shut down // restart via service-worker logic
time.Sleep(100 * time.Millisecond) return fmt.Errorf("%w: stopped conflicting name service with pid %d", modules.ErrRestartNow, pid)
} }
func takeover() (int, error) { func takeover(resolverIP net.IP) (int, error) {
pid, _, err := process.GetPidByEndpoints(net.IPv4(127, 0, 0, 1), 53, net.IPv4(127, 0, 0, 1), 65535, packet.UDP) pid, _, err := process.GetPidByEndpoints(resolverIP, 53, resolverIP, 65535, packet.UDP)
if err != nil { if err != nil {
// there may be nothing listening on :53 // there may be nothing listening on :53
log.Tracef("nameserver: expected conflicting name service, but could not find anything listenting on :53")
return 0, nil return 0, nil
} }