mirror of
https://github.com/safing/portmaster
synced 2025-09-02 18:49:14 +00:00
Revamp intel and nameserver packages
This commit is contained in:
parent
5799d2559b
commit
25b1d59663
18 changed files with 1675 additions and 1060 deletions
|
@ -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 != "" {
|
||||||
|
|
|
@ -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, ", ")
|
||||||
|
}
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
|
||||||
}
|
|
||||||
|
|
|
@ -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)
|
||||||
|
|
232
intel/mdns.go
232
intel/mdns.go
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
517
intel/resolve.go
517
intel/resolve.go
|
@ -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
276
intel/resolver-scopes.go
Normal 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
|
||||||
|
}
|
|
@ -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
357
intel/resolvers.go
Normal 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
36
intel/resolvers_test.go
Normal 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)
|
||||||
|
}
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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")
|
||||||
}
|
}
|
||||||
|
|
128
intel/rrcache.go
128
intel/rrcache.go
|
@ -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,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
|
||||||
}
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
(¬ifications.Notification{
|
(¬ifications.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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Add table
Reference in a new issue