dnstype.Resolver.UseWithExitNode

Signed-off-by: Michael Ben-Ami <mzb@tailscale.com>
This commit is contained in:
Michael Ben-Ami
2025-08-07 20:53:00 -04:00
parent d12c7a4a6c
commit 953330718f
13 changed files with 97 additions and 205 deletions

View File

@@ -16,4 +16,5 @@ type DNSConfig struct {
type DNSResolver struct { type DNSResolver struct {
Addr string `json:"addr"` Addr string `json:"addr"`
BootstrapResolution []string `json:"bootstrapResolution,omitempty"` BootstrapResolution []string `json:"bootstrapResolution,omitempty"`
UseWithExitNode bool `json:"useWithExitNode"`
} }

View File

@@ -266,14 +266,14 @@ func TestDNSConfigForNetmap(t *testing.T) {
os: "android", os: "android",
nm: &netmap.NetworkMap{ nm: &netmap.NetworkMap{
DNS: tailcfg.DNSConfig{ DNS: tailcfg.DNSConfig{
Resolvers: []*tailcfg.DNSResolver{ Resolvers: []*dnstype.Resolver{
{Resolver: dnstype.Resolver{Addr: "8.8.8.8"}}, {Addr: "8.8.8.8"},
}, },
FallbackResolvers: []*dnstype.Resolver{ FallbackResolvers: []*dnstype.Resolver{
{Addr: "8.8.4.4"}, {Addr: "8.8.4.4"},
}, },
Routes: map[string][]*tailcfg.DNSResolver{ Routes: map[string][]*dnstype.Resolver{
"foo.com.": {{Resolver: dnstype.Resolver{Addr: "1.2.3.4"}}}, "foo.com.": {{Addr: "1.2.3.4"}},
}, },
}, },
}, },

View File

@@ -2084,19 +2084,10 @@ func TestDNSConfigForNetmapForExitNodeConfigs(t *testing.T) {
defaultResolvers := []*dnstype.Resolver{ defaultResolvers := []*dnstype.Resolver{
{Addr: "default.example.com"}, {Addr: "default.example.com"},
} }
containsFlaggedResolvers := append([]*dnstype.Resolver{{Addr: tsUseWithExitNodeResolverAddr}}, defaultResolvers...) containsFlaggedResolvers := append([]*dnstype.Resolver{
{Addr: tsUseWithExitNodeResolverAddr, UseWithExitNode: true},
}, defaultResolvers...)
wrapResolvers := func(resolvers []*dnstype.Resolver) []*tailcfg.DNSResolver {
rr := make([]*tailcfg.DNSResolver, 0, len(resolvers))
for _, r := range resolvers {
wrapped := &tailcfg.DNSResolver{Resolver: *r}
if r.Addr == tsUseWithExitNodeResolverAddr {
wrapped.UseWithExitNode = true
}
rr = append(rr, wrapped)
}
return rr
}
wgResolvers := []*dnstype.Resolver{{Addr: "wg.example.com"}} wgResolvers := []*dnstype.Resolver{{Addr: "wg.example.com"}}
peers := []tailcfg.NodeView{ peers := []tailcfg.NodeView{
(&tailcfg.Node{ (&tailcfg.Node{
@@ -2124,39 +2115,31 @@ func TestDNSConfigForNetmapForExitNodeConfigs(t *testing.T) {
} }
containsFlaggedRoutes := map[dnsname.FQDN][]*dnstype.Resolver{ containsFlaggedRoutes := map[dnsname.FQDN][]*dnstype.Resolver{
"route.example.com.": {{Addr: "route.example.com"}}, "route.example.com.": {{Addr: "route.example.com"}},
"withexit.example.com.": {{Addr: tsUseWithExitNodeResolverAddr}}, "withexit.example.com.": {{Addr: tsUseWithExitNodeResolverAddr, UseWithExitNode: true}},
} }
containsFlaggedAndEmptyRoutes := map[dnsname.FQDN][]*dnstype.Resolver{ containsFlaggedAndEmptyRoutes := map[dnsname.FQDN][]*dnstype.Resolver{
"empty.example.com.": {}, "empty.example.com.": {},
"route.example.com.": {{Addr: "route.example.com"}}, "route.example.com.": {{Addr: "route.example.com"}},
"withexit.example.com.": {{Addr: tsUseWithExitNodeResolverAddr}}, "withexit.example.com.": {{Addr: tsUseWithExitNodeResolverAddr, UseWithExitNode: true}},
} }
flaggedRoutes := map[dnsname.FQDN][]*dnstype.Resolver{ flaggedRoutes := map[dnsname.FQDN][]*dnstype.Resolver{
"withexit.example.com.": {{Addr: tsUseWithExitNodeResolverAddr}}, "withexit.example.com.": {{Addr: tsUseWithExitNodeResolverAddr, UseWithExitNode: true}},
} }
emptyRoutes := map[dnsname.FQDN][]*dnstype.Resolver{ emptyRoutes := map[dnsname.FQDN][]*dnstype.Resolver{
"empty.example.com.": {}, "empty.example.com.": {},
} }
flaggedAndEmptyRoutes := map[dnsname.FQDN][]*dnstype.Resolver{ flaggedAndEmptyRoutes := map[dnsname.FQDN][]*dnstype.Resolver{
"empty.example.com.": {}, "empty.example.com.": {},
"withexit.example.com.": {{Addr: tsUseWithExitNodeResolverAddr}}, "withexit.example.com.": {{Addr: tsUseWithExitNodeResolverAddr, UseWithExitNode: true}},
} }
stringifyRoutes := func(routes map[dnsname.FQDN][]*dnstype.Resolver) map[string][]*tailcfg.DNSResolver { stringifyRoutes := func(routes map[dnsname.FQDN][]*dnstype.Resolver) map[string][]*dnstype.Resolver {
if routes == nil { if routes == nil {
return nil return nil
} }
m := make(map[string][]*tailcfg.DNSResolver) m := make(map[string][]*dnstype.Resolver)
for k, v := range routes { for k, v := range routes {
var rr []*tailcfg.DNSResolver m[string(k)] = v
for _, res := range v {
wrapped := &tailcfg.DNSResolver{Resolver: *res}
if res.Addr == tsUseWithExitNodeResolverAddr {
wrapped.UseWithExitNode = true
}
rr = append(rr, wrapped)
}
m[string(k)] = rr
} }
return m return m
} }
@@ -2182,7 +2165,7 @@ func TestDNSConfigForNetmapForExitNodeConfigs(t *testing.T) {
name: "tsExit/noRoutes/defaultResolver", name: "tsExit/noRoutes/defaultResolver",
exitNode: "ts", exitNode: "ts",
peers: peers, peers: peers,
dnsConfig: &tailcfg.DNSConfig{Resolvers: wrapResolvers(defaultResolvers)}, dnsConfig: &tailcfg.DNSConfig{Resolvers: defaultResolvers},
wantDefaultResolvers: []*dnstype.Resolver{{Addr: exitDOH}}, wantDefaultResolvers: []*dnstype.Resolver{{Addr: exitDOH}},
wantRoutes: nil, wantRoutes: nil,
}, },
@@ -2190,8 +2173,8 @@ func TestDNSConfigForNetmapForExitNodeConfigs(t *testing.T) {
name: "tsExit/noRoutes/flaggedResolverOnly", name: "tsExit/noRoutes/flaggedResolverOnly",
exitNode: "ts", exitNode: "ts",
peers: peers, peers: peers,
dnsConfig: &tailcfg.DNSConfig{Resolvers: wrapResolvers(containsFlaggedResolvers)}, dnsConfig: &tailcfg.DNSConfig{Resolvers: containsFlaggedResolvers},
wantDefaultResolvers: []*dnstype.Resolver{{Addr: tsUseWithExitNodeResolverAddr}}, wantDefaultResolvers: []*dnstype.Resolver{{Addr: tsUseWithExitNodeResolverAddr, UseWithExitNode: true}},
wantRoutes: nil, wantRoutes: nil,
}, },
@@ -2210,7 +2193,7 @@ func TestDNSConfigForNetmapForExitNodeConfigs(t *testing.T) {
name: "tsExit/routes/defaultResolver", name: "tsExit/routes/defaultResolver",
exitNode: "ts", exitNode: "ts",
peers: peers, peers: peers,
dnsConfig: &tailcfg.DNSConfig{Routes: stringifyRoutes(baseRoutes), Resolvers: wrapResolvers(defaultResolvers)}, dnsConfig: &tailcfg.DNSConfig{Routes: stringifyRoutes(baseRoutes), Resolvers: defaultResolvers},
wantDefaultResolvers: []*dnstype.Resolver{{Addr: exitDOH}}, wantDefaultResolvers: []*dnstype.Resolver{{Addr: exitDOH}},
wantRoutes: nil, wantRoutes: nil,
}, },
@@ -2218,15 +2201,15 @@ func TestDNSConfigForNetmapForExitNodeConfigs(t *testing.T) {
name: "tsExit/routes/flaggedResolverOnly", name: "tsExit/routes/flaggedResolverOnly",
exitNode: "ts", exitNode: "ts",
peers: peers, peers: peers,
dnsConfig: &tailcfg.DNSConfig{Routes: stringifyRoutes(baseRoutes), Resolvers: wrapResolvers(containsFlaggedResolvers)}, dnsConfig: &tailcfg.DNSConfig{Routes: stringifyRoutes(baseRoutes), Resolvers: containsFlaggedResolvers},
wantDefaultResolvers: []*dnstype.Resolver{{Addr: tsUseWithExitNodeResolverAddr}}, wantDefaultResolvers: []*dnstype.Resolver{{Addr: tsUseWithExitNodeResolverAddr, UseWithExitNode: true}},
wantRoutes: nil, wantRoutes: nil,
}, },
{ {
name: "tsExit/flaggedRoutesOnly/defaultResolver", name: "tsExit/flaggedRoutesOnly/defaultResolver",
exitNode: "ts", exitNode: "ts",
peers: peers, peers: peers,
dnsConfig: &tailcfg.DNSConfig{Routes: stringifyRoutes(containsFlaggedRoutes), Resolvers: wrapResolvers(defaultResolvers)}, dnsConfig: &tailcfg.DNSConfig{Routes: stringifyRoutes(containsFlaggedRoutes), Resolvers: defaultResolvers},
wantDefaultResolvers: []*dnstype.Resolver{{Addr: exitDOH}}, wantDefaultResolvers: []*dnstype.Resolver{{Addr: exitDOH}},
wantRoutes: flaggedRoutes, wantRoutes: flaggedRoutes,
}, },
@@ -2234,15 +2217,15 @@ func TestDNSConfigForNetmapForExitNodeConfigs(t *testing.T) {
name: "tsExit/flaggedRoutesOnly/flaggedResolverOnly", name: "tsExit/flaggedRoutesOnly/flaggedResolverOnly",
exitNode: "ts", exitNode: "ts",
peers: peers, peers: peers,
dnsConfig: &tailcfg.DNSConfig{Routes: stringifyRoutes(containsFlaggedRoutes), Resolvers: wrapResolvers(containsFlaggedResolvers)}, dnsConfig: &tailcfg.DNSConfig{Routes: stringifyRoutes(containsFlaggedRoutes), Resolvers: containsFlaggedResolvers},
wantDefaultResolvers: []*dnstype.Resolver{{Addr: tsUseWithExitNodeResolverAddr}}, wantDefaultResolvers: []*dnstype.Resolver{{Addr: tsUseWithExitNodeResolverAddr, UseWithExitNode: true}},
wantRoutes: flaggedRoutes, wantRoutes: flaggedRoutes,
}, },
{ {
name: "tsExit/emptyRoutesOnly/defaultResolver", name: "tsExit/emptyRoutesOnly/defaultResolver",
exitNode: "ts", exitNode: "ts",
peers: peers, peers: peers,
dnsConfig: &tailcfg.DNSConfig{Routes: stringifyRoutes(containsEmptyRoutes), Resolvers: wrapResolvers(defaultResolvers)}, dnsConfig: &tailcfg.DNSConfig{Routes: stringifyRoutes(containsEmptyRoutes), Resolvers: defaultResolvers},
wantDefaultResolvers: []*dnstype.Resolver{{Addr: exitDOH}}, wantDefaultResolvers: []*dnstype.Resolver{{Addr: exitDOH}},
wantRoutes: emptyRoutes, wantRoutes: emptyRoutes,
}, },
@@ -2250,7 +2233,7 @@ func TestDNSConfigForNetmapForExitNodeConfigs(t *testing.T) {
name: "tsExit/flaggedAndEmptyRoutesOnly/defaultResolver", name: "tsExit/flaggedAndEmptyRoutesOnly/defaultResolver",
exitNode: "ts", exitNode: "ts",
peers: peers, peers: peers,
dnsConfig: &tailcfg.DNSConfig{Routes: stringifyRoutes(containsFlaggedAndEmptyRoutes), Resolvers: wrapResolvers(defaultResolvers)}, dnsConfig: &tailcfg.DNSConfig{Routes: stringifyRoutes(containsFlaggedAndEmptyRoutes), Resolvers: defaultResolvers},
wantDefaultResolvers: []*dnstype.Resolver{{Addr: exitDOH}}, wantDefaultResolvers: []*dnstype.Resolver{{Addr: exitDOH}},
wantRoutes: flaggedAndEmptyRoutes, wantRoutes: flaggedAndEmptyRoutes,
}, },
@@ -2258,8 +2241,8 @@ func TestDNSConfigForNetmapForExitNodeConfigs(t *testing.T) {
name: "tsExit/flaggedAndEmptyRoutesOnly/flaggedResolverOnly", name: "tsExit/flaggedAndEmptyRoutesOnly/flaggedResolverOnly",
exitNode: "ts", exitNode: "ts",
peers: peers, peers: peers,
dnsConfig: &tailcfg.DNSConfig{Routes: stringifyRoutes(containsFlaggedAndEmptyRoutes), Resolvers: wrapResolvers(containsFlaggedResolvers)}, dnsConfig: &tailcfg.DNSConfig{Routes: stringifyRoutes(containsFlaggedAndEmptyRoutes), Resolvers: containsFlaggedResolvers},
wantDefaultResolvers: []*dnstype.Resolver{{Addr: tsUseWithExitNodeResolverAddr}}, wantDefaultResolvers: []*dnstype.Resolver{{Addr: tsUseWithExitNodeResolverAddr, UseWithExitNode: true}},
wantRoutes: flaggedAndEmptyRoutes, wantRoutes: flaggedAndEmptyRoutes,
}, },
@@ -2279,7 +2262,7 @@ func TestDNSConfigForNetmapForExitNodeConfigs(t *testing.T) {
name: "wgExit/noRoutes/defaultResolver", name: "wgExit/noRoutes/defaultResolver",
exitNode: "wg", exitNode: "wg",
peers: peers, peers: peers,
dnsConfig: &tailcfg.DNSConfig{Resolvers: wrapResolvers(defaultResolvers)}, dnsConfig: &tailcfg.DNSConfig{Resolvers: defaultResolvers},
wantDefaultResolvers: defaultResolvers, wantDefaultResolvers: defaultResolvers,
wantRoutes: nil, wantRoutes: nil,
}, },
@@ -2287,7 +2270,7 @@ func TestDNSConfigForNetmapForExitNodeConfigs(t *testing.T) {
name: "wgExit/routes/defaultResolver", name: "wgExit/routes/defaultResolver",
exitNode: "wg", exitNode: "wg",
peers: peers, peers: peers,
dnsConfig: &tailcfg.DNSConfig{Routes: stringifyRoutes(baseRoutes), Resolvers: wrapResolvers(defaultResolvers)}, dnsConfig: &tailcfg.DNSConfig{Routes: stringifyRoutes(baseRoutes), Resolvers: defaultResolvers},
wantDefaultResolvers: defaultResolvers, wantDefaultResolvers: defaultResolvers,
wantRoutes: baseRoutes, wantRoutes: baseRoutes,
}, },

