From 20383226f83b4c2e4e9ae29de5aa0d0e36d5a45c Mon Sep 17 00:00:00 2001 From: Daniel Date: Sat, 20 Mar 2021 22:19:27 +0100 Subject: [PATCH] Move some Resolver information to ResolverInfo and propagate it --- network/connection.go | 6 ++ resolver/ipinfo.go | 4 ++ resolver/namerecord.go | 31 ++++++-- resolver/resolve.go | 8 +-- resolver/resolver-env.go | 27 ++++--- resolver/resolver-mdns.go | 45 ++++++------ resolver/resolver-plain.go | 18 +++-- resolver/resolver-tcp.go | 53 ++++++-------- resolver/resolver.go | 140 +++++++++++++++++++++++++++++-------- resolver/resolver_test.go | 4 +- resolver/resolvers.go | 77 ++++++++++---------- resolver/rrcache.go | 30 +++----- resolver/scopes.go | 12 ++-- 13 files changed, 275 insertions(+), 180 deletions(-) diff --git a/network/connection.go b/network/connection.go index 5ae6bd72..6967c2e2 100644 --- a/network/connection.go +++ b/network/connection.go @@ -85,6 +85,9 @@ type Connection struct { //nolint:maligned // TODO: fix alignment // be added to it during the livetime of a connection. Access to // entity must be guarded by the connection lock. 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. // The verdict may change so any access to it must be guarded by the // connection lock. @@ -320,6 +323,7 @@ func NewConnectionFromFirstPacket(pkt packet.Packet) *Connection { scope = lastResolvedDomain.Domain entity.Domain = lastResolvedDomain.Domain entity.CNAME = lastResolvedDomain.CNAMEs + resolverInfo = lastResolvedDomain.Resolver removeOpenDNSRequest(proc.Pid, lastResolvedDomain.Domain) } } @@ -364,6 +368,8 @@ func NewConnectionFromFirstPacket(pkt packet.Packet) *Connection { process: proc, // remote endpoint Entity: entity, + // resolver used to resolve dns request + Resolver: resolverInfo, // meta Started: time.Now().Unix(), ProfileRevisionCounter: proc.Profile().RevisionCnt(), diff --git a/resolver/ipinfo.go b/resolver/ipinfo.go index 7217e306..7c5f3d5f 100644 --- a/resolver/ipinfo.go +++ b/resolver/ipinfo.go @@ -40,6 +40,10 @@ type ResolvedDomain struct { // Domain. CNAMEs []string + // Resolver holds basic information about the resolver that provided this + // information. + Resolver *ResolverInfo + // Expires holds the timestamp when this entry expires. // 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. diff --git a/resolver/namerecord.go b/resolver/namerecord.go index 42a33323..68d6055f 100644 --- a/resolver/namerecord.go +++ b/resolver/namerecord.go @@ -49,9 +49,20 @@ type NameRecord struct { Extra []string Expires int64 - Server string - ServerScope int8 - ServerInfo string + Resolver *ResolverInfo +} + +// 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 { @@ -67,7 +78,7 @@ func GetNameRecord(domain, question string) (*NameRecord, error) { return nil, err } - // unwrap + // Unwrap record if it's wrapped. if r.IsWrapped() { // only allocate a new struct, if we need it new := &NameRecord{} @@ -75,14 +86,24 @@ func GetNameRecord(domain, question string) (*NameRecord, error) { if err != nil { return nil, err } + // Check if the record is valid. + if !new.IsValid() { + return nil, errors.New("record is invalid (outdated format)") + } + return new, nil } - // or adjust type + // Or just adjust the type. new, ok := r.(*NameRecord) if !ok { 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 } diff --git a/resolver/resolve.go b/resolver/resolve.go index 11ed1590..31ceeb74 100644 --- a/resolver/resolve.go +++ b/resolver/resolve.go @@ -175,9 +175,9 @@ func checkCache(ctx context.Context, q *Query) *RRCache { } // Get the resolver that the rrCache was resolved with. - resolver := getActiveResolverByIDWithLocking(rrCache.Server) + resolver := getActiveResolverByIDWithLocking(rrCache.Resolver.ID()) 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 } @@ -361,11 +361,11 @@ resolveLoop: continue case errors.Is(err, ErrTimeout): 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 default: 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 } } diff --git a/resolver/resolver-env.go b/resolver/resolver-env.go index 2d4ad230..30786063 100644 --- a/resolver/resolver-env.go +++ b/resolver/resolver-env.go @@ -20,12 +20,13 @@ const ( var ( envResolver = &Resolver{ - Server: ServerSourceEnv, - ServerType: ServerTypeEnv, - ServerIPScope: netutils.SiteLocal, - ServerInfo: "Portmaster environment", - Source: ServerSourceEnv, - Conn: &envResolverConn{}, + ConfigURL: ServerSourceEnv, + Info: &ResolverInfo{ + Type: ServerTypeEnv, + Source: ServerSourceEnv, + IPScope: netutils.SiteLocal, + }, + Conn: &envResolverConn{}, } envResolvers = []*Resolver{envResolver} @@ -109,14 +110,12 @@ func (er *envResolverConn) makeRRCache(q *Query, answers []dns.RR) *RRCache { q.NoCaching = true return &RRCache{ - Domain: q.FQDN, - Question: q.QType, - RCode: dns.RcodeSuccess, - Answer: answers, - Extra: []dns.RR{internalSpecialUseComment}, // Always add comment about this TLD. - Server: envResolver.Server, - ServerScope: envResolver.ServerIPScope, - ServerInfo: envResolver.ServerInfo, + Domain: q.FQDN, + Question: q.QType, + RCode: dns.RcodeSuccess, + Answer: answers, + Extra: []dns.RR{internalSpecialUseComment}, // Always add comment about this TLD. + Resolver: envResolver.Info.Copy(), } } diff --git a/resolver/resolver-mdns.go b/resolver/resolver-mdns.go index 166cebfb..cd28eb5f 100644 --- a/resolver/resolver-mdns.go +++ b/resolver/resolver-mdns.go @@ -31,12 +31,13 @@ var ( questionsLock sync.Mutex mDNSResolver = &Resolver{ - Server: ServerSourceMDNS, - ServerType: ServerTypeDNS, - ServerIPScope: netutils.SiteLocal, - ServerInfo: "mDNS resolver", - Source: ServerSourceMDNS, - Conn: &mDNSResolverConn{}, + ConfigURL: ServerSourceMDNS, + Info: &ResolverInfo{ + Type: ServerTypeMDNS, + Source: ServerSourceMDNS, + IPScope: netutils.SiteLocal, + }, + Conn: &mDNSResolverConn{}, } mDNSResolvers = []*Resolver{mDNSResolver} ) @@ -200,12 +201,10 @@ func handleMDNSMessages(ctx context.Context, messages chan *dns.Msg) error { // create new and do not append if err != nil || rrCache.Modified < time.Now().Add(-2*time.Second).Unix() || rrCache.Expired() { rrCache = &RRCache{ - Domain: question.Name, - Question: dns.Type(question.Qtype), - RCode: dns.RcodeSuccess, - Server: mDNSResolver.Server, - ServerScope: mDNSResolver.ServerIPScope, - ServerInfo: mDNSResolver.ServerInfo, + Domain: question.Name, + Question: dns.Type(question.Qtype), + RCode: dns.RcodeSuccess, + Resolver: mDNSResolver.Info.Copy(), } } } @@ -302,13 +301,11 @@ func handleMDNSMessages(ctx context.Context, messages chan *dns.Msg) error { continue } rrCache = &RRCache{ - Domain: v.Header().Name, - Question: dns.Type(v.Header().Class), - RCode: dns.RcodeSuccess, - Answer: []dns.RR{v}, - Server: mDNSResolver.Server, - ServerScope: mDNSResolver.ServerIPScope, - ServerInfo: mDNSResolver.ServerInfo, + Domain: v.Header().Name, + Question: dns.Type(v.Header().Class), + RCode: dns.RcodeSuccess, + Answer: []dns.RR{v}, + Resolver: mDNSResolver.Info.Copy(), } rrCache.Clean(minMDnsTTL) err := rrCache.Save() @@ -423,12 +420,10 @@ func queryMulticastDNS(ctx context.Context, q *Query) (*RRCache, error) { // Respond with NXDomain. return &RRCache{ - Domain: q.FQDN, - Question: q.QType, - RCode: dns.RcodeNameError, - Server: mDNSResolver.Server, - ServerScope: mDNSResolver.ServerIPScope, - ServerInfo: mDNSResolver.ServerInfo, + Domain: q.FQDN, + Question: q.QType, + RCode: dns.RcodeNameError, + Resolver: mDNSResolver.Info.Copy(), }, nil } diff --git a/resolver/resolver-plain.go b/resolver/resolver-plain.go index 3892ab91..01862a7e 100644 --- a/resolver/resolver-plain.go +++ b/resolver/resolver-plain.go @@ -72,22 +72,20 @@ func (pr *PlainResolver) Query(ctx context.Context, q *Query) (*RRCache, error) // check if blocked if pr.resolver.IsBlockedUpstream(reply) { - return nil, &BlockedUpstreamError{pr.resolver.GetName()} + return nil, &BlockedUpstreamError{pr.resolver.Info.DescriptiveName()} } // hint network environment at successful connection netenv.ReportSuccessfulConnection() newRecord := &RRCache{ - Domain: q.FQDN, - Question: q.QType, - RCode: reply.Rcode, - Answer: reply.Answer, - Ns: reply.Ns, - Extra: reply.Extra, - Server: pr.resolver.Server, - ServerScope: pr.resolver.ServerIPScope, - ServerInfo: pr.resolver.ServerInfo, + Domain: q.FQDN, + Question: q.QType, + RCode: reply.Rcode, + Answer: reply.Answer, + Ns: reply.Ns, + Extra: reply.Extra, + Resolver: pr.resolver.Info.Copy(), } // TODO: check if reply.Answer is valid diff --git a/resolver/resolver-tcp.go b/resolver/resolver-tcp.go index a5e1ed30..49680516 100644 --- a/resolver/resolver-tcp.go +++ b/resolver/resolver-tcp.go @@ -49,15 +49,13 @@ type InFlightQuery struct { // MakeCacheRecord creates an RCache record from a reply. func (ifq *InFlightQuery) MakeCacheRecord(reply *dns.Msg) *RRCache { return &RRCache{ - Domain: ifq.Query.FQDN, - Question: ifq.Query.QType, - RCode: reply.Rcode, - Answer: reply.Answer, - Ns: reply.Ns, - Extra: reply.Extra, - Server: ifq.Resolver.Server, - ServerScope: ifq.Resolver.ServerIPScope, - ServerInfo: ifq.Resolver.ServerInfo, + Domain: ifq.Query.FQDN, + Question: ifq.Query.QType, + RCode: reply.Rcode, + Answer: reply.Answer, + Ns: reply.Ns, + Extra: reply.Extra, + Resolver: ifq.Resolver.Info.Copy(), } } @@ -172,7 +170,7 @@ func (tr *TCPResolver) Query(ctx context.Context, q *Query) (*RRCache, error) { } if tr.resolver.IsBlockedUpstream(reply) { - return nil, &BlockedUpstreamError{tr.resolver.GetName()} + return nil, &BlockedUpstreamError{tr.resolver.Info.DescriptiveName()} } return inFlight.MakeCacheRecord(reply), nil @@ -189,7 +187,7 @@ func (tr *TCPResolver) checkClientStatus() { select { case tr.clientHeartbeat <- struct{}{}: 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() } } @@ -299,7 +297,7 @@ func (mgr *tcpResolverConnMgr) waitForWork(clientCtx context.Context) (proceed b select { case mgr.tr.queries <- inFlight.Msg: 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. @@ -317,7 +315,7 @@ func (mgr *tcpResolverConnMgr) waitForWork(clientCtx context.Context) (proceed b select { case mgr.tr.queries <- msg: 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 }) @@ -343,7 +341,7 @@ func (mgr *tcpResolverConnMgr) establishConnection() ( var err error conn, err = mgr.tr.dnsClient.Dial(mgr.tr.resolver.ServerAddress) 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 } connCtx, cancelConnCtx = context.WithCancel(context.Background()) @@ -356,9 +354,8 @@ func (mgr *tcpResolverConnMgr) establishConnection() ( // Log that a connection to the resolver was established. log.Debugf( - "resolver: connected to %s (%s) with %d queries waiting", - mgr.tr.resolver.GetName(), - conn.RemoteAddr(), + "resolver: connected to %s with %d queries waiting", + mgr.tr.resolver.Info.DescriptiveName(), waitingQueries, ) @@ -434,7 +431,7 @@ func (mgr *tcpResolverConnMgr) queryHandler( //nolint:golint // context.Context activeQueries := len(mgr.tr.inFlightQueries) mgr.tr.Unlock() 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 } } @@ -454,9 +451,8 @@ func (mgr *tcpResolverConnMgr) handleQueryResponse(conn *dns.Conn, msg *dns.Msg) if !ok { log.Debugf( - "resolver: received possibly unsolicited reply from %s (%s): txid=%d q=%+v", - mgr.tr.resolver.GetName(), - conn.RemoteAddr(), + "resolver: received possibly unsolicited reply from %s: txid=%d q=%+v", + mgr.tr.resolver.Info.DescriptiveName(), msg.Id, msg.Question, ) @@ -519,24 +515,21 @@ func (mgr *tcpResolverConnMgr) logConnectionError(err error, conn *dns.Conn, con switch { case errors.Is(err, io.EOF): log.Debugf( - "resolver: connection to %s (%s) was closed with %d in-flight queries", - mgr.tr.resolver.GetName(), - conn.RemoteAddr(), + "resolver: connection to %s was closed with %d in-flight queries", + mgr.tr.resolver.Info.DescriptiveName(), inFlightQueries, ) case reading: log.Warningf( - "resolver: read error from %s (%s) with %d in-flight queries: %s", - mgr.tr.resolver.GetName(), - conn.RemoteAddr(), + "resolver: read error from %s with %d in-flight queries: %s", + mgr.tr.resolver.Info.DescriptiveName(), inFlightQueries, err, ) default: log.Warningf( - "resolver: write error to %s (%s) with %d in-flight queries: %s", - mgr.tr.resolver.GetName(), - conn.RemoteAddr(), + "resolver: write error to %s with %d in-flight queries: %s", + mgr.tr.resolver.Info.DescriptiveName(), inFlightQueries, err, ) diff --git a/resolver/resolver.go b/resolver/resolver.go index ad7f5741..8b523148 100644 --- a/resolver/resolver.go +++ b/resolver/resolver.go @@ -2,21 +2,24 @@ package resolver import ( "context" + "fmt" "net" "sync" "time" "github.com/miekg/dns" "github.com/safing/portmaster/netenv" + "github.com/safing/portmaster/network/netutils" ) // DNS Resolver Attributes const ( - ServerTypeDNS = "dns" - ServerTypeTCP = "tcp" - ServerTypeDoT = "dot" - ServerTypeDoH = "doh" - ServerTypeEnv = "env" + ServerTypeDNS = "dns" + ServerTypeTCP = "tcp" + ServerTypeDoT = "dot" + ServerTypeDoH = "doh" + ServerTypeMDNS = "mdns" + ServerTypeEnv = "env" ServerSourceConfigured = "config" ServerSourceOperatingSystem = "system" @@ -39,14 +42,13 @@ type Resolver struct { // - `empty`: NXDomain result, but without any other record in any section // - `refused`: Request was refused // - `zeroip`: Answer only contains zeroip - Server string + ConfigURL string - // Source describes from where the resolver configuration originated. - Source string + // Info holds the parsed configuration. + Info *ResolverInfo - // Name is the name of the resolver as passed via - // ?name=. - Name string + // ServerAddress holds the resolver address for easier use. + ServerAddress string // UpstreamBlockDetection defines the detection type // to identifier upstream DNS query blocking. @@ -57,14 +59,6 @@ type Resolver struct { // - disabled UpstreamBlockDetection string - // Parsed config - ServerType string - ServerAddress string - ServerIP net.IP - ServerIPScope int8 - ServerPort uint16 - ServerInfo string - // Special Options VerifyDomain string Search []string @@ -73,25 +67,111 @@ type Resolver struct { 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 // upstream. func (resolver *Resolver) IsBlockedUpstream(answer *dns.Msg) bool { 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. func (resolver *Resolver) String() string { - return resolver.GetName() + return resolver.Info.DescriptiveName() } // ResolverConn is an interface to implement different types of query backends. diff --git a/resolver/resolver_test.go b/resolver/resolver_test.go index d8ab14e4..5070a248 100644 --- a/resolver/resolver_test.go +++ b/resolver/resolver_test.go @@ -52,7 +52,7 @@ func TestSingleResolving(t *testing.T) { if err != nil { 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() @@ -83,7 +83,7 @@ func TestBulkResolving(t *testing.T) { if err != nil { 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() diff --git a/resolver/resolvers.go b/resolver/resolvers.go index 2baeb2de..40608faa 100644 --- a/resolver/resolvers.go +++ b/resolver/resolvers.go @@ -5,6 +5,7 @@ import ( "net" "net/url" "sort" + "strconv" "strings" "sync" @@ -61,7 +62,7 @@ func formatIPAndPort(ip net.IP, port uint16) string { } func resolverConnFactory(resolver *Resolver) ResolverConn { - switch resolver.ServerType { + switch resolver.Info.Type { case ServerTypeTCP: return NewTCPResolver(resolver) case ServerTypeDoT: @@ -82,26 +83,36 @@ func createResolver(resolverURL, source string) (*Resolver, bool, error) { switch u.Scheme { case ServerTypeDNS, ServerTypeDoT, ServerTypeTCP: 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()) 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. - if u.Port() == "" { - switch u.Scheme { - case ServerTypeDNS, ServerTypeTCP: - u.Host += ":53" - case ServerTypeDoT: - u.Host += ":853" + var port uint16 + hostPort := u.Port() + switch { + case hostPort != "": + parsedPort, err := strconv.ParseUint(hostPort, 10, 16) + 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) - if scope == netutils.HostLocal { + scope := netutils.GetIPScope(ip) + if scope.IsLocalhost() { return nil, true, nil // skip } @@ -127,24 +138,20 @@ func createResolver(resolverURL, source string) (*Resolver, bool, error) { } new := &Resolver{ - Server: resolverURL, - ServerType: u.Scheme, - ServerAddress: u.Host, - ServerIP: ip, - ServerIPScope: scope, - Source: source, + ConfigURL: resolverURL, + Info: &ResolverInfo{ + Name: query.Get("name"), + Type: u.Scheme, + Source: source, + IP: ip, + IPScope: scope, + Port: port, + }, + ServerAddress: net.JoinHostPort(ip.String(), strconv.Itoa(int(port))), VerifyDomain: verifyDomain, - Name: query.Get("name"), 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) return new, false, nil } @@ -195,7 +202,7 @@ func getSystemResolvers() (resolvers []*Resolver) { continue } - if netutils.IPIsLAN(nameserver.IP) { + if resolver.Info.IPScope.IsLAN() { configureSearchDomains(resolver, nameserver.Search) } @@ -244,16 +251,16 @@ func loadResolvers() { activeResolvers = make(map[string]*Resolver) // add for _, resolver := range newResolvers { - activeResolvers[resolver.Server] = resolver + activeResolvers[resolver.Info.ID()] = resolver } - activeResolvers[mDNSResolver.Server] = mDNSResolver - activeResolvers[envResolver.Server] = envResolver + activeResolvers[mDNSResolver.Info.ID()] = mDNSResolver + activeResolvers[envResolver.Info.ID()] = envResolver // log global resolvers if len(globalResolvers) > 0 { log.Trace("resolver: loaded global resolvers:") for _, resolver := range globalResolvers { - log.Tracef("resolver: %s", resolver.Server) + log.Tracef("resolver: %s", resolver.ConfigURL) } } else { log.Warning("resolver: no global resolvers loaded") @@ -263,7 +270,7 @@ func loadResolvers() { if len(localResolvers) > 0 { log.Trace("resolver: loaded local resolvers:") for _, resolver := range localResolvers { - log.Tracef("resolver: %s", resolver.Server) + log.Tracef("resolver: %s", resolver.ConfigURL) } } else { log.Info("resolver: no local resolvers loaded") @@ -273,7 +280,7 @@ func loadResolvers() { if len(systemResolvers) > 0 { log.Trace("resolver: loaded system/network-assigned resolvers:") for _, resolver := range systemResolvers { - log.Tracef("resolver: %s", resolver.Server) + log.Tracef("resolver: %s", resolver.ConfigURL) } } else { log.Info("resolver: no system/network-assigned resolvers loaded") @@ -285,7 +292,7 @@ func loadResolvers() { for _, scope := range localScopes { var scopeServers []string 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, ", ")) } @@ -306,11 +313,11 @@ func setScopedResolvers(resolvers []*Resolver) { localScopes = make([]*Scope, 0) for _, resolver := range resolvers { - if resolver.ServerIP != nil && netutils.IPIsLAN(resolver.ServerIP) { + if resolver.Info.IPScope.IsLAN() { localResolvers = append(localResolvers, resolver) } - if resolver.Source == ServerSourceOperatingSystem { + if resolver.Info.Source == ServerSourceOperatingSystem { systemResolvers = append(systemResolvers, resolver) } diff --git a/resolver/rrcache.go b/resolver/rrcache.go index 79118145..8c901131 100644 --- a/resolver/rrcache.go +++ b/resolver/rrcache.go @@ -29,10 +29,8 @@ type RRCache struct { Extra []dns.RR Expires int64 - // Source Information - Server string - ServerScope int8 - ServerInfo string + // Resolver Information + Resolver *ResolverInfo // Metadata about the request and handling ServedFromCache bool @@ -133,13 +131,11 @@ func (rrCache *RRCache) ExportAllARecords() (ips []net.IP) { // ToNameRecord converts the RRCache to a NameRecord for cleaner persistence. func (rrCache *RRCache) ToNameRecord() *NameRecord { new := &NameRecord{ - Domain: rrCache.Domain, - Question: rrCache.Question.String(), - RCode: rrCache.RCode, - Expires: rrCache.Expires, - Server: rrCache.Server, - ServerScope: rrCache.ServerScope, - ServerInfo: rrCache.ServerInfo, + Domain: rrCache.Domain, + Question: rrCache.Question.String(), + RCode: rrCache.RCode, + Expires: rrCache.Expires, + Resolver: rrCache.Resolver, } // stringify RR entries @@ -204,9 +200,7 @@ func GetRRCache(domain string, question dns.Type) (*RRCache, error) { rrCache.Extra = parseRR(rrCache.Extra, entry) } - rrCache.Server = nameRecord.Server - rrCache.ServerScope = nameRecord.ServerScope - rrCache.ServerInfo = nameRecord.ServerInfo + rrCache.Resolver = nameRecord.Resolver rrCache.ServedFromCache = true rrCache.Modified = nameRecord.Meta().Modified return rrCache, nil @@ -259,9 +253,7 @@ func (rrCache *RRCache) ShallowCopy() *RRCache { Extra: rrCache.Extra, Expires: rrCache.Expires, - Server: rrCache.Server, - ServerScope: rrCache.ServerScope, - ServerInfo: rrCache.ServerInfo, + Resolver: rrCache.Resolver, ServedFromCache: rrCache.ServedFromCache, 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) { // Add cache status and source of data. 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 { - extra = addExtra(ctx, extra, "freshly resolved by "+rrCache.ServerInfo) + extra = addExtra(ctx, extra, "freshly resolved by "+rrCache.Resolver.DescriptiveName()) } // Add expiry and cache information. diff --git a/resolver/scopes.go b/resolver/scopes.go index a7772186..54819358 100644 --- a/resolver/scopes.go +++ b/resolver/scopes.go @@ -158,13 +158,13 @@ addNextResolver: for _, resolver := range addResolvers { // check for compliance 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 } // deduplicate for _, selectedResolver := range selected { - if selectedResolver.Server == resolver.Server { + if selectedResolver.Info.ID() == resolver.Info.ID() { continue addNextResolver } } @@ -208,7 +208,7 @@ func (q *Query) checkCompliance() error { func (resolver *Resolver) checkCompliance(_ context.Context, q *Query) error { if noInsecureProtocols(q.SecurityLevel) { - switch resolver.ServerType { + switch resolver.Info.Type { case ServerTypeDNS: return errInsecureProtocol case ServerTypeTCP: @@ -218,20 +218,20 @@ func (resolver *Resolver) checkCompliance(_ context.Context, q *Query) error { case ServerTypeDoH: // compliant 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: return errInsecureProtocol } } if noAssignedNameservers(q.SecurityLevel) { - if resolver.Source == ServerSourceOperatingSystem { + if resolver.Info.Source == ServerSourceOperatingSystem { return errAssignedServer } } if noMulticastDNS(q.SecurityLevel) { - if resolver.Source == ServerSourceMDNS { + if resolver.Info.Source == ServerSourceMDNS { return errMulticastDNS } }