diff --git a/ipn/ipnlocal/local.go b/ipn/ipnlocal/local.go index e6df1269f..6395be9f9 100644 --- a/ipn/ipnlocal/local.go +++ b/ipn/ipnlocal/local.go @@ -1486,16 +1486,30 @@ func (b *LocalBackend) authReconfig() { // If CorpDNS is false, dcfg remains the zero value. if uc.CorpDNS { - proxied := nm.DNS.Proxied - if proxied && len(nm.DNS.Nameservers) == 0 { - b.logf("[unexpected] dns proxied but no nameservers") - proxied = false + for _, resolver := range nm.DNS.Resolvers { + res, err := parseResolver(resolver) + if err != nil { + b.logf(err.Error()) + continue + } + dcfg.DefaultResolvers = append(dcfg.DefaultResolvers, res) } - for _, ip := range nm.DNS.Nameservers { - dcfg.DefaultResolvers = append(dcfg.DefaultResolvers, netaddr.IPPort{ - IP: ip, - Port: 53, - }) + if len(nm.DNS.Routes) > 0 { + dcfg.Routes = map[string][]netaddr.IPPort{} + } + for suffix, resolvers := range nm.DNS.Routes { + if !strings.HasSuffix(suffix, ".") || strings.HasPrefix(suffix, ".") { + b.logf("[unexpected] malformed DNS route suffix %q", suffix) + continue + } + for _, resolver := range resolvers { + res, err := parseResolver(resolver) + if err != nil { + b.logf(err.Error()) + continue + } + dcfg.Routes[suffix] = append(dcfg.Routes[suffix], res) + } } dcfg.SearchDomains = nm.DNS.Domains dcfg.AuthoritativeSuffixes = magicDNSRootDomains(nm) @@ -1509,9 +1523,12 @@ func (b *LocalBackend) authReconfig() { } dcfg.Hosts[name] = ips } - // TODO: hack to make the current code continue to work while - // refactoring happens. - if proxied { + enableMagicDNS := nm.DNS.Proxied + if enableMagicDNS && len(nm.DNS.Resolvers) == 0 { + b.logf("[unexpected] dns proxied but no nameservers") + enableMagicDNS = false + } + if enableMagicDNS { dcfg.Hosts = map[string][]netaddr.IP{} set(nm.Name, nm.Addresses) for _, peer := range nm.Peers { @@ -1529,6 +1546,17 @@ func (b *LocalBackend) authReconfig() { b.initPeerAPIListener() } +func parseResolver(cfg tailcfg.DNSResolver) (netaddr.IPPort, error) { + ip, err := netaddr.ParseIP(cfg.Addr) + if err != nil { + return netaddr.IPPort{}, fmt.Errorf("[unexpected] non-IP resolver %q", cfg.Addr) + } + return netaddr.IPPort{ + IP: ip, + Port: 53, + }, nil +} + // tailscaleVarRoot returns the root directory of Tailscale's writable // storage area. (e.g. "/var/lib/tailscale") func tailscaleVarRoot() string { diff --git a/tailcfg/tailcfg.go b/tailcfg/tailcfg.go index 2ab061b63..2a05d9e9a 100644 --- a/tailcfg/tailcfg.go +++ b/tailcfg/tailcfg.go @@ -4,7 +4,7 @@ package tailcfg -//go:generate go run tailscale.com/cmd/cloner --type=User,Node,Hostinfo,NetInfo,Login,DNSConfig,RegisterResponse --clonefunc=true --output=tailcfg_clone.go +//go:generate go run tailscale.com/cmd/cloner --type=User,Node,Hostinfo,NetInfo,Login,DNSConfig,DNSResolver,RegisterResponse --clonefunc=true --output=tailcfg_clone.go import ( "bytes" @@ -36,7 +36,8 @@ // 11: 2021-03-03: client understands IPv6, multiple default routes, and goroutine dumping // 12: 2021-03-04: client understands PingRequest // 13: 2021-03-19: client understands FilterRule.IPProto -const CurrentMapRequestVersion = 13 +// 14: 2021-04-07: client understands DNSConfig.Routes and DNSConfig.Resolvers +const CurrentMapRequestVersion = 14 type StableID string @@ -763,19 +764,52 @@ type FilterRule struct { }, } +// DNSResolver is the configuration for one DNS resolver. +type DNSResolver struct { + // Addr is the address of the DNS resolver, one of: + // - A plain IP address for a "classic" UDP+TCP DNS resolver + // - [TODO] "tls://resolver.com" for DNS over TCP+TLS + // - [TODO] "https://resolver.com/query-tmpl" for DNS over HTTPS + Addr string `json:",omitempty"` + + // BootstrapResolution is an optional suggested resolution for the + // DoT/DoH resolver, if the resolver URL does not reference an IP + // address directly. + // BootstrapResolution may be empty, in which case clients should + // look up the DoT/DoH server using their local "classic" DNS + // resolver. + BootstrapResolution []netaddr.IP `json:",omitempty"` +} + // DNSConfig is the DNS configuration. type DNSConfig struct { + // Resolvers are the DNS resolvers to use, in order of preference. + Resolvers []DNSResolver `json:",omitempty"` + // Routes maps DNS name suffixes to a set of DNS resolvers to + // use. It is used to implement "split DNS" and other advanced DNS + // routing overlays. + // Map keys must be fully-qualified DNS name suffixes, with a + // trailing dot but no leading dot. + Routes map[string][]DNSResolver `json:",omitempty"` + // Domains are the search domains to use. + // Search domains must be FQDNs, but *without* the trailing dot. + Domains []string `json:",omitempty"` + // Proxied turns on automatic resolution of hostnames for devices + // in the network map, aka MagicDNS. + // Despite the (legacy) name, does not necessarily cause request + // proxying to be enabled. + Proxied bool `json:",omitempty"` + + // The following fields are only set and used by + // MapRequest.Version >=9 and <14. + // Nameservers are the IP addresses of the nameservers to use. Nameservers []netaddr.IP `json:",omitempty"` - // Domains are the search domains to use. - Domains []string `json:",omitempty"` + // PerDomain is not set by the control server, and does nothing. // TODO(danderson): revise DNS configuration to make this useful // again. - PerDomain bool - // Proxied indicates whether DNS requests are proxied through a dns.Resolver. - // This enables MagicDNS. - Proxied bool + PerDomain bool `json:",omitempty"` } // PingRequest is a request to send an HTTP request to prove the @@ -829,15 +863,11 @@ type MapResponse struct { PeerSeenChange map[NodeID]bool `json:",omitempty"` // DNS is the same as DNSConfig.Nameservers. - // - // TODO(dmytro): should be sent in DNSConfig.Nameservers once clients have updated. + // Only populated if MapRequest.Version < 9. DNS []netaddr.IP `json:",omitempty"` - // SearchPaths is the old way to specify DNS search - // domains. Clients should use these values if set, but the - // server will omit this field for clients with - // MapRequest.Version >= 9. Clients should prefer to use - // DNSConfig.Domains instead. + // SearchPaths is the old way to specify DNS search domains. + // Only populated if MapRequest.Version < 9. SearchPaths []string `json:",omitempty"` // DNSConfig contains the DNS settings for the client to use. diff --git a/tailcfg/tailcfg_clone.go b/tailcfg/tailcfg_clone.go index b24036dee..8b390ecd4 100644 --- a/tailcfg/tailcfg_clone.go +++ b/tailcfg/tailcfg_clone.go @@ -2,7 +2,7 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. -// Code generated by tailscale.com/cmd/cloner -type User,Node,Hostinfo,NetInfo,Login,DNSConfig,RegisterResponse; DO NOT EDIT. +// Code generated by tailscale.com/cmd/cloner -type User,Node,Hostinfo,NetInfo,Login,DNSConfig,DNSResolver,RegisterResponse; DO NOT EDIT. package tailcfg @@ -26,7 +26,7 @@ func (src *User) Clone() *User { } // A compilation failure here means this code must be regenerated, with command: -// tailscale.com/cmd/cloner -type User,Node,Hostinfo,NetInfo,Login,DNSConfig,RegisterResponse +// tailscale.com/cmd/cloner -type User,Node,Hostinfo,NetInfo,Login,DNSConfig,DNSResolver,RegisterResponse var _UserNeedsRegeneration = User(struct { ID UserID LoginName string @@ -58,7 +58,7 @@ func (src *Node) Clone() *Node { } // A compilation failure here means this code must be regenerated, with command: -// tailscale.com/cmd/cloner -type User,Node,Hostinfo,NetInfo,Login,DNSConfig,RegisterResponse +// tailscale.com/cmd/cloner -type User,Node,Hostinfo,NetInfo,Login,DNSConfig,DNSResolver,RegisterResponse var _NodeNeedsRegeneration = Node(struct { ID NodeID StableID StableNodeID @@ -100,7 +100,7 @@ func (src *Hostinfo) Clone() *Hostinfo { } // A compilation failure here means this code must be regenerated, with command: -// tailscale.com/cmd/cloner -type User,Node,Hostinfo,NetInfo,Login,DNSConfig,RegisterResponse +// tailscale.com/cmd/cloner -type User,Node,Hostinfo,NetInfo,Login,DNSConfig,DNSResolver,RegisterResponse var _HostinfoNeedsRegeneration = Hostinfo(struct { IPNVersion string FrontendLogID string @@ -137,7 +137,7 @@ func (src *NetInfo) Clone() *NetInfo { } // A compilation failure here means this code must be regenerated, with command: -// tailscale.com/cmd/cloner -type User,Node,Hostinfo,NetInfo,Login,DNSConfig,RegisterResponse +// tailscale.com/cmd/cloner -type User,Node,Hostinfo,NetInfo,Login,DNSConfig,DNSResolver,RegisterResponse var _NetInfoNeedsRegeneration = NetInfo(struct { MappingVariesByDestIP opt.Bool HairPinning opt.Bool @@ -164,7 +164,7 @@ func (src *Login) Clone() *Login { } // A compilation failure here means this code must be regenerated, with command: -// tailscale.com/cmd/cloner -type User,Node,Hostinfo,NetInfo,Login,DNSConfig,RegisterResponse +// tailscale.com/cmd/cloner -type User,Node,Hostinfo,NetInfo,Login,DNSConfig,DNSResolver,RegisterResponse var _LoginNeedsRegeneration = Login(struct { _ structs.Incomparable ID LoginID @@ -183,18 +183,49 @@ func (src *DNSConfig) Clone() *DNSConfig { } dst := new(DNSConfig) *dst = *src - dst.Nameservers = append(src.Nameservers[:0:0], src.Nameservers...) + dst.Resolvers = make([]DNSResolver, len(src.Resolvers)) + for i := range dst.Resolvers { + dst.Resolvers[i] = *src.Resolvers[i].Clone() + } + if dst.Routes != nil { + dst.Routes = map[string][]DNSResolver{} + for k := range src.Routes { + dst.Routes[k] = append([]DNSResolver{}, src.Routes[k]...) + } + } dst.Domains = append(src.Domains[:0:0], src.Domains...) + dst.Nameservers = append(src.Nameservers[:0:0], src.Nameservers...) return dst } // A compilation failure here means this code must be regenerated, with command: -// tailscale.com/cmd/cloner -type User,Node,Hostinfo,NetInfo,Login,DNSConfig,RegisterResponse +// tailscale.com/cmd/cloner -type User,Node,Hostinfo,NetInfo,Login,DNSConfig,DNSResolver,RegisterResponse var _DNSConfigNeedsRegeneration = DNSConfig(struct { - Nameservers []netaddr.IP + Resolvers []DNSResolver + Routes map[string][]DNSResolver Domains []string - PerDomain bool Proxied bool + Nameservers []netaddr.IP + PerDomain bool +}{}) + +// Clone makes a deep copy of DNSResolver. +// The result aliases no memory with the original. +func (src *DNSResolver) Clone() *DNSResolver { + if src == nil { + return nil + } + dst := new(DNSResolver) + *dst = *src + dst.BootstrapResolution = append(src.BootstrapResolution[:0:0], src.BootstrapResolution...) + return dst +} + +// A compilation failure here means this code must be regenerated, with command: +// tailscale.com/cmd/cloner -type User,Node,Hostinfo,NetInfo,Login,DNSConfig,DNSResolver,RegisterResponse +var _DNSResolverNeedsRegeneration = DNSResolver(struct { + Addr string + BootstrapResolution []netaddr.IP }{}) // Clone makes a deep copy of RegisterResponse. @@ -210,7 +241,7 @@ func (src *RegisterResponse) Clone() *RegisterResponse { } // A compilation failure here means this code must be regenerated, with command: -// tailscale.com/cmd/cloner -type User,Node,Hostinfo,NetInfo,Login,DNSConfig,RegisterResponse +// tailscale.com/cmd/cloner -type User,Node,Hostinfo,NetInfo,Login,DNSConfig,DNSResolver,RegisterResponse var _RegisterResponseNeedsRegeneration = RegisterResponse(struct { User User Login Login @@ -221,7 +252,7 @@ func (src *RegisterResponse) Clone() *RegisterResponse { // Clone duplicates src into dst and reports whether it succeeded. // To succeed, must be of types <*T, *T> or <*T, **T>, -// where T is one of User,Node,Hostinfo,NetInfo,Login,DNSConfig,RegisterResponse. +// where T is one of User,Node,Hostinfo,NetInfo,Login,DNSConfig,DNSResolver,RegisterResponse. func Clone(dst, src interface{}) bool { switch src := src.(type) { case *User: @@ -278,6 +309,15 @@ func Clone(dst, src interface{}) bool { *dst = src.Clone() return true } + case *DNSResolver: + switch dst := dst.(type) { + case *DNSResolver: + *dst = *src.Clone() + return true + case **DNSResolver: + *dst = src.Clone() + return true + } case *RegisterResponse: switch dst := dst.(type) { case *RegisterResponse: