Move some Resolver information to ResolverInfo and propagate it

This commit is contained in:
Daniel 2021-03-20 22:19:27 +01:00
parent 43cfba8445
commit 20383226f8
13 changed files with 275 additions and 180 deletions

View file

@ -85,6 +85,9 @@ type Connection struct { //nolint:maligned // TODO: fix alignment
// be added to it during the livetime of a connection. Access to // be added to it during the livetime of a connection. Access to
// entity must be guarded by the connection lock. // entity must be guarded by the connection lock.
Entity *intel.Entity Entity *intel.Entity
// Resolver holds information about the resolver used to resolve
// Entity.Domain.
Resolver *resolver.ResolverInfo
// Verdict is the final decision that has been made for a connection. // Verdict is the final decision that has been made for a connection.
// The verdict may change so any access to it must be guarded by the // The verdict may change so any access to it must be guarded by the
// connection lock. // connection lock.
@ -320,6 +323,7 @@ func NewConnectionFromFirstPacket(pkt packet.Packet) *Connection {
scope = lastResolvedDomain.Domain scope = lastResolvedDomain.Domain
entity.Domain = lastResolvedDomain.Domain entity.Domain = lastResolvedDomain.Domain
entity.CNAME = lastResolvedDomain.CNAMEs entity.CNAME = lastResolvedDomain.CNAMEs
resolverInfo = lastResolvedDomain.Resolver
removeOpenDNSRequest(proc.Pid, lastResolvedDomain.Domain) removeOpenDNSRequest(proc.Pid, lastResolvedDomain.Domain)
} }
} }
@ -364,6 +368,8 @@ func NewConnectionFromFirstPacket(pkt packet.Packet) *Connection {
process: proc, process: proc,
// remote endpoint // remote endpoint
Entity: entity, Entity: entity,
// resolver used to resolve dns request
Resolver: resolverInfo,
// meta // meta
Started: time.Now().Unix(), Started: time.Now().Unix(),
ProfileRevisionCounter: proc.Profile().RevisionCnt(), ProfileRevisionCounter: proc.Profile().RevisionCnt(),

View file

@ -40,6 +40,10 @@ type ResolvedDomain struct {
// Domain. // Domain.
CNAMEs []string CNAMEs []string
// Resolver holds basic information about the resolver that provided this
// information.
Resolver *ResolverInfo
// Expires holds the timestamp when this entry expires. // Expires holds the timestamp when this entry expires.
// This does not mean that the entry may not be used anymore afterwards, // This does not mean that the entry may not be used anymore afterwards,
// but that this is used to calcuate the TTL of the database record. // but that this is used to calcuate the TTL of the database record.

View file

@ -49,9 +49,20 @@ type NameRecord struct {
Extra []string Extra []string
Expires int64 Expires int64
Server string Resolver *ResolverInfo
ServerScope int8 }
ServerInfo string
// IsValid returns whether the NameRecord is valid and may be used. Otherwise,
// it should be disregarded.
func (nameRecord *NameRecord) IsValid() bool {
switch {
case nameRecord.Resolver == nil || nameRecord.Resolver.Type == "":
// Changed in v0.6.7: Introduced Resolver *ResolverInfo
return false
default:
// Up to date!
return true
}
} }
func makeNameRecordKey(domain string, question string) string { func makeNameRecordKey(domain string, question string) string {
@ -67,7 +78,7 @@ func GetNameRecord(domain, question string) (*NameRecord, error) {
return nil, err return nil, err
} }
// unwrap // Unwrap record if it's wrapped.
if r.IsWrapped() { if r.IsWrapped() {
// only allocate a new struct, if we need it // only allocate a new struct, if we need it
new := &NameRecord{} new := &NameRecord{}
@ -75,14 +86,24 @@ func GetNameRecord(domain, question string) (*NameRecord, error) {
if err != nil { if err != nil {
return nil, err return nil, err
} }
// Check if the record is valid.
if !new.IsValid() {
return nil, errors.New("record is invalid (outdated format)")
}
return new, nil return new, nil
} }
// or adjust type // Or just adjust the type.
new, ok := r.(*NameRecord) new, ok := r.(*NameRecord)
if !ok { if !ok {
return nil, fmt.Errorf("record not of type *NameRecord, but %T", r) return nil, fmt.Errorf("record not of type *NameRecord, but %T", r)
} }
// Check if the record is valid.
if !new.IsValid() {
return nil, errors.New("record is invalid (outdated format)")
}
return new, nil return new, nil
} }

View file

@ -175,9 +175,9 @@ func checkCache(ctx context.Context, q *Query) *RRCache {
} }
// Get the resolver that the rrCache was resolved with. // Get the resolver that the rrCache was resolved with.
resolver := getActiveResolverByIDWithLocking(rrCache.Server) resolver := getActiveResolverByIDWithLocking(rrCache.Resolver.ID())
if resolver == nil { if resolver == nil {
log.Tracer(ctx).Debugf("resolver: ignoring RRCache %s%s because source server %s has been removed", q.FQDN, q.QType.String(), rrCache.Server) log.Tracer(ctx).Debugf("resolver: ignoring RRCache %s%s because source server %q has been removed", q.FQDN, q.QType.String(), rrCache.Resolver.ID())
return nil return nil
} }
@ -361,11 +361,11 @@ resolveLoop:
continue continue
case errors.Is(err, ErrTimeout): case errors.Is(err, ErrTimeout):
resolver.Conn.ReportFailure() resolver.Conn.ReportFailure()
log.Tracer(ctx).Debugf("resolver: query to %s timed out", resolver.GetName()) log.Tracer(ctx).Debugf("resolver: query to %s timed out", resolver.Info.ID())
continue continue
default: default:
resolver.Conn.ReportFailure() resolver.Conn.ReportFailure()
log.Tracer(ctx).Debugf("resolver: query to %s failed: %s", resolver.GetName(), err) log.Tracer(ctx).Debugf("resolver: query to %s failed: %s", resolver.Info.ID(), err)
continue continue
} }
} }

