Rename intel to resolver

This commit is contained in:
Daniel
2020-03-20 23:01:15 +01:00
parent f270ccc21f
commit 55033404d4
18 changed files with 181 additions and 19 deletions

124
resolver/clients.go Normal file
View File

@@ -0,0 +1,124 @@
package resolver
import (
"crypto/tls"
"net"
"sync"
"time"
"github.com/miekg/dns"
)
var (
localAddrFactory func(network string) net.Addr
)
// SetLocalAddrFactory supplies the intel package with a function to get permitted local addresses for connections.
func SetLocalAddrFactory(laf func(network string) net.Addr) {
if localAddrFactory == nil {
localAddrFactory = laf
}
}
func getLocalAddr(network string) net.Addr {
if localAddrFactory != nil {
return localAddrFactory(network)
}
return nil
}
type clientManager struct {
dnsClient *dns.Client
factory func() *dns.Client
lock sync.Mutex
refreshAfter time.Time
ttl time.Duration // force refresh of connection to reduce traceability
}
func newDNSClientManager(_ *Resolver) *clientManager {
return &clientManager{
ttl: 0, // new client for every request, as we need to randomize the port
factory: func() *dns.Client {
return &dns.Client{
Timeout: 5 * time.Second,
Dialer: &net.Dialer{
LocalAddr: getLocalAddr("udp"),
},
}
},
}
}
func newTCPClientManager(_ *Resolver) *clientManager {
return &clientManager{
ttl: 0, // TODO: build a custom client that can reuse connections to some degree (performance / privacy tradeoff)
factory: func() *dns.Client {
return &dns.Client{
Net: "tcp",
Timeout: 5 * time.Second,
Dialer: &net.Dialer{
LocalAddr: getLocalAddr("tcp"),
KeepAlive: 15 * time.Second,
},
}
},
}
}
func newTLSClientManager(resolver *Resolver) *clientManager {
return &clientManager{
ttl: 0, // TODO: build a custom client that can reuse connections to some degree (performance / privacy tradeoff)
factory: func() *dns.Client {
return &dns.Client{
Net: "tcp-tls",
TLSConfig: &tls.Config{
MinVersion: tls.VersionTLS12,
ServerName: resolver.VerifyDomain,
// TODO: use portbase rng
},
Timeout: 5 * time.Second,
Dialer: &net.Dialer{
LocalAddr: getLocalAddr("tcp"),
KeepAlive: 15 * time.Second,
},
}
},
}
}
func newHTTPSClientManager(resolver *Resolver) *clientManager {
return &clientManager{
ttl: 0, // TODO: build a custom client that can reuse connections to some degree (performance / privacy tradeoff)
factory: func() *dns.Client {
new := &dns.Client{
Net: "https",
TLSConfig: &tls.Config{
MinVersion: tls.VersionTLS12,
// TODO: use portbase rng
},
Timeout: 5 * time.Second,
Dialer: &net.Dialer{
LocalAddr: getLocalAddr("tcp"),
KeepAlive: 15 * time.Second,
},
}
if resolver.VerifyDomain != "" {
new.TLSConfig.ServerName = resolver.VerifyDomain
}
return new
},
}
}
func (cm *clientManager) getDNSClient() *dns.Client {
cm.lock.Lock()
defer cm.lock.Unlock()
if cm.dnsClient == nil || cm.ttl == 0 || time.Now().After(cm.refreshAfter) {
cm.dnsClient = cm.factory()
cm.refreshAfter = time.Now().Add(cm.ttl)
}
return cm.dnsClient
}

155
resolver/config.go Normal file
View File

