diff --git a/netenv/online-status.go b/netenv/online-status.go index 6778cdf3..72dd9076 100644 --- a/netenv/online-status.go +++ b/netenv/online-status.go @@ -41,7 +41,10 @@ var ( DNSTestDomain = "one.one.one.one." DNSTestExpectedIP = net.IPv4(1, 1, 1, 1) - SpecialCaptivePortalDomain = "captiveportal.local." + // SpecialCaptivePortalDomain is the domain name used to point to the detected captive portal IP + // or the captive portal test IP. The default value should be overridden by the resolver package, + // which defines the custom internal domain name to use. + SpecialCaptivePortalDomain = "captiveportal.invalid." ) var ( diff --git a/resolver/config.go b/resolver/config.go index 96334eb8..e2205728 100644 --- a/resolver/config.go +++ b/resolver/config.go @@ -72,10 +72,6 @@ var ( dontResolveSpecialDomains status.SecurityLevelOption cfgOptionDontResolveSpecialDomainsOrder = 16 - CfgOptionDontResolveTestDomainsKey = "dns/dontResolveTestDomains" - dontResolveTestDomains status.SecurityLevelOption - cfgOptionDontResolveTestDomainsOrder = 17 - CfgOptionNameserverRetryRateKey = "dns/nameserverRetryRate" nameserverRetryRate config.IntOption cfgOptionNameserverRetryRateOrder = 32 @@ -191,7 +187,7 @@ Parameters: err = config.Register(&config.Option{ Name: "Do not resolve special domains", Key: CfgOptionDontResolveSpecialDomainsKey, - Description: fmt.Sprintf("Do not resolve the special top level domains %s", formatScopeList(specialServiceScopes)), + Description: fmt.Sprintf("Do not resolve the special top level domains %s", formatScopeList(specialServiceDomains)), Order: cfgOptionDontResolveSpecialDomainsOrder, OptType: config.OptTypeInt, ExpertiseLevel: config.ExpertiseLevelExpert, @@ -205,23 +201,6 @@ Parameters: } dontResolveSpecialDomains = status.ConfigIsActiveConcurrent(CfgOptionDontResolveSpecialDomainsKey) - err = config.Register(&config.Option{ - Name: "Do not resolve test domains", - Key: CfgOptionDontResolveTestDomainsKey, - Description: fmt.Sprintf("Do not resolve the special testing top level domains %s", formatScopeList(localTestScopes)), - Order: cfgOptionDontResolveTestDomainsOrder, - OptType: config.OptTypeInt, - ExpertiseLevel: config.ExpertiseLevelExpert, - ReleaseLevel: config.ReleaseLevelStable, - ExternalOptType: "security level", - DefaultValue: status.SecurityLevelsHighAndExtreme, - ValidationRegex: "^(4|6|7)$", - }) - if err != nil { - return err - } - dontResolveTestDomains = status.ConfigIsActiveConcurrent(CfgOptionDontResolveTestDomainsKey) - return nil } diff --git a/resolver/resolve.go b/resolver/resolve.go index f4b05c15..6234a345 100644 --- a/resolver/resolve.go +++ b/resolver/resolve.go @@ -292,7 +292,7 @@ retry: func resolveAndCache(ctx context.Context, q *Query) (rrCache *RRCache, err error) { //nolint:gocognit // get resolvers - resolvers := GetResolversInScope(ctx, q) + resolvers, tryAll := GetResolversInScope(ctx, q) if len(resolvers) == 0 { return nil, ErrNoCompliance } @@ -330,6 +330,9 @@ resolveLoop: switch { case errors.Is(err, ErrNotFound): // NXDomain, or similar + if tryAll { + continue + } return nil, err case errors.Is(err, ErrBlocked): // some resolvers might also block diff --git a/resolver/resolver-env.go b/resolver/resolver-env.go index 9607f800..4a8caa31 100644 --- a/resolver/resolver-env.go +++ b/resolver/resolver-env.go @@ -2,6 +2,7 @@ package resolver import ( "context" + "fmt" "net" "github.com/miekg/dns" @@ -10,6 +11,13 @@ import ( "github.com/safing/portmaster/network/netutils" ) +const ( + internalSpecialUseDomain = "17.home.arpa." + + routerDomain = "router.local." + internalSpecialUseDomain + captivePortalDomain = "captiveportal.local." + internalSpecialUseDomain +) + var ( envResolver = &Resolver{ Server: ServerSourceEnv, @@ -18,67 +26,92 @@ var ( Source: ServerSourceEnv, Conn: &envResolverConn{}, } + envResolvers = []*Resolver{envResolver} - localSOA dns.RR + internalSpecialUseSOA dns.RR + internalSpecialUseComment dns.RR ) func prepEnvResolver() (err error) { - localSOA, err = dns.NewRR("local. 17 IN SOA localhost. none.localhost. 17 17 17 17 17") + netenv.SpecialCaptivePortalDomain = captivePortalDomain + + internalSpecialUseSOA, err = dns.NewRR(fmt.Sprintf( + "%s 17 IN SOA localhost. none.localhost. 0 0 0 0 0", + internalSpecialUseDomain, + )) + if err != nil { + return err + } + + internalSpecialUseComment, err = dns.NewRR(fmt.Sprintf( + `%s 17 IN TXT "This is a special use TLD of the Portmaster."`, + internalSpecialUseDomain, + )) return err } type envResolverConn struct{} func (er *envResolverConn) Query(ctx context.Context, q *Query) (*RRCache, error) { - // prepping - portal := netenv.GetCaptivePortal() + switch uint16(q.QType) { + case dns.TypeA, dns.TypeAAAA: // We respond with all IPv4/6 addresses we can find. + switch q.FQDN { + case captivePortalDomain: + // Get IP address of the captive portal. + portal := netenv.GetCaptivePortal() + portalIP := portal.IP + if portalIP == nil { + portalIP = netenv.PortalTestIP + } + // Convert IP to record and respond. + records, err := netutils.IPsToRRs(q.FQDN, []net.IP{portalIP}) + if err != nil { + log.Warningf("nameserver: failed to create captive portal response to %s: %s", q.FQDN, err) + return er.nxDomain(q), nil + } + return er.makeRRCache(q, records), nil - // check for matching name - switch q.FQDN { - case "local.": - // Firefox requests the SOA request for local. before resolving any local. domains. - // Others might be doing this too. We guessed this behaviour, weren't able to find docs. - if q.QType == dns.Type(dns.TypeSOA) { - return er.makeRRCache(q, []dns.RR{localSOA}), nil - } - return nil, ErrNotFound + case routerDomain: + // Get gateways from netenv system. + routers := netenv.Gateways() + if len(routers) == 0 { + return er.nxDomain(q), nil + } + // Convert IP to record and respond. + records, err := netutils.IPsToRRs(q.FQDN, routers) + if err != nil { + log.Warningf("nameserver: failed to create gateway response to %s: %s", q.FQDN, err) + return er.nxDomain(q), nil + } + return er.makeRRCache(q, records), nil - case netenv.SpecialCaptivePortalDomain: - portalIP := portal.IP - if portal.IP == nil { - portalIP = netenv.PortalTestIP } - - records, err := netutils.IPsToRRs(q.FQDN, []net.IP{portalIP}) - if err != nil { - log.Warningf("nameserver: failed to create captive portal response to %s: %s", q.FQDN, err) - return nil, ErrNotFound + case dns.TypeSOA: + // Direct query for the SOA record. + if q.FQDN == internalSpecialUseDomain { + return er.makeRRCache(q, []dns.RR{internalSpecialUseSOA}), nil } - return er.makeRRCache(q, records), nil - - case "router.local.": - routers := netenv.Gateways() - if len(routers) == 0 { - return nil, ErrNotFound - } - records, err := netutils.IPsToRRs(q.FQDN, routers) - if err != nil { - log.Warningf("nameserver: failed to create gateway response to %s: %s", q.FQDN, err) - return nil, ErrNotFound - } - return er.makeRRCache(q, records), nil } - // no match - return nil, ErrContinue // continue with next resolver + // No match, reply with NXDOMAIN and SOA record + reply := er.nxDomain(q) + reply.Ns = []dns.RR{internalSpecialUseSOA} + return reply, nil +} + +func (er *envResolverConn) nxDomain(q *Query) *RRCache { + return er.makeRRCache(q, nil) } func (er *envResolverConn) makeRRCache(q *Query, answers []dns.RR) *RRCache { - q.NoCaching = true // disable caching, as the env always has the data available and more up to date. + // Disable caching, as the env always has the raw data available. + q.NoCaching = true + return &RRCache{ Domain: q.FQDN, Question: q.QType, Answer: answers, + Extra: []dns.RR{internalSpecialUseComment}, // Always add comment about this TLD. Server: envResolver.Server, ServerScope: envResolver.ServerIPScope, } diff --git a/resolver/resolver-mdns.go b/resolver/resolver-mdns.go index 1dcf9f9f..9a3732e7 100644 --- a/resolver/resolver-mdns.go +++ b/resolver/resolver-mdns.go @@ -37,6 +37,7 @@ var ( Source: ServerSourceMDNS, Conn: &mDNSResolverConn{}, } + mDNSResolvers = []*Resolver{mDNSResolver} ) type mDNSResolverConn struct{} @@ -208,7 +209,7 @@ func handleMDNSMessages(ctx context.Context, messages chan *dns.Msg) error { // add all entries to RRCache for _, entry := range message.Answer { - if strings.HasSuffix(entry.Header().Name, ".local.") || domainInScope(entry.Header().Name, localReverseScopes) { + if domainInScope(entry.Header().Name, multicastDomains) { if saveFullRequest { k := indexOfRR(entry.Header(), &rrCache.Answer) if k == -1 { @@ -230,7 +231,7 @@ func handleMDNSMessages(ctx context.Context, messages chan *dns.Msg) error { } } for _, entry := range message.Ns { - if strings.HasSuffix(entry.Header().Name, ".local.") || domainInScope(entry.Header().Name, localReverseScopes) { + if domainInScope(entry.Header().Name, multicastDomains) { if saveFullRequest { k := indexOfRR(entry.Header(), &rrCache.Ns) if k == -1 { @@ -252,7 +253,7 @@ func handleMDNSMessages(ctx context.Context, messages chan *dns.Msg) error { } } for _, entry := range message.Extra { - if strings.HasSuffix(entry.Header().Name, ".local.") || domainInScope(entry.Header().Name, localReverseScopes) { + if domainInScope(entry.Header().Name, multicastDomains) { if saveFullRequest { k := indexOfRR(entry.Header(), &rrCache.Extra) if k == -1 { diff --git a/resolver/resolvers.go b/resolver/resolvers.go index 5ce0b094..da5c30ba 100644 --- a/resolver/resolvers.go +++ b/resolver/resolvers.go @@ -344,7 +344,7 @@ func checkSearchScope(searchDomain string) (ok bool) { } // check if suffix is a special service domain (may be handled fully by local nameserver) - if domainInScope("."+suffix+".", specialServiceScopes) { + if domainInScope("."+suffix+".", specialServiceDomains) { return true } diff --git a/resolver/resolvers_test.go b/resolver/resolvers_test.go index c40e6464..93f411e3 100644 --- a/resolver/resolvers_test.go +++ b/resolver/resolvers_test.go @@ -24,6 +24,7 @@ func TestCheckResolverSearchScope(t *testing.T) { 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) diff --git a/resolver/scopes.go b/resolver/scopes.go index 8b8edd77..c0414381 100644 --- a/resolver/scopes.go +++ b/resolver/scopes.go @@ -11,87 +11,74 @@ import ( "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 - +// Domain Scopes var ( - // RFC6761 - respond with 127.0.0.1 and ::1 to A and AAAA queries respectively, else nxdomain - localhost = ".localhost." + // Localhost Domain + // Handling: Respond with 127.0.0.1 and ::1 to A and AAAA queries, respectively. + // RFC6761 + localhostDomain = ".localhost." - // RFC6761 - always respond with nxdomain - invalid = ".invalid." + // Invalid Domain + // Handling: Always respond with NXDOMAIN. + // RFC6761 + invalidDomain = ".invalid." - // RFC6762 - resolve locally - local = ".local." + // Internal Special-Use Domain + // Used by Portmaster for special addressing. + internalSpecialUseDomainScope = "." + internalSpecialUseDomain - // 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 + // Multicast DNS + // Handling: Send to nameservers with matching search scope, then MDNS + // RFC6762 + multicastDomains = []string{ + ".local.", + ".254.169.in-addr.arpa.", + ".8.e.f.ip6.arpa.", + ".9.e.f.ip6.arpa.", + ".a.e.f.ip6.arpa.", + ".b.e.f.ip6.arpa.", } - // RFC6761 - only resolve locally - localTestScopes = []string{ + // Special-Use Domain Names + // Handling: Send to nameservers with matching search scope, then local and system assigned nameservers + // IANA Ref: https://www.iana.org/assignments/special-use-domain-names + specialUseDomains = []string{ + // RFC8375: Designated for non-unique use in residential home networks. + ".home.arpa.", + + // RFC6762 (Appendix G): Non-official, but officially listed, private use domains. + ".intranet.", + ".internal.", + ".private.", + ".corp.", + ".home.", + ".lan.", + + // RFC6761: IPv4 private-address reverse-mapping domains. + ".10.in-addr.arpa.", + ".16.172.in-addr.arpa.", + ".17.172.in-addr.arpa.", + ".18.172.in-addr.arpa.", + ".19.172.in-addr.arpa.", + ".20.172.in-addr.arpa.", + ".21.172.in-addr.arpa.", + ".22.172.in-addr.arpa.", + ".23.172.in-addr.arpa.", + ".24.172.in-addr.arpa.", + ".25.172.in-addr.arpa.", + ".26.172.in-addr.arpa.", + ".27.172.in-addr.arpa.", + ".28.172.in-addr.arpa.", + ".29.172.in-addr.arpa.", + ".30.172.in-addr.arpa.", + ".31.172.in-addr.arpa.", + ".168.192.in-addr.arpa.", + + // RFC4193: IPv6 private-address reverse-mapping domains. + ".d.f.ip6.arpa", + ".c.f.ip6.arpa", + + // RFC6761: Special use domains for documentation and testing. ".example.", ".example.com.", ".example.net.", @@ -99,10 +86,14 @@ var ( ".test.", } - // resolve globally - resolving these should be disabled by default - specialServiceScopes = []string{ - ".onion.", // Tor Hidden Services, RFC7686 - ".bit.", // Namecoin, https://www.namecoin.org/ + // Special-Service Domain Names + // Handling: Send to nameservers with matching search scope, then local and system assigned nameservers + specialServiceDomains = []string{ + // RFC7686: Tor Hidden Services + ".onion.", + + // Namecoin: Blockchain based nameservice, https://www.namecoin.org/ + ".bit.", } ) @@ -116,99 +107,70 @@ func domainInScope(dotPrefixedFQDN string, scopeList []string) bool { } // GetResolversInScope returns all resolvers that are in scope the resolve the given query and options. -func GetResolversInScope(ctx context.Context, q *Query) (selected []*Resolver) { //nolint:gocognit // TODO +func GetResolversInScope(ctx context.Context, q *Query) (selected []*Resolver, tryAll bool) { //nolint:gocognit // TODO resolversLock.RLock() defer resolversLock.RUnlock() - // resolver selection: - // local -> local scopes, mdns - // local-inaddr -> local, mdns - // global -> local scopes, global - // special -> local scopes, local - - // special connectivity domains - if netenv.IsConnectivityDomain(q.FQDN) && len(systemResolvers) > 0 { - selected = append(selected, envResolver) - selected = append(selected, systemResolvers...) // dhcp assigned resolvers - return selected + // Internal use domains + if strings.HasSuffix(q.dotPrefixedFQDN, internalSpecialUseDomainScope) { + return envResolvers, false } - // check local scopes + // Special connectivity domains + if netenv.IsConnectivityDomain(q.FQDN) && len(systemResolvers) > 0 { + // Do not do compliance checks for connectivity domains. + selected = append(selected, systemResolvers...) // dhcp assigned resolvers + return selected, false + } + + // Prioritize search 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) - } + selected = addResolvers(ctx, q, selected, scope.Resolvers) + } + } + + // Handle multicast domains + if domainInScope(q.dotPrefixedFQDN, multicastDomains) { + selected = addResolvers(ctx, q, selected, mDNSResolvers) + // Add local resolvers if no resolvers were selected. + if len(selected) == 0 { + selected = addResolvers(ctx, q, selected, localResolvers) + } + return selected, true + } + + // Special use domains + if domainInScope(q.dotPrefixedFQDN, specialUseDomains) || + domainInScope(q.dotPrefixedFQDN, specialServiceDomains) { + selected = addResolvers(ctx, q, selected, localResolvers) + selected = addResolvers(ctx, q, selected, systemResolvers) + return selected, true + } + + // Global domains + selected = addResolvers(ctx, q, selected, globalResolvers) + return selected, false +} + +func addResolvers(ctx context.Context, q *Query, selected []*Resolver, addResolvers []*Resolver) []*Resolver { +addNextResolver: + for _, resolver := range addResolvers { + // check for compliance + if err := resolver.checkCompliance(ctx, q); err != nil { + log.Tracef("skipping non-compliant resolver %s: %s", resolver.GetName(), err) + continue + } + + // deduplicate + for _, selectedResolver := range selected { + if selectedResolver.Server == resolver.Server { + continue addNextResolver } } - } - // 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 env resolver - selected = append(selected, envResolver) - // 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) - } + // add compliant and unique resolvers to selected resolvers + selected = append(selected, resolver) } return selected } @@ -222,12 +184,12 @@ var ( func (q *Query) checkCompliance() error { // RFC6761 - always respond with nxdomain - if strings.HasSuffix(q.dotPrefixedFQDN, invalid) { + if strings.HasSuffix(q.dotPrefixedFQDN, invalidDomain) { 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) { + if strings.HasSuffix(q.dotPrefixedFQDN, localhostDomain) { switch uint16(q.QType) { case dns.TypeA, dns.TypeAAAA: return ErrLocalhost @@ -238,16 +200,10 @@ func (q *Query) checkCompliance() error { // special TLDs if dontResolveSpecialDomains(q.SecurityLevel) && - domainInScope(q.dotPrefixedFQDN, specialServiceScopes) { + domainInScope(q.dotPrefixedFQDN, specialServiceDomains) { return ErrSpecialDomainsDisabled } - // testing TLDs - if dontResolveTestDomains(q.SecurityLevel) && - domainInScope(q.dotPrefixedFQDN, localTestScopes) { - return ErrTestDomainsDisabled - } - return nil }