View file

@ -20,12 +20,13 @@ const (
var ( var (
envResolver = &Resolver{ envResolver = &Resolver{
Server: ServerSourceEnv, ConfigURL: ServerSourceEnv,
ServerType: ServerTypeEnv, Info: &ResolverInfo{
ServerIPScope: netutils.SiteLocal, Type: ServerTypeEnv,
ServerInfo: "Portmaster environment", Source: ServerSourceEnv,
Source: ServerSourceEnv, IPScope: netutils.SiteLocal,
Conn: &envResolverConn{}, },
Conn: &envResolverConn{},
} }
envResolvers = []*Resolver{envResolver} envResolvers = []*Resolver{envResolver}
@ -109,14 +110,12 @@ func (er *envResolverConn) makeRRCache(q *Query, answers []dns.RR) *RRCache {
q.NoCaching = true q.NoCaching = true
return &RRCache{ return &RRCache{
Domain: q.FQDN, Domain: q.FQDN,
Question: q.QType, Question: q.QType,
RCode: dns.RcodeSuccess, RCode: dns.RcodeSuccess,
Answer: answers, Answer: answers,
Extra: []dns.RR{internalSpecialUseComment}, // Always add comment about this TLD. Extra: []dns.RR{internalSpecialUseComment}, // Always add comment about this TLD.
Server: envResolver.Server, Resolver: envResolver.Info.Copy(),
ServerScope: envResolver.ServerIPScope,
ServerInfo: envResolver.ServerInfo,
} }
} }

View file

@ -31,12 +31,13 @@ var (
questionsLock sync.Mutex questionsLock sync.Mutex
mDNSResolver = &Resolver{ mDNSResolver = &Resolver{
Server: ServerSourceMDNS, ConfigURL: ServerSourceMDNS,
ServerType: ServerTypeDNS, Info: &ResolverInfo{
ServerIPScope: netutils.SiteLocal, Type: ServerTypeMDNS,
ServerInfo: "mDNS resolver", Source: ServerSourceMDNS,
Source: ServerSourceMDNS, IPScope: netutils.SiteLocal,
Conn: &mDNSResolverConn{}, },
Conn: &mDNSResolverConn{},
} }
mDNSResolvers = []*Resolver{mDNSResolver} mDNSResolvers = []*Resolver{mDNSResolver}
) )
@ -200,12 +201,10 @@ func handleMDNSMessages(ctx context.Context, messages chan *dns.Msg) error {
// create new and do not append // create new and do not append
if err != nil || rrCache.Modified < time.Now().Add(-2*time.Second).Unix() || rrCache.Expired() { if err != nil || rrCache.Modified < time.Now().Add(-2*time.Second).Unix() || rrCache.Expired() {
rrCache = &RRCache{ rrCache = &RRCache{
Domain: question.Name, Domain: question.Name,
Question: dns.Type(question.Qtype), Question: dns.Type(question.Qtype),
RCode: dns.RcodeSuccess, RCode: dns.RcodeSuccess,
Server: mDNSResolver.Server, Resolver: mDNSResolver.Info.Copy(),
ServerScope: mDNSResolver.ServerIPScope,
ServerInfo: mDNSResolver.ServerInfo,
} }
} }
} }
@ -302,13 +301,11 @@ func handleMDNSMessages(ctx context.Context, messages chan *dns.Msg) error {
continue continue
} }
rrCache = &RRCache{ rrCache = &RRCache{
Domain: v.Header().Name, Domain: v.Header().Name,
Question: dns.Type(v.Header().Class), Question: dns.Type(v.Header().Class),
RCode: dns.RcodeSuccess, RCode: dns.RcodeSuccess,
Answer: []dns.RR{v}, Answer: []dns.RR{v},
Server: mDNSResolver.Server, Resolver: mDNSResolver.Info.Copy(),
ServerScope: mDNSResolver.ServerIPScope,
ServerInfo: mDNSResolver.ServerInfo,
} }
rrCache.Clean(minMDnsTTL) rrCache.Clean(minMDnsTTL)
err := rrCache.Save() err := rrCache.Save()
@ -423,12 +420,10 @@ func queryMulticastDNS(ctx context.Context, q *Query) (*RRCache, error) {
// Respond with NXDomain. // Respond with NXDomain.
return &RRCache{ return &RRCache{
Domain: q.FQDN, Domain: q.FQDN,
Question: q.QType, Question: q.QType,
RCode: dns.RcodeNameError, RCode: dns.RcodeNameError,
Server: mDNSResolver.Server, Resolver: mDNSResolver.Info.Copy(),
ServerScope: mDNSResolver.ServerIPScope,
ServerInfo: mDNSResolver.ServerInfo,
}, nil }, nil
} }