@@ -0,0 +1,155 @@
package resolver
import (
"fmt"
"strings"
"github.com/safing/portbase/config"
"github.com/safing/portmaster/status"
)
var (
configuredNameServers config.StringArrayOption
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.0.0.1:53", // Cloudflare
// "doh|cloudflare-dns.com/dns-query", // DoH still experimental
}
nameserverRetryRate config.IntOption
doNotUseMulticastDNS status.SecurityLevelOption
doNotUseAssignedNameservers status.SecurityLevelOption
doNotUseInsecureProtocols status.SecurityLevelOption
doNotResolveSpecialDomains status.SecurityLevelOption
doNotResolveTestDomains status.SecurityLevelOption
)
func prepConfig() error {
err := config.Register(&config.Option{
Name: "Nameservers (DNS)",
Key: "intel/nameservers",
Description: "Nameserver to use for resolving DNS requests.",
OptType: config.OptTypeStringArray,
ExpertiseLevel: config.ExpertiseLevelExpert,
ReleaseLevel: config.ReleaseLevelStable,
DefaultValue: defaultNameServers,
ValidationRegex: "^(dns|tcp|tls|https)|[a-z0-9\\.|-]+$",
})
if err != nil {
return err
}
configuredNameServers = config.Concurrent.GetAsStringArray("intel/nameservers", defaultNameServers)
err = config.Register(&config.Option{
Name: "Nameserver Retry Rate",
Key: "intel/nameserverRetryRate",
Description: "Rate at which to retry failed nameservers, in seconds.",
OptType: config.OptTypeInt,
ExpertiseLevel: config.ExpertiseLevelExpert,
ReleaseLevel: config.ReleaseLevelStable,
DefaultValue: 600,
})
if err != nil {
return err
}
nameserverRetryRate = config.Concurrent.GetAsInt("intel/nameserverRetryRate", 0)
err = config.Register(&config.Option{
Name: "Do not use Multicast DNS",
Key: "intel/doNotUseMulticastDNS",
Description: "Multicast DNS queries other devices in the local network",
OptType: config.OptTypeInt,
ExpertiseLevel: config.ExpertiseLevelExpert,
ReleaseLevel: config.ReleaseLevelStable,
ExternalOptType: "security level",
DefaultValue: 6,
ValidationRegex: "^(7|6|4)$",
})
if err != nil {
return err
}
doNotUseMulticastDNS = status.ConfigIsActiveConcurrent("intel/doNotUseMulticastDNS")
err = config.Register(&config.Option{
Name: "Do not use assigned Nameservers",
Key: "intel/doNotUseAssignedNameservers",
Description: "that were acquired by the network (dhcp) or system",
OptType: config.OptTypeInt,
ExpertiseLevel: config.ExpertiseLevelExpert,
ReleaseLevel: config.ReleaseLevelStable,
ExternalOptType: "security level",
DefaultValue: 4,
ValidationRegex: "^(7|6|4)$",
})
if err != nil {
return err
}
doNotUseAssignedNameservers = status.ConfigIsActiveConcurrent("intel/doNotUseAssignedNameservers")
err = config.Register(&config.Option{
Name: "Do not resolve insecurely",
Key: "intel/doNotUseInsecureProtocols",
Description: "Do not resolve domains with insecure protocols, ie. plain DNS",
OptType: config.OptTypeInt,
ExpertiseLevel: config.ExpertiseLevelExpert,
ReleaseLevel: config.ReleaseLevelStable,
ExternalOptType: "security level",
DefaultValue: 4,
ValidationRegex: "^(7|6|4)$",
})
if err != nil {
return err
}
doNotUseInsecureProtocols = status.ConfigIsActiveConcurrent("intel/doNotUseInsecureProtocols")
err = config.Register(&config.Option{
Name: "Do not resolve special domains",
Key: "intel/doNotResolveSpecialDomains",
Description: fmt.Sprintf("Do not resolve the special top level domains %s", formatScopeList(specialServiceScopes)),
OptType: config.OptTypeInt,
ExpertiseLevel: config.ExpertiseLevelExpert,
ReleaseLevel: config.ReleaseLevelStable,
ExternalOptType: "security level",
DefaultValue: 7,
ValidationRegex: "^(7|6|4)$",
})
if err != nil {
return err
}
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
}
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, ", ")
}

30
resolver/doc.go Normal file
View File

@@ -0,0 +1,30 @@
/*
package resolver is responsible for fetching intelligence data, including DNS, on remote entities.
DNS Servers
Internal lists of resolvers to use are built on start and rebuilt on every config or network change.
Configured DNS servers are prioritized over servers assigned by dhcp. Domain and search options (here referred to as "search scopes") are being considered.
Security
Usage of DNS Servers can be regulated using the configuration:
DoNotUseAssignedDNS // Do not use DNS servers assigned by DHCP
DoNotUseMDNS // Do not use mDNS
DoNotForwardSpecialDomains // Do not forward special domains to local resolvers, except if they have a search scope for it
Note: The DHCP options "domain" and "search" are ignored for servers assigned by DHCP that do not reside within local address space.
Resolving DNS
Various different queries require the resolver to behave in different manner:
Queries for "localhost." are immediately responded with 127.0.0.1 and ::1, for A and AAAA queries and NXDomain for others.
Reverse lookups on local address ranges (10/8, 172.16/12, 192.168/16, fe80::/7) will be tried against every local resolver and finally mDNS until a successful, non-NXDomain answer is received.
Special domains ("example.", "example.com.", "example.net.", "example.org.", "invalid.", "test.", "onion.") are resolved using search scopes and local resolvers.
All other domains are resolved using search scopes and all available resolvers.
*/
package resolver

91
resolver/ipinfo.go Normal file
View File

@@ -0,0 +1,91 @@
package resolver
import (
"fmt"
"strings"
"sync"
"github.com/safing/portbase/database"
"github.com/safing/portbase/database/record"
"github.com/safing/portbase/utils"
)
var (
ipInfoDatabase = database.NewInterface(&database.Options{
AlwaysSetRelativateExpiry: 86400, // 24 hours
})
)
// IPInfo represents various information about an IP.
type IPInfo struct {
record.Base
sync.Mutex
IP string
Domains []string
}
func makeIPInfoKey(ip string) string {
return fmt.Sprintf("cache:intel/ipInfo/%s", ip)
}
// GetIPInfo gets an IPInfo record from the database.
func GetIPInfo(ip string) (*IPInfo, error) {
key := makeIPInfoKey(ip)
r, err := ipInfoDatabase.Get(key)
if err != nil {
return nil, err
}
// unwrap
if r.IsWrapped() {
// only allocate a new struct, if we need it
new := &IPInfo{}
err = record.Unwrap(r, new)
if err != nil {
return nil, err
}
return new, nil
}
// or adjust type
new, ok := r.(*IPInfo)
if !ok {
return nil, fmt.Errorf("record not of type *IPInfo, but %T", r)
}
return new, nil
}
// AddDomain adds a domain to the list and reports back if it was added, or was already present.
func (ipi *IPInfo) AddDomain(domain string) (added bool) {
ipi.Lock()
defer ipi.Unlock()
if !utils.StringInSlice(ipi.Domains, domain) {
ipi.Domains = append([]string{domain}, ipi.Domains...)
return true
}
return false
}
// Save saves the IPInfo record to the database.
func (ipi *IPInfo) Save() error {
ipi.Lock()
if !ipi.KeyIsSet() {
ipi.SetKey(makeIPInfoKey(ipi.IP))
}
ipi.Unlock()
return ipInfoDatabase.Put(ipi)
}
// FmtDomains returns a string consisting of the domains that have seen to use this IP, joined by " or "
func (ipi *IPInfo) FmtDomains() string {
return strings.Join(ipi.Domains, " or ")
}
// FmtDomains returns a string consisting of the domains that have seen to use this IP, joined by " or "
func (ipi *IPInfo) String() string {
ipi.Lock()
defer ipi.Unlock()
return fmt.Sprintf("<IPInfo[%s] %s: %s", ipi.Key(), ipi.IP, ipi.FmtDomains())
}

25
resolver/ipinfo_test.go Normal file
View File

@@ -0,0 +1,25 @@
package resolver
import "testing"
func testDomains(t *testing.T, ipi *IPInfo, expectedDomains string) {
if ipi.FmtDomains() != expectedDomains {
t.Errorf("unexpected domains '%s', expected '%s'", ipi.FmtDomains(), expectedDomains)
}
}
func TestIPInfo(t *testing.T) {
ipi := &IPInfo{
IP: "1.2.3.4",
Domains: []string{"example.com.", "sub.example.com."},
}
testDomains(t, ipi, "example.com. or sub.example.com.")
ipi.AddDomain("added.example.com.")
testDomains(t, ipi, "added.example.com. or example.com. or sub.example.com.")
ipi.AddDomain("sub.example.com.")
testDomains(t, ipi, "added.example.com. or example.com. or sub.example.com.")
ipi.AddDomain("added.example.com.")
testDomains(t, ipi, "added.example.com. or example.com. or sub.example.com.")
}

54
resolver/main.go Normal file
View File

@@ -0,0 +1,54 @@
package resolver
import (
"context"
"time"
"github.com/safing/portbase/log"
"github.com/safing/portbase/modules"
"github.com/safing/portmaster/intel"
// module dependencies
_ "github.com/safing/portmaster/core"
)
var (
module *modules.Module
)
func init() {
module = modules.Register("resolver", prep, start, nil, "core", "network")
}
func prep() error {
intel.SetReverseResolver(ResolveIPAndValidate)
return prepConfig()
}
func start() error {
// load resolvers from config and environment
loadResolvers()
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
}

6
resolver/main_test.go Normal file
View File

@@ -0,0 +1,6 @@
package resolver
import (
// portmaster tests helper
_ "github.com/safing/portmaster/core/pmtesting"
)

415
resolver/mdns.go Normal file
View File

