types/dnstype, ipn/ipnlocal: allow other DNS resolvers with exit nodes

dnstype.Resolver adds a boolean UseWithExitNode that controls
whether the resolver should be used in tailscale exit node contexts
(not wireguard exit nodes). If UseWithExitNode resolvers are found,
they are installed as the global resolvers. If no UseWithExitNode resolvers
are found, the exit node resolver continues to be installed as the global
resolver. Split DNS Routes referencing UseWithExitNode resolvers are also
installed.

Updates #8237

Fixes tailscale/corp#30906
Fixes tailscale/corp#30907

Signed-off-by: Michael Ben-Ami <mzb@tailscale.com>
This commit is contained in:
Michael Ben-Ami
2025-08-11 12:10:33 -04:00
committed by mzbenami
parent b8c45a6a8f
commit 3f1851a6d9
8 changed files with 235 additions and 51 deletions

View File

@@ -35,6 +35,12 @@ type Resolver struct {
//
// As of 2022-09-08, BootstrapResolution is not yet used.
BootstrapResolution []netip.Addr `json:",omitempty"`
// UseWithExitNode designates that this resolver should continue to be used when an
// exit node is in use. Normally, DNS resolution is delegated to the exit node but
// there are situations where it is preferable to still use a Split DNS server and/or
// global DNS server instead of the exit node.
UseWithExitNode bool `json:",omitempty"`
}
// IPPort returns r.Addr as an IP address and port if either
@@ -64,5 +70,7 @@ func (r *Resolver) Equal(other *Resolver) bool {
return true
}
return r.Addr == other.Addr && slices.Equal(r.BootstrapResolution, other.BootstrapResolution)
return r.Addr == other.Addr &&
slices.Equal(r.BootstrapResolution, other.BootstrapResolution) &&
r.UseWithExitNode == other.UseWithExitNode
}

View File

@@ -25,6 +25,7 @@ func (src *Resolver) Clone() *Resolver {
var _ResolverCloneNeedsRegeneration = Resolver(struct {
Addr string
BootstrapResolution []netip.Addr
UseWithExitNode bool
}{})
// Clone duplicates src into dst and reports whether it succeeded.

View File

@@ -17,7 +17,7 @@ func TestResolverEqual(t *testing.T) {
fieldNames = append(fieldNames, field.Name)
}
sort.Strings(fieldNames)
if !slices.Equal(fieldNames, []string{"Addr", "BootstrapResolution"}) {
if !slices.Equal(fieldNames, []string{"Addr", "BootstrapResolution", "UseWithExitNode"}) {
t.Errorf("Resolver fields changed; update test")
}
@@ -68,6 +68,18 @@ func TestResolverEqual(t *testing.T) {
},
want: false,
},
{
name: "equal UseWithExitNode",
a: &Resolver{Addr: "dns.example.com", UseWithExitNode: true},
b: &Resolver{Addr: "dns.example.com", UseWithExitNode: true},
want: true,
},
{
name: "not equal UseWithExitNode",
a: &Resolver{Addr: "dns.example.com", UseWithExitNode: true},
b: &Resolver{Addr: "dns.example.com", UseWithExitNode: false},
want: false,
},
}
for _, tt := range tests {

View File

@@ -88,10 +88,12 @@ func (v ResolverView) Addr() string { return v.ж.Addr }
func (v ResolverView) BootstrapResolution() views.Slice[netip.Addr] {
return views.SliceOf(v.ж.BootstrapResolution)
}
func (v ResolverView) UseWithExitNode() bool { return v.ж.UseWithExitNode }
func (v ResolverView) Equal(v2 ResolverView) bool { return v.ж.Equal(v2.ж) }
// A compilation failure here means this code must be regenerated, with the command at the top of this file.
var _ResolverViewNeedsRegeneration = Resolver(struct {
Addr string
BootstrapResolution []netip.Addr
UseWithExitNode bool
}{})