mapper: send base_domain as the exit node filtered set

SaaS sends its own domains, which name nothing here. Otherwise an exit node
proxies tailnet names, and where its OS resolver reaches MagicDNS a client
reads its view of the tailnet. MagicDNS only: without it, base_domain can be
public, and clients ask the exit node for its names. Reverse lookups stay
open, as on SaaS.

Updates #3626
This commit is contained in:
Kristoffer Dalby committed 2026-11-03 16:29:03 +01:00
1 parent ef6d033d19
commit a7f2c85cfd
3 files changed
+38 -2

No files matched your search

+10
View File
@@ -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
}
+28
View File
@@ -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
-2
View File
@@ -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,