@@ -0,0 +1,415 @@
package resolver
import (
"context"
"errors"
"fmt"
"net"
"strings"
"sync"
"time"
"github.com/miekg/dns"
"github.com/safing/portbase/log"
)
// DNS Classes
const (
DNSClassMulticast = dns.ClassINET | 1<<15
)
var (
multicast4Conn *net.UDPConn
multicast6Conn *net.UDPConn
unicast4Conn *net.UDPConn
unicast6Conn *net.UDPConn
questions = make(map[uint16]*savedQuestion)
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 {
question dns.Question
expires time.Time
response chan *RRCache
}
func indexOfRR(entry *dns.RR_Header, list *[]dns.RR) int {
for k, v := range *list {
if entry.Name == v.Header().Name && entry.Rrtype == v.Header().Rrtype {
return k
}
}
return -1
}
//nolint:gocyclo,gocognit // TODO: make simpler
func listenToMDNS(ctx context.Context) error {
var err error
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})
if err != nil {
// TODO: retry after some time
log.Warningf("intel(mdns): failed to create udp4 listen multicast socket: %s", err)
} else {
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})
if err != nil {
// TODO: retry after some time
log.Warningf("intel(mdns): failed to create udp6 listen multicast socket: %s", err)
} else {
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})
if err != nil {
// TODO: retry after some time
log.Warningf("intel(mdns): failed to create udp4 listen socket: %s", err)
} else {
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})
if err != nil {
// TODO: retry after some time
log.Warningf("intel(mdns): failed to create udp6 listen socket: %s", err)
} else {
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 {
select {
case <-ctx.Done():
return nil
case message := <-messages:
// log.Tracef("intel: got net mdns message: %s", message)
var err error
var question *dns.Question
var saveFullRequest bool
scavengedRecords := make(map[string]dns.RR)
var rrCache *RRCache
// save every received response
// if previous save was less than 2 seconds ago, add to response, else replace
// pick out A and AAAA records and save separately
// continue if not response
if !message.Response {
// log.Tracef("intel: mdns message has no response, ignoring")
continue
}
// continue if rcode is not success
if message.Rcode != dns.RcodeSuccess {
// log.Tracef("intel: mdns message has error, ignoring")
continue
}
// continue if answer section is empty
if len(message.Answer) == 0 {
// log.Tracef("intel: mdns message has no answers, ignoring")
continue
}
// return saved question
questionsLock.Lock()
savedQ := questions[message.MsgHdr.Id]
questionsLock.Unlock()
// get question, some servers do not reply with question
if len(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 {
// continue if class is not INTERNET
if question.Qclass != dns.ClassINET && question.Qclass != DNSClassMulticast {
continue
}
// mark request to be saved
saveFullRequest = true
}
// get entry from database
if saveFullRequest {
rrCache, err = GetRRCache(question.Name, dns.Type(question.Qtype))
if err != nil || rrCache.updated < time.Now().Add(-2*time.Second).Unix() || rrCache.TTL < time.Now().Unix() {
rrCache = &RRCache{
Domain: question.Name,
Question: dns.Type(question.Qtype),
}
}
}
for _, entry := range message.Answer {
if strings.HasSuffix(entry.Header().Name, ".local.") || domainInScope(entry.Header().Name, localReverseScopes) {
if saveFullRequest {
k := indexOfRR(entry.Header(), &rrCache.Answer)
if k == -1 {
rrCache.Answer = append(rrCache.Answer, entry)
} else {
rrCache.Answer[k] = entry
}
}
switch entry.(type) {
case *dns.A:
scavengedRecords[fmt.Sprintf("%sA", entry.Header().Name)] = entry
case *dns.AAAA:
scavengedRecords[fmt.Sprintf("%sAAAA", entry.Header().Name)] = entry
case *dns.PTR:
if !strings.HasPrefix(entry.Header().Name, "_") {
scavengedRecords[fmt.Sprintf("%sPTR", entry.Header().Name)] = entry
}
}
}
}
for _, entry := range message.Ns {
if strings.HasSuffix(entry.Header().Name, ".local.") || domainInScope(entry.Header().Name, localReverseScopes) {
if saveFullRequest {
k := indexOfRR(entry.Header(), &rrCache.Ns)
if k == -1 {
rrCache.Ns = append(rrCache.Ns, entry)
} else {
rrCache.Ns[k] = entry
}
}
switch entry.(type) {
case *dns.A:
scavengedRecords[fmt.Sprintf("%s_A", entry.Header().Name)] = entry
case *dns.AAAA:
scavengedRecords[fmt.Sprintf("%s_AAAA", entry.Header().Name)] = entry
case *dns.PTR:
if !strings.HasPrefix(entry.Header().Name, "_") {
scavengedRecords[fmt.Sprintf("%s_PTR", entry.Header().Name)] = entry
}
}
}
}
for _, entry := range message.Extra {
if strings.HasSuffix(entry.Header().Name, ".local.") || domainInScope(entry.Header().Name, localReverseScopes) {
if saveFullRequest {
k := indexOfRR(entry.Header(), &rrCache.Extra)
if k == -1 {
rrCache.Extra = append(rrCache.Extra, entry)
} else {
rrCache.Extra[k] = entry
}
}
switch entry.(type) {
case *dns.A:
scavengedRecords[fmt.Sprintf("%sA", entry.Header().Name)] = entry
case *dns.AAAA:
scavengedRecords[fmt.Sprintf("%sAAAA", entry.Header().Name)] = entry
case *dns.PTR:
if !strings.HasPrefix(entry.Header().Name, "_") {
scavengedRecords[fmt.Sprintf("%sPTR", entry.Header().Name)] = entry
}
}
}
}
var questionID string
if saveFullRequest {
rrCache.Clean(60)
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())
}
for k, v := range scavengedRecords {
if saveFullRequest && k == questionID {
continue
}
rrCache = &RRCache{
Domain: v.Header().Name,
Question: dns.Type(v.Header().Class),
Answer: []dns.RR{v},
}
rrCache.Clean(60)
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)
}
}
cleanSavedQuestions()
}
}
func listenForDNSPackets(conn *net.UDPConn, messages chan *dns.Msg) error {
buf := make([]byte, 65536)
for {
n, err := conn.Read(buf)
if err != nil {
if module.IsStopping() {
return nil
}
log.Debugf("intel: failed to read packet: %s", err)
return err
}
message := new(dns.Msg)
if err = message.Unpack(buf[:n]); err != nil {
log.Debugf("intel: failed to unpack message: %s", err)
continue
}
messages <- message
}
}
func queryMulticastDNS(ctx context.Context, q *Query) (*RRCache, error) {
// check for active connections
if unicast4Conn == nil && unicast6Conn == nil {
return nil, errors.New("unicast mdns connections not initialized")
}
// 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})
if err != nil {
return nil, fmt.Errorf("failed to send query: %s", err)
}
}
if unicast6Conn != nil && uint16(q.QType) != dns.TypeA {
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})
if err != nil {
return nil, fmt.Errorf("failed to send query: %s", err)
}
}
// wait for response or timeout
select {
case rrCache := <-response:
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 nil, ErrNotFound
}
func cleanSavedQuestions() {
questionsLock.Lock()
defer questionsLock.Unlock()
now := time.Now()
for msgID, savedQuestion := range questions {
if now.After(savedQuestion.expires) {
delete(questions, msgID)
}
}
}