View file

@ -72,22 +72,20 @@ func (pr *PlainResolver) Query(ctx context.Context, q *Query) (*RRCache, error)
// check if blocked // check if blocked
if pr.resolver.IsBlockedUpstream(reply) { if pr.resolver.IsBlockedUpstream(reply) {
return nil, &BlockedUpstreamError{pr.resolver.GetName()} return nil, &BlockedUpstreamError{pr.resolver.Info.DescriptiveName()}
} }
// hint network environment at successful connection // hint network environment at successful connection
netenv.ReportSuccessfulConnection() netenv.ReportSuccessfulConnection()
newRecord := &RRCache{ newRecord := &RRCache{
Domain: q.FQDN, Domain: q.FQDN,
Question: q.QType, Question: q.QType,
RCode: reply.Rcode, RCode: reply.Rcode,
Answer: reply.Answer, Answer: reply.Answer,
Ns: reply.Ns, Ns: reply.Ns,
Extra: reply.Extra, Extra: reply.Extra,
Server: pr.resolver.Server, Resolver: pr.resolver.Info.Copy(),
ServerScope: pr.resolver.ServerIPScope,
ServerInfo: pr.resolver.ServerInfo,
} }
// TODO: check if reply.Answer is valid // TODO: check if reply.Answer is valid

View file