View File

@@ -578,59 +578,40 @@ func (nb *nodeBackend) doShutdown(cause error) {
nb.eventClient.Close() nb.eventClient.Close()
} }
func useWithExitNodeResolvers(dc tailcfg.DNSConfig) []*dnstype.Resolver { // useWithExitNodeResolvers filters out resolvers so the ones that remain
return convertResolvers(dc.Resolvers, true) // are all the ones marked for use with exit nodes.
} func useWithExitNodeResolvers(resolvers []*dnstype.Resolver) []*dnstype.Resolver {
filtered := make([]*dnstype.Resolver, 0, len(resolvers))
func useWithExitNodeRoutes(dc tailcfg.DNSConfig) map[string][]*dnstype.Resolver {
return convertRoutes(dc.Routes, true)
}
func resolvers(dc tailcfg.DNSConfig) []*dnstype.Resolver {
return convertResolvers(dc.Resolvers, false)
}
func routes(dc tailcfg.DNSConfig) map[string][]*dnstype.Resolver {
return convertRoutes(dc.Routes, false)
}
// convertResolvers converts tailcfg dns resolvers, which may contain additional
// configuration, to dnstype resolvers, for use in the wireguard engine,
// taking into account exit node contexts.
func convertResolvers(resolvers []*tailcfg.DNSResolver, usingExitNode bool) []*dnstype.Resolver {
converted := make([]*dnstype.Resolver, 0, len(resolvers))
for _, res := range resolvers { for _, res := range resolvers {
// If not using an exit node, all resolvers persist. if res.UseWithExitNode {
// Otherwise, check if the resolver is marked for use with exit node. filtered = append(filtered, res)
if !usingExitNode || res.UseWithExitNode {
converted = append(converted, &res.Resolver)
} }
} }
return converted return filtered
} }
// convertRoutes converts tailcfg dns routes containing tailcfg dns resolvers // useWithExitNodeRoutes filters out routes so the ones that remain
// to a map of routes containing dnstype resolvers, for use in the wireguard engine, // are either zero-length resolver lists, or lists containing only
// taking into account exit node contexts. // resolvers marked for use with exit nodes.
func convertRoutes(routes map[string][]*tailcfg.DNSResolver, usingExitNode bool) map[string][]*dnstype.Resolver { func useWithExitNodeRoutes(routes map[string][]*dnstype.Resolver) map[string][]*dnstype.Resolver {
converted := make(map[string][]*dnstype.Resolver) filtered := make(map[string][]*dnstype.Resolver)
for suffix, resolvers := range routes { for suffix, resolvers := range routes {
// Suffixes with no resolvers represent a valid configuration, // Suffixes with no resolvers represent a valid configuration,
// and should persist regardless of exit node considerations. // and should persist regardless of exit node considerations.
if len(resolvers) == 0 { if len(resolvers) == 0 {
converted[suffix] = make([]*dnstype.Resolver, 0) filtered[suffix] = make([]*dnstype.Resolver, 0)
continue continue
} }
// In exit node contexts, we filter out resolvers not configured for use with // In exit node contexts, we filter out resolvers not configured for use with
// exit nodes. If there are no such configured resolvers, there should not be an entry for that suffix. // exit nodes. If there are no such configured resolvers, there should not be an entry for that suffix.
convertedResolvers := convertResolvers(resolvers, usingExitNode) filteredResolvers := useWithExitNodeResolvers(resolvers)
if len(convertedResolvers) > 0 { if len(filteredResolvers) > 0 {
converted[suffix] = convertedResolvers filtered[suffix] = filteredResolvers
} }
} }
return converted return filtered
} }
// dnsConfigForNetmap returns a *dns.Config for the given netmap, // dnsConfigForNetmap returns a *dns.Config for the given netmap,
@@ -781,7 +762,7 @@ func dnsConfigForNetmap(nm *netmap.NetworkMap, peers map[tailcfg.NodeID]tailcfg.
// to run a DoH DNS proxy, then send all our DNS traffic through it, // to run a DoH DNS proxy, then send all our DNS traffic through it,
// unless we find resolvers with UseWithExitNode set, in which case we use that. // unless we find resolvers with UseWithExitNode set, in which case we use that.
if dohURL, ok := exitNodeCanProxyDNS(nm, peers, prefs.ExitNodeID()); ok { if dohURL, ok := exitNodeCanProxyDNS(nm, peers, prefs.ExitNodeID()); ok {
filtered := useWithExitNodeResolvers(nm.DNS) filtered := useWithExitNodeResolvers(nm.DNS.Resolvers)
if len(filtered) > 0 { if len(filtered) > 0 {
addDefault(filtered) addDefault(filtered)
} else { } else {
@@ -790,7 +771,7 @@ func dnsConfigForNetmap(nm *netmap.NetworkMap, peers map[tailcfg.NodeID]tailcfg.
addDefault([]*dnstype.Resolver{{Addr: dohURL}}) addDefault([]*dnstype.Resolver{{Addr: dohURL}})
} }
addSplitDNSRoutes(useWithExitNodeRoutes(nm.DNS)) addSplitDNSRoutes(useWithExitNodeRoutes(nm.DNS.Routes))
return dcfg return dcfg
} }
@@ -798,7 +779,7 @@ func dnsConfigForNetmap(nm *netmap.NetworkMap, peers map[tailcfg.NodeID]tailcfg.
// use those resolvers as the default, otherwise if there are WireGuard exit // use those resolvers as the default, otherwise if there are WireGuard exit
// node resolvers, use those as the default. // node resolvers, use those as the default.
if len(nm.DNS.Resolvers) > 0 { if len(nm.DNS.Resolvers) > 0 {
addDefault(resolvers(nm.DNS)) addDefault(nm.DNS.Resolvers)
} else { } else {
if resolvers, ok := wireguardExitNodeDNSResolvers(nm, peers, prefs.ExitNodeID()); ok { if resolvers, ok := wireguardExitNodeDNSResolvers(nm, peers, prefs.ExitNodeID()); ok {
addDefault(resolvers) addDefault(resolvers)
@@ -806,7 +787,7 @@ func dnsConfigForNetmap(nm *netmap.NetworkMap, peers map[tailcfg.NodeID]tailcfg.
} }
// Add split DNS routes, with no regard to exit node configuration. // Add split DNS routes, with no regard to exit node configuration.
addSplitDNSRoutes(routes(nm.DNS)) addSplitDNSRoutes(nm.DNS.Routes)
// Set FallbackResolvers as the default resolvers in the // Set FallbackResolvers as the default resolvers in the
// scenarios that can't handle a purely split-DNS config. See // scenarios that can't handle a purely split-DNS config. See

View File

@@ -5,7 +5,7 @@
// the node and the coordination server. // the node and the coordination server.
package tailcfg package tailcfg
//go:generate go run tailscale.com/cmd/viewer --type=User,Node,Hostinfo,NetInfo,Login,DNSConfig,RegisterResponse,RegisterResponseAuth,RegisterRequest,DERPHomeParams,DERPRegion,DERPMap,DERPNode,SSHRule,SSHAction,SSHPrincipal,ControlDialPlan,Location,UserProfile,VIPService,DNSResolver --clonefunc //go:generate go run tailscale.com/cmd/viewer --type=User,Node,Hostinfo,NetInfo,Login,DNSConfig,RegisterResponse,RegisterResponseAuth,RegisterRequest,DERPHomeParams,DERPRegion,DERPMap,DERPNode,SSHRule,SSHAction,SSHPrincipal,ControlDialPlan,Location,UserProfile,VIPService --clonefunc
import ( import (
"bytes" "bytes"
@@ -168,7 +168,7 @@ type CapabilityVersion int
// - 121: 2025-07-19: Client understands peer relay endpoint alloc with [disco.AllocateUDPRelayEndpointRequest] & [disco.AllocateUDPRelayEndpointResponse] // - 121: 2025-07-19: Client understands peer relay endpoint alloc with [disco.AllocateUDPRelayEndpointRequest] & [disco.AllocateUDPRelayEndpointResponse]
// - 122: 2025-07-21: Client sends Hostinfo.ExitNodeID to report which exit node it has selected, if any. // - 122: 2025-07-21: Client sends Hostinfo.ExitNodeID to report which exit node it has selected, if any.
// - 123: 2025-07-28: fix deadlock regression from cryptokey routing change (issue #16651) // - 123: 2025-07-28: fix deadlock regression from cryptokey routing change (issue #16651)
// - 124: 2025-08-07: DNSConfig.{Resolvers,Routes} are re-typed to collections of DNSResolver instead of dnstype.Resolver. // - 124: 2025-08-08: dnstype.Resolver adds UseWithExitNode field.
const CurrentCapabilityVersion CapabilityVersion = 124 const CurrentCapabilityVersion CapabilityVersion = 124
// ID is an integer ID for a user, node, or login allocated by the // ID is an integer ID for a user, node, or login allocated by the
@@ -1699,22 +1699,10 @@ var FilterAllowAll = []FilterRule{
}, },
} }
// DNSResolver describes a single DNS resolver and any special handling needed when
// using that resolver.
type DNSResolver struct {
dnstype.Resolver `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"`
}
// DNSConfig is the DNS configuration. // DNSConfig is the DNS configuration.
type DNSConfig struct { type DNSConfig struct {
// Resolvers are the DNS resolvers to use, in order of preference. // Resolvers are the DNS resolvers to use, in order of preference.
Resolvers []*DNSResolver `json:",omitempty"` Resolvers []*dnstype.Resolver `json:",omitempty"`
// Routes maps DNS name suffixes to a set of DNS resolvers to // Routes maps DNS name suffixes to a set of DNS resolvers to
// use. It is used to implement "split DNS" and other advanced DNS // use. It is used to implement "split DNS" and other advanced DNS
@@ -1726,7 +1714,7 @@ type DNSConfig struct {
// If the value is an empty slice, that means the suffix should still // If the value is an empty slice, that means the suffix should still
// be handled by Tailscale's built-in resolver (100.100.100.100), such // be handled by Tailscale's built-in resolver (100.100.100.100), such
// as for the purpose of handling ExtraRecords. // as for the purpose of handling ExtraRecords.
Routes map[string][]*DNSResolver `json:",omitempty"` Routes map[string][]*dnstype.Resolver `json:",omitempty"`
// FallbackResolvers is like Resolvers, but is only used if a // FallbackResolvers is like Resolvers, but is only used if a
// split DNS configuration is requested in a configuration that // split DNS configuration is requested in a configuration that

View File

@@ -252,7 +252,7 @@ func (src *DNSConfig) Clone() *DNSConfig {
dst := new(DNSConfig) dst := new(DNSConfig)
*dst = *src *dst = *src
if src.Resolvers != nil { if src.Resolvers != nil {
dst.Resolvers = make([]*DNSResolver, len(src.Resolvers)) dst.Resolvers = make([]*dnstype.Resolver, len(src.Resolvers))
for i := range dst.Resolvers { for i := range dst.Resolvers {
if src.Resolvers[i] == nil { if src.Resolvers[i] == nil {
dst.Resolvers[i] = nil dst.Resolvers[i] = nil
@@ -262,9 +262,9 @@ func (src *DNSConfig) Clone() *DNSConfig {
} }
} }
if dst.Routes != nil { if dst.Routes != nil {
dst.Routes = map[string][]*DNSResolver{} dst.Routes = map[string][]*dnstype.Resolver{}
for k := range src.Routes { for k := range src.Routes {
dst.Routes[k] = append([]*DNSResolver{}, src.Routes[k]...) dst.Routes[k] = append([]*dnstype.Resolver{}, src.Routes[k]...)
} }
} }
if src.FallbackResolvers != nil { if src.FallbackResolvers != nil {
@@ -287,8 +287,8 @@ func (src *DNSConfig) Clone() *DNSConfig {
// A compilation failure here means this code must be regenerated, with the command at the top of this file. // A compilation failure here means this code must be regenerated, with the command at the top of this file.
var _DNSConfigCloneNeedsRegeneration = DNSConfig(struct { var _DNSConfigCloneNeedsRegeneration = DNSConfig(struct {
Resolvers []*DNSResolver Resolvers []*dnstype.Resolver
Routes map[string][]*DNSResolver Routes map[string][]*dnstype.Resolver
FallbackResolvers []*dnstype.Resolver FallbackResolvers []*dnstype.Resolver
Domains []string Domains []string
Proxied bool Proxied bool
@@ -651,27 +651,9 @@ var _VIPServiceCloneNeedsRegeneration = VIPService(struct {
Active bool Active 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.Resolver = *src.Resolver.Clone()
return dst
}
// A compilation failure here means this code must be regenerated, with the command at the top of this file.
var _DNSResolverCloneNeedsRegeneration = DNSResolver(struct {
dnstype.Resolver
UseWithExitNode bool
}{})
// Clone duplicates src into dst and reports whether it succeeded. // Clone duplicates src into dst and reports whether it succeeded.
// To succeed, <src, dst> must be of types <*T, *T> or <*T, **T>, // To succeed, <src, dst> must be of types <*T, *T> or <*T, **T>,
// where T is one of User,Node,Hostinfo,NetInfo,Login,DNSConfig,RegisterResponse,RegisterResponseAuth,RegisterRequest,DERPHomeParams,DERPRegion,DERPMap,DERPNode,SSHRule,SSHAction,SSHPrincipal,ControlDialPlan,Location,UserProfile,VIPService,DNSResolver. // where T is one of User,Node,Hostinfo,NetInfo,Login,DNSConfig,RegisterResponse,RegisterResponseAuth,RegisterRequest,DERPHomeParams,DERPRegion,DERPMap,DERPNode,SSHRule,SSHAction,SSHPrincipal,ControlDialPlan,Location,UserProfile,VIPService.
func Clone(dst, src any) bool { func Clone(dst, src any) bool {
switch src := src.(type) { switch src := src.(type) {
case *User: case *User:
@@ -854,15 +836,6 @@ func Clone(dst, src any) bool {
*dst = src.Clone() *dst = src.Clone()
return true return true
} }
case *DNSResolver:
switch dst := dst.(type) {
case *DNSResolver:
*dst = *src.Clone()
return true
case **DNSResolver:
*dst = src.Clone()
return true
}
} }
return false return false
} }

View File

@@ -19,7 +19,7 @@ import (
"tailscale.com/types/views" "tailscale.com/types/views"
) )
//go:generate go run tailscale.com/cmd/cloner -clonefunc=true -type=User,Node,Hostinfo,NetInfo,Login,DNSConfig,RegisterResponse,RegisterResponseAuth,RegisterRequest,DERPHomeParams,DERPRegion,DERPMap,DERPNode,SSHRule,SSHAction,SSHPrincipal,ControlDialPlan,Location,UserProfile,VIPService,DNSResolver //go:generate go run tailscale.com/cmd/cloner -clonefunc=true -type=User,Node,Hostinfo,NetInfo,Login,DNSConfig,RegisterResponse,RegisterResponseAuth,RegisterRequest,DERPHomeParams,DERPRegion,DERPMap,DERPNode,SSHRule,SSHAction,SSHPrincipal,ControlDialPlan,Location,UserProfile,VIPService
// View returns a read-only view of User. // View returns a read-only view of User.
func (p *User) View() UserView { func (p *User) View() UserView {
@@ -538,13 +538,13 @@ func (v *DNSConfigView) UnmarshalJSON(b []byte) error {
return nil return nil
} }
func (v DNSConfigView) Resolvers() views.SliceView[*DNSResolver, DNSResolverView] { func (v DNSConfigView) Resolvers() views.SliceView[*dnstype.Resolver, dnstype.ResolverView] {
return views.SliceOfViews[*DNSResolver, DNSResolverView](v.ж.Resolvers) return views.SliceOfViews[*dnstype.Resolver, dnstype.ResolverView](v.ж.Resolvers)
} }
func (v DNSConfigView) Routes() views.MapFn[string, []*DNSResolver, views.SliceView[*DNSResolver, DNSResolverView]] { func (v DNSConfigView) Routes() views.MapFn[string, []*dnstype.Resolver, views.SliceView[*dnstype.Resolver, dnstype.ResolverView]] {
return views.MapFnOf(v.ж.Routes, func(t []*DNSResolver) views.SliceView[*DNSResolver, DNSResolverView] { return views.MapFnOf(v.ж.Routes, func(t []*dnstype.Resolver) views.SliceView[*dnstype.Resolver, dnstype.ResolverView] {
return views.SliceOfViews[*DNSResolver, DNSResolverView](t) return views.SliceOfViews[*dnstype.Resolver, dnstype.ResolverView](t)
}) })
} }
func (v DNSConfigView) FallbackResolvers() views.SliceView[*dnstype.Resolver, dnstype.ResolverView] { func (v DNSConfigView) FallbackResolvers() views.SliceView[*dnstype.Resolver, dnstype.ResolverView] {
@@ -562,8 +562,8 @@ func (v DNSConfigView) TempCorpIssue13969() string { return v.ж.TempCorpIssue13
// A compilation failure here means this code must be regenerated, with the command at the top of this file. // A compilation failure here means this code must be regenerated, with the command at the top of this file.
var _DNSConfigViewNeedsRegeneration = DNSConfig(struct { var _DNSConfigViewNeedsRegeneration = DNSConfig(struct {
Resolvers []*DNSResolver Resolvers []*dnstype.Resolver
Routes map[string][]*DNSResolver Routes map[string][]*dnstype.Resolver
FallbackResolvers []*dnstype.Resolver FallbackResolvers []*dnstype.Resolver
Domains []string Domains []string
Proxied bool Proxied bool
@@ -1477,57 +1477,3 @@ var _VIPServiceViewNeedsRegeneration = VIPService(struct {
Ports []ProtoPortRange Ports []ProtoPortRange
Active bool Active bool
}{}) }{})
// View returns a read-only view of DNSResolver.
func (p *DNSResolver) View() DNSResolverView {
return DNSResolverView{ж: p}
}
// DNSResolverView provides a read-only view over DNSResolver.
//
// Its methods should only be called if `Valid()` returns true.
type DNSResolverView struct {
// ж is the underlying mutable value, named with a hard-to-type
// character that looks pointy like a pointer.
// It is named distinctively to make you think of how dangerous it is to escape
// to callers. You must not let callers be able to mutate it.
ж *DNSResolver
}
// Valid reports whether v's underlying value is non-nil.
func (v DNSResolverView) Valid() bool { return v.ж != nil }
// AsStruct returns a clone of the underlying value which aliases no memory with
// the original.
func (v DNSResolverView) AsStruct() *DNSResolver {
if v.ж == nil {
return nil
}
return v.ж.Clone()
}
func (v DNSResolverView) MarshalJSON() ([]byte, error) { return json.Marshal(v.ж) }
func (v *DNSResolverView) UnmarshalJSON(b []byte) error {
if v.ж != nil {
return errors.New("already initialized")
}
if len(b) == 0 {
return nil
}
var x DNSResolver
if err := json.Unmarshal(b, &x); err != nil {
return err
}
v.ж = &x
return nil
}
func (v DNSResolverView) Resolver() dnstype.ResolverView { return v.ж.Resolver.View() }
func (v DNSResolverView) UseWithExitNode() bool { return v.ж.UseWithExitNode }
// A compilation failure here means this code must be regenerated, with the command at the top of this file.
var _DNSResolverViewNeedsRegeneration = DNSResolver(struct {
dnstype.Resolver
UseWithExitNode bool
}{})

View File

@@ -62,10 +62,7 @@ func newHarness(t *testing.T) *Harness {
// TODO: this is wrong. // TODO: this is wrong.
// It is also only one of many configurations. // It is also only one of many configurations.
// Figure out how to scale it up. // Figure out how to scale it up.
Resolvers: []*tailcfg.DNSResolver{ Resolvers: []*dnstype.Resolver{{Addr: "100.100.100.100"}, {Addr: "8.8.8.8"}},
{Resolver: dnstype.Resolver{Addr: "100.100.100.100"}},
{Resolver: dnstype.Resolver{Addr: "8.8.8.8"}},
},
Domains: []string{"record"}, Domains: []string{"record"},
Proxied: true, Proxied: true,
ExtraRecords: []tailcfg.DNSRecord{{Name: "extratest.record", Type: "A", Value: "1.2.3.4"}}, ExtraRecords: []tailcfg.DNSRecord{{Name: "extratest.record", Type: "A", Value: "1.2.3.4"}},

View File

@@ -35,6 +35,12 @@ type Resolver struct {
// //
// As of 2022-09-08, BootstrapResolution is not yet used. // As of 2022-09-08, BootstrapResolution is not yet used.
BootstrapResolution []netip.Addr `json:",omitempty"` 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 // 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 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 { var _ResolverCloneNeedsRegeneration = Resolver(struct {
Addr string Addr string
BootstrapResolution []netip.Addr BootstrapResolution []netip.Addr
UseWithExitNode bool
}{}) }{})
// Clone duplicates src into dst and reports whether it succeeded. // 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) fieldNames = append(fieldNames, field.Name)
} }
sort.Strings(fieldNames) 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") t.Errorf("Resolver fields changed; update test")
} }
@@ -68,6 +68,18 @@ func TestResolverEqual(t *testing.T) {
}, },
want: false, 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 { for _, tt := range tests {

View File

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

View File

@@ -142,8 +142,8 @@ func getVal() *tailscaleTypes {
}, },
}, },
DNSConfig: &tailcfg.DNSConfig{ DNSConfig: &tailcfg.DNSConfig{
Resolvers: []*tailcfg.DNSResolver{ Resolvers: []*dnstype.Resolver{
{Resolver: dnstype.Resolver{Addr: "10.0.0.1"}}, {Addr: "10.0.0.1"},
}, },
}, },
PacketFilter: []tailcfg.FilterRule{ PacketFilter: []tailcfg.FilterRule{