diff --git a/hscontrol/mapper/mapper.go b/hscontrol/mapper/mapper.go index 9053f3459..a6704f366 100644 --- a/hscontrol/mapper/mapper.go +++ b/hscontrol/mapper/mapper.go @@ -135,6 +135,16 @@ func generateDNSConfig( dnsConfig.CertDomains = []string{domain} } + // An exit node refuses to proxy these names. Without this, where its OS + // resolver reaches MagicDNS, a client allowed only to use the exit node can + // read its view of the tailnet through it. Two gaps are deliberate. Without + // MagicDNS, base_domain can be a public domain whose names clients ask the + // exit node for, so node names stay readable. Reverse lookups stay open, as + // on Tailscale SaaS. + if dnsConfig.Proxied && cfg.BaseDomain != "" { + dnsConfig.ExitNodeFilteredSet = []string{"." + strings.ToLower(cfg.BaseDomain)} + } + return dnsConfig } diff --git a/hscontrol/mapper/mapper_test.go b/hscontrol/mapper/mapper_test.go index 0c37ba133..d0edcdf4a 100644 --- a/hscontrol/mapper/mapper_test.go +++ b/hscontrol/mapper/mapper_test.go @@ -42,6 +42,34 @@ var iap = func(ipStr string) *netip.Addr { return &ip } +func TestExitNodeFilteredSet(t *testing.T) { + tests := []struct { + name string + magicDNS bool + baseDomain string + want []string + }{ + {name: "magicdns", magicDNS: true, baseDomain: "Tail.Example.com", want: []string{".tail.example.com"}}, + {name: "no magicdns", magicDNS: false, baseDomain: "tail.example.com"}, + {name: "no base domain", magicDNS: true}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := generateDNSConfig( + &types.Config{ + BaseDomain: tt.baseDomain, + TailcfgDNSConfig: &tailcfg.DNSConfig{Proxied: tt.magicDNS}, + }, + (&types.Node{}).View(), + nil, + ) + + assert.Equal(t, tt.want, got.ExitNodeFilteredSet) + }) + } +} + func TestDNSConfigMapResponse(t *testing.T) { tests := []struct { magicDNS bool diff --git a/hscontrol/servertest/via_compat_test.go b/hscontrol/servertest/via_compat_test.go index 3baba61cd..ae9264916 100644 --- a/hscontrol/servertest/via_compat_test.go +++ b/hscontrol/servertest/via_compat_test.go @@ -144,8 +144,6 @@ var aclMapCompatTests = []string{ var viaCompatSkipReasons = map[string]string{ "tagged-self": "TAGGED_SELF_PROFILE: SaaS gives a tagged node a profile of its own, headscale the shared tagged-devices one", - "exit-node-filtered-set": "EXIT_NODE_FILTER_UNSET: headscale sends no ExitNodeFilteredSet, so an exit node proxies queries for the tailnet's own names; SaaS sends the suffixes those names sit under", - "routes-m1-wildcard-src-multi-range/m-odin1/peers": sameUserPeers, "routes-m1-wildcard-src-multi-range/m-odin2/peers": sameUserPeers, "routes-m2-member-partial-subnet/m-odin1/peers": sameUserPeers,