75
resolver/namerecord.go Normal file
View File

@@ -0,0 +1,75 @@
package resolver
import (
"errors"
"fmt"
"sync"
"github.com/safing/portbase/database"
"github.com/safing/portbase/database/record"
)
var (
recordDatabase = database.NewInterface(&database.Options{
AlwaysSetRelativateExpiry: 2592000, // 30 days
CacheSize: 128,
})
)
// NameRecord is helper struct to RRCache to better save data to the database.
type NameRecord struct {
record.Base
sync.Mutex
Domain string
Question string
Answer []string
Ns []string
Extra []string
TTL int64
Server string
ServerScope int8
}
func makeNameRecordKey(domain string, question string) string {
return fmt.Sprintf("cache:intel/nameRecord/%s%s", domain, question)
}
// GetNameRecord gets a NameRecord from the database.
func GetNameRecord(domain string, question string) (*NameRecord, error) {
key := makeNameRecordKey(domain, question)
r, err := recordDatabase.Get(key)
if err != nil {
return nil, err
}
// unwrap
if r.IsWrapped() {
// only allocate a new struct, if we need it
new := &NameRecord{}
err = record.Unwrap(r, new)
if err != nil {
return nil, err
}
return new, nil
}
// or adjust type
new, ok := r.(*NameRecord)
if !ok {
return nil, fmt.Errorf("record not of type *NameRecord, but %T", r)
}
return new, nil
}
// Save saves the NameRecord to the database.
func (rec *NameRecord) Save() error {
if rec.Domain == "" || rec.Question == "" {
return errors.New("could not save NameRecord, missing Domain and/or Question")
}
rec.SetKey(makeNameRecordKey(rec.Domain, rec.Question))
return recordDatabase.PutNew(rec)
}

272
resolver/resolve.go Normal file
View File