@ -49,15 +49,13 @@ type InFlightQuery struct {
// MakeCacheRecord creates an RCache record from a reply. // MakeCacheRecord creates an RCache record from a reply.
func (ifq *InFlightQuery) MakeCacheRecord(reply *dns.Msg) *RRCache { func (ifq *InFlightQuery) MakeCacheRecord(reply *dns.Msg) *RRCache {
return &RRCache{ return &RRCache{
Domain: ifq.Query.FQDN, Domain: ifq.Query.FQDN,
Question: ifq.Query.QType, Question: ifq.Query.QType,
RCode: reply.Rcode, RCode: reply.Rcode,
Answer: reply.Answer, Answer: reply.Answer,
Ns: reply.Ns, Ns: reply.Ns,
Extra: reply.Extra, Extra: reply.Extra,
Server: ifq.Resolver.Server, Resolver: ifq.Resolver.Info.Copy(),
ServerScope: ifq.Resolver.ServerIPScope,
ServerInfo: ifq.Resolver.ServerInfo,
} }
} }
@ -172,7 +170,7 @@ func (tr *TCPResolver) Query(ctx context.Context, q *Query) (*RRCache, error) {
} }
if tr.resolver.IsBlockedUpstream(reply) { if tr.resolver.IsBlockedUpstream(reply) {
return nil, &BlockedUpstreamError{tr.resolver.GetName()} return nil, &BlockedUpstreamError{tr.resolver.Info.DescriptiveName()}
} }
return inFlight.MakeCacheRecord(reply), nil return inFlight.MakeCacheRecord(reply), nil
@ -189,7 +187,7 @@ func (tr *TCPResolver) checkClientStatus() {
select { select {
case tr.clientHeartbeat <- struct{}{}: case tr.clientHeartbeat <- struct{}{}:
case <-time.After(heartbeatTimeout): case <-time.After(heartbeatTimeout):
log.Warningf("resolver: heartbeat failed for %s dns client, stopping", tr.resolver.GetName()) log.Warningf("resolver: heartbeat failed for %s dns client, stopping", tr.resolver.Info.DescriptiveName())
stopClient() stopClient()
} }
} }
@ -299,7 +297,7 @@ func (mgr *tcpResolverConnMgr) waitForWork(clientCtx context.Context) (proceed b
select { select {
case mgr.tr.queries <- inFlight.Msg: case mgr.tr.queries <- inFlight.Msg:
default: default:
log.Warningf("resolver: failed to re-inject abandoned query to %s", mgr.tr.resolver.GetName()) log.Warningf("resolver: failed to re-inject abandoned query to %s", mgr.tr.resolver.Info.DescriptiveName())
} }
} }
// in-flight queries that match the connection instance ID are not changed. They are already in the queue. // in-flight queries that match the connection instance ID are not changed. They are already in the queue.
@ -317,7 +315,7 @@ func (mgr *tcpResolverConnMgr) waitForWork(clientCtx context.Context) (proceed b
select { select {
case mgr.tr.queries <- msg: case mgr.tr.queries <- msg:
case <-time.After(2 * time.Second): case <-time.After(2 * time.Second):
log.Warningf("resolver: failed to re-inject waking query to %s", mgr.tr.resolver.GetName()) log.Warningf("resolver: failed to re-inject waking query to %s", mgr.tr.resolver.Info.DescriptiveName())
} }
return nil return nil
}) })
@ -343,7 +341,7 @@ func (mgr *tcpResolverConnMgr) establishConnection() (
var err error var err error
conn, err = mgr.tr.dnsClient.Dial(mgr.tr.resolver.ServerAddress) conn, err = mgr.tr.dnsClient.Dial(mgr.tr.resolver.ServerAddress)
if err != nil { if err != nil {
log.Debugf("resolver: failed to connect to %s (%s)", mgr.tr.resolver.GetName(), mgr.tr.resolver.ServerAddress) log.Debugf("resolver: failed to connect to %s", mgr.tr.resolver.Info.DescriptiveName())
return nil, nil, nil, nil return nil, nil, nil, nil
} }
connCtx, cancelConnCtx = context.WithCancel(context.Background()) connCtx, cancelConnCtx = context.WithCancel(context.Background())
@ -356,9 +354,8 @@ func (mgr *tcpResolverConnMgr) establishConnection() (
// Log that a connection to the resolver was established. // Log that a connection to the resolver was established.
log.Debugf( log.Debugf(
"resolver: connected to %s (%s) with %d queries waiting", "resolver: connected to %s with %d queries waiting",
mgr.tr.resolver.GetName(), mgr.tr.resolver.Info.DescriptiveName(),
conn.RemoteAddr(),
waitingQueries, waitingQueries,
) )
@ -434,7 +431,7 @@ func (mgr *tcpResolverConnMgr) queryHandler( //nolint:golint // context.Context
activeQueries := len(mgr.tr.inFlightQueries) activeQueries := len(mgr.tr.inFlightQueries)
mgr.tr.Unlock() mgr.tr.Unlock()
if activeQueries == 0 { if activeQueries == 0 {
log.Debugf("resolver: recycling conn to %s (%s)", mgr.tr.resolver.GetName(), conn.RemoteAddr()) log.Debugf("resolver: recycling conn to %s", mgr.tr.resolver.Info.DescriptiveName())
return true return true
} }
} }
@ -454,9 +451,8 @@ func (mgr *tcpResolverConnMgr) handleQueryResponse(conn *dns.Conn, msg *dns.Msg)
if !ok { if !ok {
log.Debugf( log.Debugf(
"resolver: received possibly unsolicited reply from %s (%s): txid=%d q=%+v", "resolver: received possibly unsolicited reply from %s: txid=%d q=%+v",
mgr.tr.resolver.GetName(), mgr.tr.resolver.Info.DescriptiveName(),
conn.RemoteAddr(),
msg.Id, msg.Id,
msg.Question, msg.Question,
) )
@ -519,24 +515,21 @@ func (mgr *tcpResolverConnMgr) logConnectionError(err error, conn *dns.Conn, con
switch { switch {
case errors.Is(err, io.EOF): case errors.Is(err, io.EOF):
log.Debugf( log.Debugf(
"resolver: connection to %s (%s) was closed with %d in-flight queries", "resolver: connection to %s was closed with %d in-flight queries",
mgr.tr.resolver.GetName(), mgr.tr.resolver.Info.DescriptiveName(),
conn.RemoteAddr(),
inFlightQueries, inFlightQueries,
) )
case reading: case reading:
log.Warningf( log.Warningf(
"resolver: read error from %s (%s) with %d in-flight queries: %s", "resolver: read error from %s with %d in-flight queries: %s",
mgr.tr.resolver.GetName(), mgr.tr.resolver.Info.DescriptiveName(),
conn.RemoteAddr(),
inFlightQueries, inFlightQueries,
err, err,
) )
default: default:
log.Warningf( log.Warningf(
"resolver: write error to %s (%s) with %d in-flight queries: %s", "resolver: write error to %s with %d in-flight queries: %s",
mgr.tr.resolver.GetName(), mgr.tr.resolver.Info.DescriptiveName(),
conn.RemoteAddr(),
inFlightQueries, inFlightQueries,
err, err,
) )

View file

@ -2,21 +2,24 @@ package resolver
import ( import (
"context" "context"
"fmt"
"net" "net"
"sync" "sync"
"time" "time"
"github.com/miekg/dns" "github.com/miekg/dns"
"github.com/safing/portmaster/netenv" "github.com/safing/portmaster/netenv"
"github.com/safing/portmaster/network/netutils"
) )
// DNS Resolver Attributes // DNS Resolver Attributes
const ( const (
ServerTypeDNS = "dns" ServerTypeDNS = "dns"
ServerTypeTCP = "tcp" ServerTypeTCP = "tcp"
ServerTypeDoT = "dot" ServerTypeDoT = "dot"
ServerTypeDoH = "doh" ServerTypeDoH = "doh"
ServerTypeEnv = "env" ServerTypeMDNS = "mdns"
ServerTypeEnv = "env"
ServerSourceConfigured = "config" ServerSourceConfigured = "config"
ServerSourceOperatingSystem = "system" ServerSourceOperatingSystem = "system"
@ -39,14 +42,13 @@ type Resolver struct {
// - `empty`: NXDomain result, but without any other record in any section // - `empty`: NXDomain result, but without any other record in any section
// - `refused`: Request was refused // - `refused`: Request was refused
// - `zeroip`: Answer only contains zeroip // - `zeroip`: Answer only contains zeroip
Server string ConfigURL string
// Source describes from where the resolver configuration originated. // Info holds the parsed configuration.
Source string Info *ResolverInfo
// Name is the name of the resolver as passed via // ServerAddress holds the resolver address for easier use.
// ?name=. ServerAddress string
Name string
// UpstreamBlockDetection defines the detection type // UpstreamBlockDetection defines the detection type
// to identifier upstream DNS query blocking. // to identifier upstream DNS query blocking.
@ -57,14 +59,6 @@ type Resolver struct {
// - disabled // - disabled
UpstreamBlockDetection string UpstreamBlockDetection string
// Parsed config
ServerType string
ServerAddress string
ServerIP net.IP
ServerIPScope int8
ServerPort uint16
ServerInfo string
// Special Options // Special Options
VerifyDomain string VerifyDomain string
Search []string Search []string
@ -73,25 +67,111 @@ type Resolver struct {
Conn ResolverConn `json:"-"` Conn ResolverConn `json:"-"`
} }
// ResolverInfo is a subset of resolver attributes that is attached to answers
// from that server in order to use it later for decision making. It must not
// be changed by anyone after creation and initialization is complete.
type ResolverInfo struct {
// Name describes the name given to the resolver. The name is configured in the config URL using the name parameter.
Name string
// Type describes the type of the resolver.
// Possible values include dns, tcp, dot, doh, mdns, env.
Type string
// Source describes where the resolver configuration came from.
// Possible values include config, system, mdns, env.
Source string
// IP is the IP address of the resolver
IP net.IP
// IPScope is the network scope of the IP address.
IPScope netutils.IPScope
// Port is the udp/tcp port of the resolver.
Port uint16
// id holds a unique ID for this resolver.
id string
idGen sync.Once
}
// ID returns the unique ID of the resolver.
func (info *ResolverInfo) ID() string {
// Generate the ID the first time.
info.idGen.Do(func() {
switch info.Type {
case ServerTypeMDNS:
info.id = ServerTypeMDNS
case ServerTypeEnv:
info.id = ServerTypeEnv
default:
info.id = fmt.Sprintf(
"%s://%s:%d#%s",
info.Type,
info.IP,
info.Port,
info.Source,
)
}
})
return info.id
}
// DescriptiveName returns a human readable, but also detailed representation
// of the resolver.
func (info *ResolverInfo) DescriptiveName() string {
switch {
case info.Type == ServerTypeMDNS:
return "MDNS"
case info.Type == ServerTypeEnv:
return "Portmaster Environment"
case info.Name != "":
return fmt.Sprintf(
"%s (%s)",
info.Name,
info.ID(),
)
default:
return fmt.Sprintf(
"%s (%s)",
info.IP.String(),
info.ID(),
)
}
}
// Copy returns a full copy of the ResolverInfo.
func (info *ResolverInfo) Copy() *ResolverInfo {
// Force idGen to run before we copy.
_ = info.ID()
// Copy manually in order to not copy the mutex.
cp := &ResolverInfo{
Name: info.Name,
Type: info.Type,
Source: info.Source,
IP: info.IP,
IPScope: info.IPScope,
Port: info.Port,
id: info.id,
}
// Trigger idGen.Do(), as the ID is already generated.
cp.idGen.Do(func() {})
return cp
}
// IsBlockedUpstream returns true if the request has been blocked // IsBlockedUpstream returns true if the request has been blocked
// upstream. // upstream.
func (resolver *Resolver) IsBlockedUpstream(answer *dns.Msg) bool { func (resolver *Resolver) IsBlockedUpstream(answer *dns.Msg) bool {
return isBlockedUpstream(resolver, answer) return isBlockedUpstream(resolver, answer)
} }
// GetName returns the name of the server. If no name
// is configured the server address is returned.
func (resolver *Resolver) GetName() string {
if resolver.Name != "" {
return resolver.Name
}
return resolver.Server
}
// String returns the URL representation of the resolver. // String returns the URL representation of the resolver.
func (resolver *Resolver) String() string { func (resolver *Resolver) String() string {
return resolver.GetName() return resolver.Info.DescriptiveName()
} }
// ResolverConn is an interface to implement different types of query backends. // ResolverConn is an interface to implement different types of query backends.

View file

@ -52,7 +52,7 @@ func TestSingleResolving(t *testing.T) {
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
t.Logf("running bulk query test with resolver %s", resolver.Server) t.Logf("running bulk query test with resolver %s", resolver.Info.DescriptiveName())
started := time.Now() started := time.Now()
@ -83,7 +83,7 @@ func TestBulkResolving(t *testing.T) {
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
t.Logf("running bulk query test with resolver %s", resolver.Server) t.Logf("running bulk query test with resolver %s", resolver.Info.DescriptiveName())
started := time.Now() started := time.Now()

View file

@ -5,6 +5,7 @@ import (
"net" "net"
"net/url" "net/url"
"sort" "sort"
"strconv"
"strings" "strings"
"sync" "sync"
@ -61,7 +62,7 @@ func formatIPAndPort(ip net.IP, port uint16) string {
} }
func resolverConnFactory(resolver *Resolver) ResolverConn { func resolverConnFactory(resolver *Resolver) ResolverConn {
switch resolver.ServerType { switch resolver.Info.Type {
case ServerTypeTCP: case ServerTypeTCP:
return NewTCPResolver(resolver) return NewTCPResolver(resolver)
case ServerTypeDoT: case ServerTypeDoT:
@ -82,26 +83,36 @@ func createResolver(resolverURL, source string) (*Resolver, bool, error) {
switch u.Scheme { switch u.Scheme {
case ServerTypeDNS, ServerTypeDoT, ServerTypeTCP: case ServerTypeDNS, ServerTypeDoT, ServerTypeTCP:
default: default:
return nil, false, fmt.Errorf("invalid DNS resolver scheme %q", u.Scheme) return nil, false, fmt.Errorf("DNS resolver scheme %q invalid", u.Scheme)
} }
ip := net.ParseIP(u.Hostname()) ip := net.ParseIP(u.Hostname())
if ip == nil { if ip == nil {
return nil, false, fmt.Errorf("invalid resolver IP") return nil, false, fmt.Errorf("resolver IP %q invalid", u.Hostname())
} }
// Add default port for scheme if it is missing. // Add default port for scheme if it is missing.
if u.Port() == "" { var port uint16
switch u.Scheme { hostPort := u.Port()
case ServerTypeDNS, ServerTypeTCP: switch {
u.Host += ":53" case hostPort != "":
case ServerTypeDoT: parsedPort, err := strconv.ParseUint(hostPort, 10, 16)
u.Host += ":853" if err != nil {
return nil, false, fmt.Errorf("resolver port %q invalid", u.Port())
} }
port = uint16(parsedPort)
case u.Scheme == ServerTypeDNS, u.Scheme == ServerTypeTCP:
port = 53
case u.Scheme == ServerTypeDoH:
port = 443
case u.Scheme == ServerTypeDoT:
port = 853
default:
return nil, false, fmt.Errorf("missing port in %q", u.Host)
} }
scope := netutils.ClassifyIP(ip) scope := netutils.GetIPScope(ip)
if scope == netutils.HostLocal { if scope.IsLocalhost() {
return nil, true, nil // skip return nil, true, nil // skip
} }
@ -127,24 +138,20 @@ func createResolver(resolverURL, source string) (*Resolver, bool, error) {
} }
new := &Resolver{ new := &Resolver{
Server: resolverURL, ConfigURL: resolverURL,
ServerType: u.Scheme, Info: &ResolverInfo{
ServerAddress: u.Host, Name: query.Get("name"),
ServerIP: ip, Type: u.Scheme,
ServerIPScope: scope, Source: source,
Source: source, IP: ip,
IPScope: scope,
Port: port,
},
ServerAddress: net.JoinHostPort(ip.String(), strconv.Itoa(int(port))),
VerifyDomain: verifyDomain, VerifyDomain: verifyDomain,
Name: query.Get("name"),
UpstreamBlockDetection: blockType, UpstreamBlockDetection: blockType,
} }
u.RawQuery = "" // Remove options from parsed URL
if new.Name != "" {
new.ServerInfo = fmt.Sprintf("%s (%s, from %s)", new.Name, u, source)
} else {
new.ServerInfo = fmt.Sprintf("%s (from %s)", u, source)
}
new.Conn = resolverConnFactory(new) new.Conn = resolverConnFactory(new)
return new, false, nil return new, false, nil
} }
@ -195,7 +202,7 @@ func getSystemResolvers() (resolvers []*Resolver) {
continue continue
} }
if netutils.IPIsLAN(nameserver.IP) { if resolver.Info.IPScope.IsLAN() {
configureSearchDomains(resolver, nameserver.Search) configureSearchDomains(resolver, nameserver.Search)
} }
@ -244,16 +251,16 @@ func loadResolvers() {
activeResolvers = make(map[string]*Resolver) activeResolvers = make(map[string]*Resolver)
// add // add
for _, resolver := range newResolvers { for _, resolver := range newResolvers {
activeResolvers[resolver.Server] = resolver activeResolvers[resolver.Info.ID()] = resolver
} }
activeResolvers[mDNSResolver.Server] = mDNSResolver activeResolvers[mDNSResolver.Info.ID()] = mDNSResolver
activeResolvers[envResolver.Server] = envResolver activeResolvers[envResolver.Info.ID()] = envResolver
// log global resolvers // log global resolvers
if len(globalResolvers) > 0 { if len(globalResolvers) > 0 {
log.Trace("resolver: loaded global resolvers:") log.Trace("resolver: loaded global resolvers:")
for _, resolver := range globalResolvers { for _, resolver := range globalResolvers {
log.Tracef("resolver: %s", resolver.Server) log.Tracef("resolver: %s", resolver.ConfigURL)
} }
} else { } else {
log.Warning("resolver: no global resolvers loaded") log.Warning("resolver: no global resolvers loaded")
@ -263,7 +270,7 @@ func loadResolvers() {
if len(localResolvers) > 0 { if len(localResolvers) > 0 {
log.Trace("resolver: loaded local resolvers:") log.Trace("resolver: loaded local resolvers:")
for _, resolver := range localResolvers { for _, resolver := range localResolvers {
log.Tracef("resolver: %s", resolver.Server) log.Tracef("resolver: %s", resolver.ConfigURL)
} }
} else { } else {
log.Info("resolver: no local resolvers loaded") log.Info("resolver: no local resolvers loaded")
@ -273,7 +280,7 @@ func loadResolvers() {
if len(systemResolvers) > 0 { if len(systemResolvers) > 0 {
log.Trace("resolver: loaded system/network-assigned resolvers:") log.Trace("resolver: loaded system/network-assigned resolvers:")
for _, resolver := range systemResolvers { for _, resolver := range systemResolvers {
log.Tracef("resolver: %s", resolver.Server) log.Tracef("resolver: %s", resolver.ConfigURL)
} }
} else { } else {
log.Info("resolver: no system/network-assigned resolvers loaded") log.Info("resolver: no system/network-assigned resolvers loaded")
@ -285,7 +292,7 @@ func loadResolvers() {
for _, scope := range localScopes { for _, scope := range localScopes {
var scopeServers []string var scopeServers []string
for _, resolver := range scope.Resolvers { for _, resolver := range scope.Resolvers {
scopeServers = append(scopeServers, resolver.Server) scopeServers = append(scopeServers, resolver.ConfigURL)
} }
log.Tracef("resolver: %s: %s", scope.Domain, strings.Join(scopeServers, ", ")) log.Tracef("resolver: %s: %s", scope.Domain, strings.Join(scopeServers, ", "))
} }
@ -306,11 +313,11 @@ func setScopedResolvers(resolvers []*Resolver) {
localScopes = make([]*Scope, 0) localScopes = make([]*Scope, 0)
for _, resolver := range resolvers { for _, resolver := range resolvers {
if resolver.ServerIP != nil && netutils.IPIsLAN(resolver.ServerIP) { if resolver.Info.IPScope.IsLAN() {
localResolvers = append(localResolvers, resolver) localResolvers = append(localResolvers, resolver)
} }
if resolver.Source == ServerSourceOperatingSystem { if resolver.Info.Source == ServerSourceOperatingSystem {
systemResolvers = append(systemResolvers, resolver) systemResolvers = append(systemResolvers, resolver)
} }

View file

@ -29,10 +29,8 @@ type RRCache struct {
Extra []dns.RR Extra []dns.RR
Expires int64 Expires int64
// Source Information // Resolver Information
Server string Resolver *ResolverInfo
ServerScope int8
ServerInfo string
// Metadata about the request and handling // Metadata about the request and handling
ServedFromCache bool ServedFromCache bool
@ -133,13 +131,11 @@ func (rrCache *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 (rrCache *RRCache) ToNameRecord() *NameRecord { func (rrCache *RRCache) ToNameRecord() *NameRecord {
new := &NameRecord{ new := &NameRecord{
Domain: rrCache.Domain, Domain: rrCache.Domain,
Question: rrCache.Question.String(), Question: rrCache.Question.String(),
RCode: rrCache.RCode, RCode: rrCache.RCode,
Expires: rrCache.Expires, Expires: rrCache.Expires,
Server: rrCache.Server, Resolver: rrCache.Resolver,
ServerScope: rrCache.ServerScope,
ServerInfo: rrCache.ServerInfo,
} }
// stringify RR entries // stringify RR entries
@ -204,9 +200,7 @@ func GetRRCache(domain string, question dns.Type) (*RRCache, error) {
rrCache.Extra = parseRR(rrCache.Extra, entry) rrCache.Extra = parseRR(rrCache.Extra, entry)
} }
rrCache.Server = nameRecord.Server rrCache.Resolver = nameRecord.Resolver
rrCache.ServerScope = nameRecord.ServerScope
rrCache.ServerInfo = nameRecord.ServerInfo
rrCache.ServedFromCache = true rrCache.ServedFromCache = true
rrCache.Modified = nameRecord.Meta().Modified rrCache.Modified = nameRecord.Meta().Modified
return rrCache, nil return rrCache, nil
@ -259,9 +253,7 @@ func (rrCache *RRCache) ShallowCopy() *RRCache {
Extra: rrCache.Extra, Extra: rrCache.Extra,
Expires: rrCache.Expires, Expires: rrCache.Expires,
Server: rrCache.Server, Resolver: rrCache.Resolver,
ServerScope: rrCache.ServerScope,
ServerInfo: rrCache.ServerInfo,
ServedFromCache: rrCache.ServedFromCache, ServedFromCache: rrCache.ServedFromCache,
RequestingNew: rrCache.RequestingNew, RequestingNew: rrCache.RequestingNew,
@ -302,9 +294,9 @@ func (rrCache *RRCache) ReplyWithDNS(ctx context.Context, request *dns.Msg) *dns
func (rrCache *RRCache) GetExtraRRs(ctx context.Context, query *dns.Msg) (extra []dns.RR) { func (rrCache *RRCache) GetExtraRRs(ctx context.Context, query *dns.Msg) (extra []dns.RR) {
// Add cache status and source of data. // Add cache status and source of data.
if rrCache.ServedFromCache { if rrCache.ServedFromCache {
extra = addExtra(ctx, extra, "served from cache, resolved by "+rrCache.ServerInfo) extra = addExtra(ctx, extra, "served from cache, resolved by "+rrCache.Resolver.DescriptiveName())
} else { } else {
extra = addExtra(ctx, extra, "freshly resolved by "+rrCache.ServerInfo) extra = addExtra(ctx, extra, "freshly resolved by "+rrCache.Resolver.DescriptiveName())
} }
// Add expiry and cache information. // Add expiry and cache information.

View file

@ -158,13 +158,13 @@ addNextResolver:
for _, resolver := range addResolvers { for _, resolver := range addResolvers {
// check for compliance // check for compliance
if err := resolver.checkCompliance(ctx, q); err != nil { if err := resolver.checkCompliance(ctx, q); err != nil {
log.Tracer(ctx).Tracef("skipping non-compliant resolver %s: %s", resolver.GetName(), err) log.Tracer(ctx).Tracef("skipping non-compliant resolver %s: %s", resolver.Info.DescriptiveName(), err)
continue continue
} }
// deduplicate // deduplicate
for _, selectedResolver := range selected { for _, selectedResolver := range selected {
if selectedResolver.Server == resolver.Server { if selectedResolver.Info.ID() == resolver.Info.ID() {
continue addNextResolver continue addNextResolver
} }
} }
@ -208,7 +208,7 @@ func (q *Query) checkCompliance() error {
func (resolver *Resolver) checkCompliance(_ context.Context, q *Query) error { func (resolver *Resolver) checkCompliance(_ context.Context, q *Query) error {
if noInsecureProtocols(q.SecurityLevel) { if noInsecureProtocols(q.SecurityLevel) {
switch resolver.ServerType { switch resolver.Info.Type {
case ServerTypeDNS: case ServerTypeDNS:
return errInsecureProtocol return errInsecureProtocol
case ServerTypeTCP: case ServerTypeTCP:
@ -218,20 +218,20 @@ func (resolver *Resolver) checkCompliance(_ context.Context, q *Query) error {
case ServerTypeDoH: case ServerTypeDoH:
// compliant // compliant
case ServerTypeEnv: case ServerTypeEnv:
// compliant (data is sources from local network only and is highly limited) // compliant (data is sourced from local network only and is highly limited)
default: default:
return errInsecureProtocol return errInsecureProtocol
} }
} }
if noAssignedNameservers(q.SecurityLevel) { if noAssignedNameservers(q.SecurityLevel) {
if resolver.Source == ServerSourceOperatingSystem { if resolver.Info.Source == ServerSourceOperatingSystem {
return errAssignedServer return errAssignedServer
} }
} }
if noMulticastDNS(q.SecurityLevel) { if noMulticastDNS(q.SecurityLevel) {
if resolver.Source == ServerSourceMDNS { if resolver.Info.Source == ServerSourceMDNS {
return errMulticastDNS return errMulticastDNS
} }
} }