Clean up domain resolving scopes

Switch to .17.home.arpa. for internal use
This commit is contained in:
Daniel 2020-08-14 16:44:19 +02:00
parent 633bb34288
commit 1da8b7148e
8 changed files with 213 additions and 237 deletions

View file

@ -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 (

View file

@ -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
}

View file

@ -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

View file

@ -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,
}

View file

@ -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 {

View file

@ -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
}

View file

@ -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)

View file

@ -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
}