ipn: delete domainsForProxying, require explicit DNS search domains (mapver 9) (#1078)

Previously the client had heuristics to calculate which DNS search domains
to set, based on the peers' names. Unfortunately that prevented us from
doing some things we wanted to do server-side related to node sharing.

So, bump MapRequest.Version to 9 to signal that the client only uses the
explicitly configured DNS search domains and doesn't augment it with its own
list.

Updates tailscale/corp#1026

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
pull/1091/head
Brad Fitzpatrick 4 years ago committed by GitHub
parent 1ccf997699
commit e8ae355bb8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -547,7 +547,7 @@ func (c *Direct) PollNetMap(ctx context.Context, maxPolls int, cb func(*NetworkM
} }
request := tailcfg.MapRequest{ request := tailcfg.MapRequest{
Version: 8, Version: tailcfg.CurrentMapRequestVersion,
KeepAlive: c.keepAlive, KeepAlive: c.keepAlive,
NodeKey: tailcfg.NodeKey(persist.PrivateNodeKey.Public()), NodeKey: tailcfg.NodeKey(persist.PrivateNodeKey.Public()),
DiscoKey: c.discoPubKey, DiscoKey: c.discoPubKey,

@ -639,7 +639,7 @@ func (b *LocalBackend) updateDNSMap(netMap *controlclient.NetworkMap) {
} }
set(netMap.Name, netMap.Addresses) set(netMap.Name, netMap.Addresses)
dnsMap := tsdns.NewMap(nameToIP, domainsForProxying(netMap)) dnsMap := tsdns.NewMap(nameToIP, magicDNSRootDomains(netMap))
// map diff will be logged in tsdns.Resolver.SetMap. // map diff will be logged in tsdns.Resolver.SetMap.
b.e.SetDNSMap(dnsMap) b.e.SetDNSMap(dnsMap)
} }
@ -1209,20 +1209,14 @@ func (b *LocalBackend) authReconfig() {
// If CorpDNS is false, rcfg.DNS remains the zero value. // If CorpDNS is false, rcfg.DNS remains the zero value.
if uc.CorpDNS { if uc.CorpDNS {
domains := nm.DNS.Domains
proxied := nm.DNS.Proxied proxied := nm.DNS.Proxied
if proxied { if proxied && len(nm.DNS.Nameservers) == 0 {
if len(nm.DNS.Nameservers) == 0 { b.logf("[unexpected] dns proxied but no nameservers")
b.logf("[unexpected] dns proxied but no nameservers") proxied = false
proxied = false
} else {
// Domains for proxying should come first to avoid leaking queries.
domains = append(domainsForProxying(nm), domains...)
}
} }
rcfg.DNS = dns.Config{ rcfg.DNS = dns.Config{
Nameservers: nm.DNS.Nameservers, Nameservers: nm.DNS.Nameservers,
Domains: domains, Domains: nm.DNS.Domains,
PerDomain: nm.DNS.PerDomain, PerDomain: nm.DNS.PerDomain,
Proxied: proxied, Proxied: proxied,
} }
@ -1235,32 +1229,31 @@ func (b *LocalBackend) authReconfig() {
b.logf("[v1] authReconfig: ra=%v dns=%v 0x%02x: %v", uc.RouteAll, uc.CorpDNS, flags, err) b.logf("[v1] authReconfig: ra=%v dns=%v 0x%02x: %v", uc.RouteAll, uc.CorpDNS, flags, err)
} }
// domainsForProxying produces a list of search domains for proxied DNS. // magicDNSRootDomains returns the subset of nm.DNS.Domains that are the search domains for MagicDNS.
func domainsForProxying(nm *controlclient.NetworkMap) []string { // Each entry has a trailing period.
var domains []string func magicDNSRootDomains(nm *controlclient.NetworkMap) []string {
if idx := strings.IndexByte(nm.Name, '.'); idx != -1 { searchPathUsedAsDNSSuffix := func(suffix string) bool {
domains = append(domains, nm.Name[idx+1:]) if tsdns.NameHasSuffix(nm.Name, suffix) {
} return true
for _, peer := range nm.Peers {
idx := strings.IndexByte(peer.Name, '.')
if idx == -1 {
continue
} }
domain := peer.Name[idx+1:] for _, p := range nm.Peers {
seen := false if tsdns.NameHasSuffix(p.Name, suffix) {
// In theory this makes the function O(n^2) worst case, return true
// but in practice we expect domains to contain very few elements
// (only one until invitations are introduced).
for _, seenDomain := range domains {
if domain == seenDomain {
seen = true
} }
} }
if !seen { return false
domains = append(domains, domain) }
var ret []string
for _, d := range nm.DNS.Domains {
if searchPathUsedAsDNSSuffix(d) {
if !strings.HasSuffix(d, ".") {
d += "."
}
ret = append(ret, d)
} }
} }
return domains return ret
} }
// routerConfig produces a router.Config from a wireguard config and IPN prefs. // routerConfig produces a router.Config from a wireguard config and IPN prefs.

@ -22,6 +22,18 @@ import (
"tailscale.com/types/structs" "tailscale.com/types/structs"
) )
// CurrentMapRequestVersion is the current MapRequest.Version value.
//
// History of versions:
// 3: implicit compression, keep-alives
// 4: opt-in keep-alives via KeepAlive field, opt-in compression via Compress
// 5: 2020-10-19, implies IncludeIPv6, delta Peers/UserProfiles, supports MagicDNS
// 6: 2020-12-07: means MapResponse.PacketFilter nil means unchanged
// 7: 2020-12-15: FilterRule.SrcIPs accepts CIDRs+ranges, doesn't warn about 0.0.0.0/::
// 8: 2020-12-19: client can receive IPv6 addresses and routes if beta enabled server-side
// 9: 2020-12-30: client doesn't auto-add implicit search domains from peers; only DNSConfig.Domains
const CurrentMapRequestVersion = 9
type ID int64 type ID int64
type UserID ID type UserID ID
@ -471,14 +483,9 @@ type MapRequest struct {
// we want to signal to the control server that we're capable of something // we want to signal to the control server that we're capable of something
// different. // different.
// //
// History of versions: // For current values and history, see CurrentMapRequestVersion above.
// 3: implicit compression, keep-alives Version int
// 4: opt-in keep-alives via KeepAlive field, opt-in compression via Compress
// 5: 2020-10-19, implies IncludeIPv6, delta Peers/UserProfiles, supports MagicDNS
// 6: 2020-12-07: means MapResponse.PacketFilter nil means unchanged
// 7: 2020-12-15: FilterRule.SrcIPs accepts CIDRs+ranges, doesn't warn about 0.0.0.0/::
// 8: 2020-12-19: client can receive IPv6 addresses and routes if beta enabled server-side
Version int
Compress string // "zstd" or "" (no compression) Compress string // "zstd" or "" (no compression)
KeepAlive bool // whether server should send keep-alives back to us KeepAlive bool // whether server should send keep-alives back to us
NodeKey NodeKey NodeKey NodeKey
@ -627,11 +634,20 @@ type MapResponse struct {
// //
// TODO(dmytro): should be sent in DNSConfig.Nameservers once clients have updated. // TODO(dmytro): should be sent in DNSConfig.Nameservers once clients have updated.
DNS []netaddr.IP `json:",omitempty"` DNS []netaddr.IP `json:",omitempty"`
// SearchPaths are the same as DNSConfig.Domains.
// 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 []string `json:",omitempty"`
// DNSConfig contains the DNS settings for the client to use.
// //
// TODO(dmytro): should be sent in DNSConfig.Domains once clients have updated. // TODO(bradfitz): make this a pointer and conditionally sent
SearchPaths []string `json:",omitempty"` // only if changed, like DERPMap, PacketFilter, etc. It's
DNSConfig DNSConfig `json:",omitempty"` // small, though.
DNSConfig DNSConfig `json:",omitempty"`
// Domain is the name of the network that this node is // Domain is the name of the network that this node is
// in. It's either of the form "example.com" (for user // in. It's either of the form "example.com" (for user

@ -26,6 +26,10 @@ type Map struct {
} }
// NewMap returns a new Map with name to address mapping given by nameToIP. // NewMap returns a new Map with name to address mapping given by nameToIP.
//
// rootDomains are the domains whose subdomains should always be
// resolved locally to prevent leakage of sensitive names. They should
// end in a period ("user-foo.tailscale.net.").
func NewMap(initNameToIP map[string]netaddr.IP, rootDomains []string) *Map { func NewMap(initNameToIP map[string]netaddr.IP, rootDomains []string) *Map {
// TODO(dmytro): we have to allocate names and ipToName, but nameToIP can be avoided. // TODO(dmytro): we have to allocate names and ipToName, but nameToIP can be avoided.
// It is here because control sends us names not in canonical form. Change this. // It is here because control sends us names not in canonical form. Change this.

@ -194,8 +194,8 @@ func (r *Resolver) Resolve(domain string, tp dns.Type) (netaddr.IP, dns.RCode, e
} }
anyHasSuffix := false anyHasSuffix := false
for _, rootDomain := range dnsMap.rootDomains { for _, suffix := range dnsMap.rootDomains {
if strings.HasSuffix(domain, rootDomain) { if NameHasSuffix(domain, suffix) {
anyHasSuffix = true anyHasSuffix = true
break break
} }
@ -611,3 +611,12 @@ func (r *Resolver) respond(query []byte) ([]byte, error) {
return marshalResponse(resp) return marshalResponse(resp)
} }
// NameHasSuffix reports whether the provided DNS name ends with the
// component(s) in suffix, ignoring any trailing dots.
func NameHasSuffix(name, suffix string) bool {
name = strings.TrimSuffix(name, ".")
suffix = strings.TrimSuffix(suffix, ".")
nameBase := strings.TrimSuffix(name, suffix)
return len(nameBase) < len(name) && strings.HasSuffix(nameBase, ".")
}

@ -758,3 +758,24 @@ func TestMarshalResponseFormatError(t *testing.T) {
} }
t.Logf("response: %q", v) t.Logf("response: %q", v)
} }
func TestNameHasSuffix(t *testing.T) {
tests := []struct {
name, suffix string
want bool
}{
{"foo.com", "com", true},
{"foo.com.", "com", true},
{"foo.com.", "com.", true},
{"", "", false},
{"foo.com.", "", false},
{"foo.com.", "o.com", false},
}
for _, tt := range tests {
got := NameHasSuffix(tt.name, tt.suffix)
if got != tt.want {
t.Errorf("NameHasSuffix(%q, %q) = %v; want %v", tt.name, tt.suffix, got, tt.want)
}
}
}

Loading…
Cancel
Save