@@ -0,0 +1,272 @@
package resolver
import (
"context"
"errors"
"fmt"
"sync"
"time"
"github.com/miekg/dns"
"github.com/safing/portbase/database"
"github.com/safing/portbase/log"
)
var (
mtAsyncResolve = "async resolve"
// basic errors
// 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")
// detailed errors
// ErrTestDomainsDisabled wraps ErrBlocked
ErrTestDomainsDisabled = fmt.Errorf("%w: test domains disabled", ErrBlocked)
// ErrSpecialDomainsDisabled wraps ErrBlocked
ErrSpecialDomainsDisabled = fmt.Errorf("%w: special domains disabled", ErrBlocked)
// ErrInvalid wraps ErrNotFound
ErrInvalid = fmt.Errorf("%w: invalid request", ErrNotFound)
// 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)
)
// Query describes a dns query.
type Query struct {
FQDN string
QType dns.Type
SecurityLevel uint8
NoCaching bool
IgnoreFailing bool
LocalResolversOnly bool
// internal
dotPrefixedFQDN string
}
// 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.
func Resolve(ctx context.Context, q *Query) (rrCache *RRCache, err error) {
// sanity check
if q == nil || !q.check() {
return nil, ErrInvalid
}
// log
log.Tracer(ctx).Tracef("intel: resolving %s%s", q.FQDN, q.QType)
// check query compliance
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 != database.ErrNotFound {
log.Tracer(ctx).Warningf("intel: getting RRCache %s%s from database failed: %s", q.FQDN, q.QType.String(), err)
log.Warningf("intel: getting RRCache %s%s from database failed: %s", q.FQDN, q.QType.String(), err)
}
return nil
}
// get resolver that rrCache was resolved with
resolver := getResolverByIDWithLocking(rrCache.Server)
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.Unlock()
log.Tracer(ctx).Trace("intel: serving from cache, requesting new")
// resolve async
module.StartMediumPriorityMicroTask(&mtAsyncResolve, func(ctx context.Context) error {
_, _ = resolveAndCache(ctx, q)
return nil
})
}
return rrCache
}
func deduplicateRequest(ctx context.Context, q *Query) (finishRequest func()) {
// create identifier key
dupKey := fmt.Sprintf("%s%s", q.FQDN, q.QType.String())
dupReqLock.Lock()
defer dupReqLock.Unlock()
// get duplicate request waitgroup
wg, requestActive := dupReqMap[dupKey]
// someone else is already on it!
if requestActive {
// log that we are waiting
log.Tracer(ctx).Tracef("intel: waiting for duplicate query for %s to complete", dupKey)
// wait
wg.Wait()
// done!
return nil
}
// 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()
defer dupReqLock.Unlock()
// mark request as done
wg.Done()
// delete from registry
delete(dupReqMap, dupKey)
}
}
func resolveAndCache(ctx context.Context, q *Query) (rrCache *RRCache, err error) {
// get resolvers
resolvers := GetResolversInScope(ctx, q)
if len(resolvers) == 0 {
return nil, ErrNoCompliance
}
// prep
lastFailBoundary := time.Now().Add(
-time.Duration(nameserverRetryRate()) * time.Second,
)
// start resolving
var i int
// once with skipping recently failed resolvers, once without
resolveLoop:
for i = 0; i < 2; i++ {
for _, resolver := range resolvers {
// check if resolver failed recently (on first run)
if i == 0 && resolver.Conn.LastFail().After(lastFailBoundary) {
log.Tracer(ctx).Tracef("intel: skipping resolver %s, because it failed recently", resolver)
continue
}
// resolve
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
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 {
return nil, err
}
// check for result
if rrCache == nil /* defensive */ {
return nil, ErrNotFound
}
// cache if enabled
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
}

15
resolver/resolve_test.go Normal file
View File

@@ -0,0 +1,15 @@
package resolver
// DISABLE TESTING FOR NOW: find a way to have tests with the module system
// import (
// "testing"
// "time"
//
// "github.com/miekg/dns"
// )
// func TestResolve(t *testing.T) {
// Resolve("google.com.", dns.Type(dns.TypeA), 0)
// time.Sleep(200 * time.Millisecond)
// }

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

@@ -0,0 +1,276 @@
package resolver
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
}

153
resolver/resolver.go Normal file
View File

@@ -0,0 +1,153 @@
package resolver
import (
"context"
"net"
"sync"
"time"
"github.com/miekg/dns"
"github.com/safing/portbase/log"
"github.com/safing/portmaster/network/environment"
)
// DNS Resolver Attributes
const (
ServerTypeDNS = "dns"
ServerTypeTCP = "tcp"
ServerTypeDoT = "dot"
ServerTypeDoH = "doh"
ServerSourceConfigured = "config"
ServerSourceAssigned = "dhcp"
ServerSourceMDNS = "mdns"
)
// Resolver holds information about an active resolver.
type Resolver struct {
// Server config url (and ID)
Server string
// Parsed config
ServerType string
ServerAddress string
ServerIP net.IP
ServerIPScope int8
ServerPort uint16
// Special Options
VerifyDomain string
Search []string
SkipFQDN string
Source string
// logic interface
Conn ResolverConn
}
// String returns the URL representation of the resolver.
func (resolver *Resolver) String() string {
return resolver.Server
}
// 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.
func (brc *BasicResolverConn) LastFail() time.Time {
brc.Lock()
defer brc.Unlock()
return brc.lastFail
}
// Query executes the given query against the resolver.
func (brc *BasicResolverConn) Query(ctx context.Context, q *Query) (*RRCache, error) {
// convenience
resolver := brc.resolver
// create query
dnsQuery := new(dns.Msg)
dnsQuery.SetQuestion(q.FQDN, uint16(q.QType))
// start
var reply *dns.Msg
var err error
for i := 0; i < 3; i++ {
// log query time
// 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))
// 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
// hint network environment at failed connection
environment.ReportFailedConnection()
// 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", q.FQDN, q.QType, resolver.Server)
continue
}
// permanent error
break
}
// no error
break
}
if err != nil {
return nil, err
// FIXME: mark as failed
}
// 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,
}
// TODO: check if reply.Answer is valid
return new, nil
}

357
resolver/resolvers.go Normal file
View File

@@ -0,0 +1,357 @@
package resolver
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
}

View File

@@ -0,0 +1,36 @@
package resolver
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)
}

82
resolver/reverse.go Normal file
View File

@@ -0,0 +1,82 @@
package resolver
import (
"context"
"fmt"
"strings"
"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.
func ResolveIPAndValidate(ctx context.Context, ip string, securityLevel uint8) (domain string, err error) {
// get reversed DNS address
reverseIP, err := dns.ReverseAddr(ip)
if err != nil {
log.Tracef("intel: failed to get reverse address of %s: %s", ip, err)
return "", ErrInvalid
}
// get PTR record
q := &Query{
FQDN: reverseIP,
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
var ptrName string
for _, rr := range rrCache.Answer {
ptrRec, ok := rr.(*dns.PTR)
if ok {
ptrName = ptrRec.Ptr
break
}
}
// check for nxDomain
if ptrName == "" {
return "", fmt.Errorf("%w: %s%s", ErrNotFound, q.FQDN, q.QType)
}
// get forward record
q = &Query{
FQDN: ptrName,
SecurityLevel: securityLevel,
}
// IPv4/6 switch
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
for _, rr := range rrCache.Answer {
switch v := rr.(type) {
case *dns.A:
log.Infof("A: %s", v.A.String())
if ip == v.A.String() {
return ptrName, nil
}
case *dns.AAAA:
log.Infof("AAAA: %s", v.AAAA.String())
if ip == v.AAAA.String() {
return ptrName, nil
}
}
}
// no match
return "", ErrBlocked
}

37
resolver/reverse_test.go Normal file
View File

@@ -0,0 +1,37 @@
package resolver
import (
"context"
"testing"
"github.com/safing/portbase/log"
)
func testReverse(t *testing.T, ip, result, expectedErr string) {
ctx, tracer := log.AddTracer(context.Background())
defer tracer.Submit()
domain, err := ResolveIPAndValidate(ctx, ip, 0)
if err != nil {
tracer.Warning(err.Error())
if expectedErr == "" || err.Error() != expectedErr {
t.Errorf("reverse-validating %s: unexpected error: %s", ip, err)
}
return
}
if domain != result {
t.Errorf("reverse-validating %s: unexpected result: %s", ip, domain)
}
}
func TestResolveIPAndValidate(t *testing.T) {
testReverse(t, "198.41.0.4", "a.root-servers.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, "1.1.1.1", "one.one.one.one.", "")
testReverse(t, "2606:4700:4700::1111", "one.one.one.one.", "")
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.", "record does not exist: 153.109.199.185.in-addr.arpa.PTR")
}

220
resolver/rrcache.go Normal file
View File

@@ -0,0 +1,220 @@
package resolver
import (
"fmt"
"math/rand"
"net"
"sync"
"time"
"github.com/miekg/dns"
)
// RRCache is used to cache DNS data
//nolint:maligned // TODO
type RRCache struct {
sync.Mutex
Domain string // constant
Question dns.Type // constant
Answer []dns.RR // might be mixed
Ns []dns.RR // constant
Extra []dns.RR // constant
TTL int64 // constant
Server string // constant
ServerScope int8 // constant
servedFromCache bool // mutable
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.
func (rrCache *RRCache) Clean(minExpires uint32) {
var lowestTTL uint32 = 0xFFFFFFFF
var header *dns.RR_Header
// set TTLs to 17
// TODO: double append? is there something more elegant?
for _, rr := range append(rrCache.Answer, append(rrCache.Ns, rrCache.Extra...)...) {
header = rr.Header()
if lowestTTL > header.Ttl {
lowestTTL = header.Ttl
}
header.Ttl = 17
}
// TTL must be at least minExpires
if lowestTTL < minExpires {
lowestTTL = minExpires
}
// log.Tracef("lowest TTL is %d", lowestTTL)
rrCache.TTL = time.Now().Unix() + int64(lowestTTL)
}
// ExportAllARecords return of a list of all A and AAAA IP addresses.
func (rrCache *RRCache) ExportAllARecords() (ips []net.IP) {
for _, rr := range rrCache.Answer {
if rr.Header().Class != dns.ClassINET {
continue
}
switch rr.Header().Rrtype {
case dns.TypeA:
aRecord, ok := rr.(*dns.A)
if ok {
ips = append(ips, aRecord.A)
}
case dns.TypeAAAA:
aaaaRecord, ok := rr.(*dns.AAAA)
if ok {
ips = append(ips, aaaaRecord.AAAA)
}
}
}
return
}
// ToNameRecord converts the RRCache to a NameRecord for cleaner persistence.
func (rrCache *RRCache) ToNameRecord() *NameRecord {
new := &NameRecord{
Domain: rrCache.Domain,
Question: rrCache.Question.String(),
TTL: rrCache.TTL,
Server: rrCache.Server,
ServerScope: rrCache.ServerScope,
}
// stringify RR entries
for _, entry := range rrCache.Answer {
new.Answer = append(new.Answer, entry.String())
}
for _, entry := range rrCache.Ns {
new.Ns = append(new.Ns, entry.String())
}
for _, entry := range rrCache.Extra {
new.Extra = append(new.Extra, entry.String())
}
return new
}
// Save saves the RRCache to the database as a NameRecord.
func (rrCache *RRCache) Save() error {
return rrCache.ToNameRecord().Save()
}
// GetRRCache tries to load the corresponding NameRecord from the database and convert it.
func GetRRCache(domain string, question dns.Type) (*RRCache, error) {
rrCache := &RRCache{
Domain: domain,
Question: question,
}
nameRecord, err := GetNameRecord(domain, question.String())
if err != nil {
return nil, err
}
rrCache.TTL = nameRecord.TTL
for _, entry := range nameRecord.Answer {
rr, err := dns.NewRR(entry)
if err == nil {
rrCache.Answer = append(rrCache.Answer, rr)
}
}
for _, entry := range nameRecord.Ns {
rr, err := dns.NewRR(entry)
if err == nil {
rrCache.Ns = append(rrCache.Ns, rr)
}
}
for _, entry := range nameRecord.Extra {
rr, err := dns.NewRR(entry)
if err == nil {
rrCache.Extra = append(rrCache.Extra, rr)
}
}
rrCache.Server = nameRecord.Server
rrCache.ServerScope = nameRecord.ServerScope
rrCache.servedFromCache = true
return rrCache, nil
}
// ServedFromCache marks the RRCache as served from cache.
func (rrCache *RRCache) ServedFromCache() bool {
return rrCache.servedFromCache
}
// RequestingNew informs that it has expired and new RRs are being fetched.
func (rrCache *RRCache) RequestingNew() bool {
return rrCache.requestingNew
}
// Flags formats ServedFromCache and RequestingNew to a condensed, flag-like format.
func (rrCache *RRCache) Flags() string {
var s string
if rrCache.servedFromCache {
s += "C"
}
if rrCache.requestingNew {
s += "R"
}
if rrCache.Filtered {
s += "F"
}
if s != "" {
return fmt.Sprintf(" [%s]", s)
}
return ""
}
// IsNXDomain returnes whether the result is nxdomain.
func (rrCache *RRCache) IsNXDomain() bool {
return len(rrCache.Answer) == 0
}
// ShallowCopy returns a shallow copy of the cache. slices are not copied, but referenced.
func (rrCache *RRCache) ShallowCopy() *RRCache {
return &RRCache{
Domain: rrCache.Domain,
Question: rrCache.Question,
Answer: rrCache.Answer,
Ns: rrCache.Ns,
Extra: rrCache.Extra,
TTL: rrCache.TTL,
Server: rrCache.Server,
ServerScope: rrCache.ServerScope,
updated: rrCache.updated,
servedFromCache: rrCache.servedFromCache,
requestingNew: rrCache.requestingNew,
Filtered: rrCache.Filtered,
FilteredEntries: rrCache.FilteredEntries,
}
}