diff --git a/client/tailscale/apitype/controltype.go b/client/tailscale/apitype/controltype.go index 9a623be31..879ddd1de 100644 --- a/client/tailscale/apitype/controltype.go +++ b/client/tailscale/apitype/controltype.go @@ -16,4 +16,5 @@ type DNSConfig struct { type DNSResolver struct { Addr string `json:"addr"` BootstrapResolution []string `json:"bootstrapResolution,omitempty"` + UseWithExitNode bool `json:"useWithExitNode"` } diff --git a/ipn/ipnlocal/dnsconfig_test.go b/ipn/ipnlocal/dnsconfig_test.go index 9aeea2f11..71f175148 100644 --- a/ipn/ipnlocal/dnsconfig_test.go +++ b/ipn/ipnlocal/dnsconfig_test.go @@ -266,14 +266,14 @@ func TestDNSConfigForNetmap(t *testing.T) { os: "android", nm: &netmap.NetworkMap{ DNS: tailcfg.DNSConfig{ - Resolvers: []*tailcfg.DNSResolver{ - {Resolver: dnstype.Resolver{Addr: "8.8.8.8"}}, + Resolvers: []*dnstype.Resolver{ + {Addr: "8.8.8.8"}, }, FallbackResolvers: []*dnstype.Resolver{ {Addr: "8.8.4.4"}, }, - Routes: map[string][]*tailcfg.DNSResolver{ - "foo.com.": {{Resolver: dnstype.Resolver{Addr: "1.2.3.4"}}}, + Routes: map[string][]*dnstype.Resolver{ + "foo.com.": {{Addr: "1.2.3.4"}}, }, }, }, diff --git a/ipn/ipnlocal/local_test.go b/ipn/ipnlocal/local_test.go index a26a7a9e4..49cfc3e07 100644 --- a/ipn/ipnlocal/local_test.go +++ b/ipn/ipnlocal/local_test.go @@ -2084,19 +2084,10 @@ func TestDNSConfigForNetmapForExitNodeConfigs(t *testing.T) { defaultResolvers := []*dnstype.Resolver{ {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"}} peers := []tailcfg.NodeView{ (&tailcfg.Node{ @@ -2124,39 +2115,31 @@ func TestDNSConfigForNetmapForExitNodeConfigs(t *testing.T) { } containsFlaggedRoutes := map[dnsname.FQDN][]*dnstype.Resolver{ "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{ "empty.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{ - "withexit.example.com.": {{Addr: tsUseWithExitNodeResolverAddr}}, + "withexit.example.com.": {{Addr: tsUseWithExitNodeResolverAddr, UseWithExitNode: true}}, } emptyRoutes := map[dnsname.FQDN][]*dnstype.Resolver{ "empty.example.com.": {}, } flaggedAndEmptyRoutes := map[dnsname.FQDN][]*dnstype.Resolver{ "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 { return nil } - m := make(map[string][]*tailcfg.DNSResolver) + m := make(map[string][]*dnstype.Resolver) for k, v := range routes { - var rr []*tailcfg.DNSResolver - for _, res := range v { - wrapped := &tailcfg.DNSResolver{Resolver: *res} - if res.Addr == tsUseWithExitNodeResolverAddr { - wrapped.UseWithExitNode = true - } - rr = append(rr, wrapped) - } - m[string(k)] = rr + m[string(k)] = v } return m } @@ -2182,7 +2165,7 @@ func TestDNSConfigForNetmapForExitNodeConfigs(t *testing.T) { name: "tsExit/noRoutes/defaultResolver", exitNode: "ts", peers: peers, - dnsConfig: &tailcfg.DNSConfig{Resolvers: wrapResolvers(defaultResolvers)}, + dnsConfig: &tailcfg.DNSConfig{Resolvers: defaultResolvers}, wantDefaultResolvers: []*dnstype.Resolver{{Addr: exitDOH}}, wantRoutes: nil, }, @@ -2190,8 +2173,8 @@ func TestDNSConfigForNetmapForExitNodeConfigs(t *testing.T) { name: "tsExit/noRoutes/flaggedResolverOnly", exitNode: "ts", peers: peers, - dnsConfig: &tailcfg.DNSConfig{Resolvers: wrapResolvers(containsFlaggedResolvers)}, - wantDefaultResolvers: []*dnstype.Resolver{{Addr: tsUseWithExitNodeResolverAddr}}, + dnsConfig: &tailcfg.DNSConfig{Resolvers: containsFlaggedResolvers}, + wantDefaultResolvers: []*dnstype.Resolver{{Addr: tsUseWithExitNodeResolverAddr, UseWithExitNode: true}}, wantRoutes: nil, }, @@ -2210,7 +2193,7 @@ func TestDNSConfigForNetmapForExitNodeConfigs(t *testing.T) { name: "tsExit/routes/defaultResolver", exitNode: "ts", peers: peers, - dnsConfig: &tailcfg.DNSConfig{Routes: stringifyRoutes(baseRoutes), Resolvers: wrapResolvers(defaultResolvers)}, + dnsConfig: &tailcfg.DNSConfig{Routes: stringifyRoutes(baseRoutes), Resolvers: defaultResolvers}, wantDefaultResolvers: []*dnstype.Resolver{{Addr: exitDOH}}, wantRoutes: nil, }, @@ -2218,15 +2201,15 @@ func TestDNSConfigForNetmapForExitNodeConfigs(t *testing.T) { name: "tsExit/routes/flaggedResolverOnly", exitNode: "ts", peers: peers, - dnsConfig: &tailcfg.DNSConfig{Routes: stringifyRoutes(baseRoutes), Resolvers: wrapResolvers(containsFlaggedResolvers)}, - wantDefaultResolvers: []*dnstype.Resolver{{Addr: tsUseWithExitNodeResolverAddr}}, + dnsConfig: &tailcfg.DNSConfig{Routes: stringifyRoutes(baseRoutes), Resolvers: containsFlaggedResolvers}, + wantDefaultResolvers: []*dnstype.Resolver{{Addr: tsUseWithExitNodeResolverAddr, UseWithExitNode: true}}, wantRoutes: nil, }, { name: "tsExit/flaggedRoutesOnly/defaultResolver", exitNode: "ts", peers: peers, - dnsConfig: &tailcfg.DNSConfig{Routes: stringifyRoutes(containsFlaggedRoutes), Resolvers: wrapResolvers(defaultResolvers)}, + dnsConfig: &tailcfg.DNSConfig{Routes: stringifyRoutes(containsFlaggedRoutes), Resolvers: defaultResolvers}, wantDefaultResolvers: []*dnstype.Resolver{{Addr: exitDOH}}, wantRoutes: flaggedRoutes, }, @@ -2234,15 +2217,15 @@ func TestDNSConfigForNetmapForExitNodeConfigs(t *testing.T) { name: "tsExit/flaggedRoutesOnly/flaggedResolverOnly", exitNode: "ts", peers: peers, - dnsConfig: &tailcfg.DNSConfig{Routes: stringifyRoutes(containsFlaggedRoutes), Resolvers: wrapResolvers(containsFlaggedResolvers)}, - wantDefaultResolvers: []*dnstype.Resolver{{Addr: tsUseWithExitNodeResolverAddr}}, + dnsConfig: &tailcfg.DNSConfig{Routes: stringifyRoutes(containsFlaggedRoutes), Resolvers: containsFlaggedResolvers}, + wantDefaultResolvers: []*dnstype.Resolver{{Addr: tsUseWithExitNodeResolverAddr, UseWithExitNode: true}}, wantRoutes: flaggedRoutes, }, { name: "tsExit/emptyRoutesOnly/defaultResolver", exitNode: "ts", peers: peers, - dnsConfig: &tailcfg.DNSConfig{Routes: stringifyRoutes(containsEmptyRoutes), Resolvers: wrapResolvers(defaultResolvers)}, + dnsConfig: &tailcfg.DNSConfig{Routes: stringifyRoutes(containsEmptyRoutes), Resolvers: defaultResolvers}, wantDefaultResolvers: []*dnstype.Resolver{{Addr: exitDOH}}, wantRoutes: emptyRoutes, }, @@ -2250,7 +2233,7 @@ func TestDNSConfigForNetmapForExitNodeConfigs(t *testing.T) { name: "tsExit/flaggedAndEmptyRoutesOnly/defaultResolver", exitNode: "ts", peers: peers, - dnsConfig: &tailcfg.DNSConfig{Routes: stringifyRoutes(containsFlaggedAndEmptyRoutes), Resolvers: wrapResolvers(defaultResolvers)}, + dnsConfig: &tailcfg.DNSConfig{Routes: stringifyRoutes(containsFlaggedAndEmptyRoutes), Resolvers: defaultResolvers}, wantDefaultResolvers: []*dnstype.Resolver{{Addr: exitDOH}}, wantRoutes: flaggedAndEmptyRoutes, }, @@ -2258,8 +2241,8 @@ func TestDNSConfigForNetmapForExitNodeConfigs(t *testing.T) { name: "tsExit/flaggedAndEmptyRoutesOnly/flaggedResolverOnly", exitNode: "ts", peers: peers, - dnsConfig: &tailcfg.DNSConfig{Routes: stringifyRoutes(containsFlaggedAndEmptyRoutes), Resolvers: wrapResolvers(containsFlaggedResolvers)}, - wantDefaultResolvers: []*dnstype.Resolver{{Addr: tsUseWithExitNodeResolverAddr}}, + dnsConfig: &tailcfg.DNSConfig{Routes: stringifyRoutes(containsFlaggedAndEmptyRoutes), Resolvers: containsFlaggedResolvers}, + wantDefaultResolvers: []*dnstype.Resolver{{Addr: tsUseWithExitNodeResolverAddr, UseWithExitNode: true}}, wantRoutes: flaggedAndEmptyRoutes, }, @@ -2279,7 +2262,7 @@ func TestDNSConfigForNetmapForExitNodeConfigs(t *testing.T) { name: "wgExit/noRoutes/defaultResolver", exitNode: "wg", peers: peers, - dnsConfig: &tailcfg.DNSConfig{Resolvers: wrapResolvers(defaultResolvers)}, + dnsConfig: &tailcfg.DNSConfig{Resolvers: defaultResolvers}, wantDefaultResolvers: defaultResolvers, wantRoutes: nil, }, @@ -2287,7 +2270,7 @@ func TestDNSConfigForNetmapForExitNodeConfigs(t *testing.T) { name: "wgExit/routes/defaultResolver", exitNode: "wg", peers: peers, - dnsConfig: &tailcfg.DNSConfig{Routes: stringifyRoutes(baseRoutes), Resolvers: wrapResolvers(defaultResolvers)}, + dnsConfig: &tailcfg.DNSConfig{Routes: stringifyRoutes(baseRoutes), Resolvers: defaultResolvers}, wantDefaultResolvers: defaultResolvers, wantRoutes: baseRoutes, }, diff --git a/ipn/ipnlocal/node_backend.go b/ipn/ipnlocal/node_backend.go index aec84ec62..b51330c76 100644 --- a/ipn/ipnlocal/node_backend.go +++ b/ipn/ipnlocal/node_backend.go @@ -578,59 +578,40 @@ func (nb *nodeBackend) doShutdown(cause error) { nb.eventClient.Close() } -func useWithExitNodeResolvers(dc tailcfg.DNSConfig) []*dnstype.Resolver { - return convertResolvers(dc.Resolvers, true) -} - -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)) +// useWithExitNodeResolvers filters out resolvers so the ones that remain +// are all the ones marked for use with exit nodes. +func useWithExitNodeResolvers(resolvers []*dnstype.Resolver) []*dnstype.Resolver { + filtered := make([]*dnstype.Resolver, 0, len(resolvers)) for _, res := range resolvers { - // If not using an exit node, all resolvers persist. - // Otherwise, check if the resolver is marked for use with exit node. - if !usingExitNode || res.UseWithExitNode { - converted = append(converted, &res.Resolver) + if res.UseWithExitNode { + filtered = append(filtered, res) } } - return converted + return filtered } -// convertRoutes converts tailcfg dns routes containing tailcfg dns resolvers -// to a map of routes containing dnstype resolvers, for use in the wireguard engine, -// taking into account exit node contexts. -func convertRoutes(routes map[string][]*tailcfg.DNSResolver, usingExitNode bool) map[string][]*dnstype.Resolver { - converted := make(map[string][]*dnstype.Resolver) +// useWithExitNodeRoutes filters out routes so the ones that remain +// are either zero-length resolver lists, or lists containing only +// resolvers marked for use with exit nodes. +func useWithExitNodeRoutes(routes map[string][]*dnstype.Resolver) map[string][]*dnstype.Resolver { + filtered := make(map[string][]*dnstype.Resolver) for suffix, resolvers := range routes { // Suffixes with no resolvers represent a valid configuration, // and should persist regardless of exit node considerations. if len(resolvers) == 0 { - converted[suffix] = make([]*dnstype.Resolver, 0) + filtered[suffix] = make([]*dnstype.Resolver, 0) continue } // 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. - convertedResolvers := convertResolvers(resolvers, usingExitNode) - if len(convertedResolvers) > 0 { - converted[suffix] = convertedResolvers + filteredResolvers := useWithExitNodeResolvers(resolvers) + if len(filteredResolvers) > 0 { + filtered[suffix] = filteredResolvers } } - return converted + return filtered } // 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, // unless we find resolvers with UseWithExitNode set, in which case we use that. if dohURL, ok := exitNodeCanProxyDNS(nm, peers, prefs.ExitNodeID()); ok { - filtered := useWithExitNodeResolvers(nm.DNS) + filtered := useWithExitNodeResolvers(nm.DNS.Resolvers) if len(filtered) > 0 { addDefault(filtered) } else { @@ -790,7 +771,7 @@ func dnsConfigForNetmap(nm *netmap.NetworkMap, peers map[tailcfg.NodeID]tailcfg. addDefault([]*dnstype.Resolver{{Addr: dohURL}}) } - addSplitDNSRoutes(useWithExitNodeRoutes(nm.DNS)) + addSplitDNSRoutes(useWithExitNodeRoutes(nm.DNS.Routes)) 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 // node resolvers, use those as the default. if len(nm.DNS.Resolvers) > 0 { - addDefault(resolvers(nm.DNS)) + addDefault(nm.DNS.Resolvers) } else { if resolvers, ok := wireguardExitNodeDNSResolvers(nm, peers, prefs.ExitNodeID()); ok { 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. - addSplitDNSRoutes(routes(nm.DNS)) + addSplitDNSRoutes(nm.DNS.Routes) // Set FallbackResolvers as the default resolvers in the // scenarios that can't handle a purely split-DNS config. See diff --git a/tailcfg/tailcfg.go b/tailcfg/tailcfg.go index 884564346..8ab0a5baf 100644 --- a/tailcfg/tailcfg.go +++ b/tailcfg/tailcfg.go @@ -5,7 +5,7 @@ // the node and the coordination server. 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 ( "bytes" @@ -168,7 +168,7 @@ type CapabilityVersion int // - 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. // - 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 // 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. type DNSConfig struct { // 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 // 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 // be handled by Tailscale's built-in resolver (100.100.100.100), such // 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 // split DNS configuration is requested in a configuration that diff --git a/tailcfg/tailcfg_clone.go b/tailcfg/tailcfg_clone.go index 07fe4f979..95f8905b8 100644 --- a/tailcfg/tailcfg_clone.go +++ b/tailcfg/tailcfg_clone.go @@ -252,7 +252,7 @@ func (src *DNSConfig) Clone() *DNSConfig { dst := new(DNSConfig) *dst = *src if src.Resolvers != nil { - dst.Resolvers = make([]*DNSResolver, len(src.Resolvers)) + dst.Resolvers = make([]*dnstype.Resolver, len(src.Resolvers)) for i := range dst.Resolvers { if src.Resolvers[i] == nil { dst.Resolvers[i] = nil @@ -262,9 +262,9 @@ func (src *DNSConfig) Clone() *DNSConfig { } } if dst.Routes != nil { - dst.Routes = map[string][]*DNSResolver{} + dst.Routes = map[string][]*dnstype.Resolver{} 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 { @@ -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. var _DNSConfigCloneNeedsRegeneration = DNSConfig(struct { - Resolvers []*DNSResolver - Routes map[string][]*DNSResolver + Resolvers []*dnstype.Resolver + Routes map[string][]*dnstype.Resolver FallbackResolvers []*dnstype.Resolver Domains []string Proxied bool @@ -651,27 +651,9 @@ var _VIPServiceCloneNeedsRegeneration = VIPService(struct { 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. // To succeed, 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 { switch src := src.(type) { case *User: @@ -854,15 +836,6 @@ func Clone(dst, src any) bool { *dst = src.Clone() return true } - case *DNSResolver: - switch dst := dst.(type) { - case *DNSResolver: - *dst = *src.Clone() - return true - case **DNSResolver: - *dst = src.Clone() - return true - } } return false } diff --git a/tailcfg/tailcfg_view.go b/tailcfg/tailcfg_view.go index 7a41da92d..c40780021 100644 --- a/tailcfg/tailcfg_view.go +++ b/tailcfg/tailcfg_view.go @@ -19,7 +19,7 @@ import ( "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. func (p *User) View() UserView { @@ -538,13 +538,13 @@ func (v *DNSConfigView) UnmarshalJSON(b []byte) error { return nil } -func (v DNSConfigView) Resolvers() views.SliceView[*DNSResolver, DNSResolverView] { - return views.SliceOfViews[*DNSResolver, DNSResolverView](v.ж.Resolvers) +func (v DNSConfigView) Resolvers() views.SliceView[*dnstype.Resolver, dnstype.ResolverView] { + return views.SliceOfViews[*dnstype.Resolver, dnstype.ResolverView](v.ж.Resolvers) } -func (v DNSConfigView) Routes() views.MapFn[string, []*DNSResolver, views.SliceView[*DNSResolver, DNSResolverView]] { - return views.MapFnOf(v.ж.Routes, func(t []*DNSResolver) views.SliceView[*DNSResolver, DNSResolverView] { - return views.SliceOfViews[*DNSResolver, DNSResolverView](t) +func (v DNSConfigView) Routes() views.MapFn[string, []*dnstype.Resolver, views.SliceView[*dnstype.Resolver, dnstype.ResolverView]] { + return views.MapFnOf(v.ж.Routes, func(t []*dnstype.Resolver) views.SliceView[*dnstype.Resolver, dnstype.ResolverView] { + return views.SliceOfViews[*dnstype.Resolver, dnstype.ResolverView](t) }) } 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. var _DNSConfigViewNeedsRegeneration = DNSConfig(struct { - Resolvers []*DNSResolver - Routes map[string][]*DNSResolver + Resolvers []*dnstype.Resolver + Routes map[string][]*dnstype.Resolver FallbackResolvers []*dnstype.Resolver Domains []string Proxied bool @@ -1477,57 +1477,3 @@ var _VIPServiceViewNeedsRegeneration = VIPService(struct { Ports []ProtoPortRange 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 -}{}) diff --git a/tstest/integration/vms/harness_test.go b/tstest/integration/vms/harness_test.go index 3eec1a9bf..256227d6c 100644 --- a/tstest/integration/vms/harness_test.go +++ b/tstest/integration/vms/harness_test.go @@ -62,10 +62,7 @@ func newHarness(t *testing.T) *Harness { // TODO: this is wrong. // It is also only one of many configurations. // Figure out how to scale it up. - Resolvers: []*tailcfg.DNSResolver{ - {Resolver: dnstype.Resolver{Addr: "100.100.100.100"}}, - {Resolver: dnstype.Resolver{Addr: "8.8.8.8"}}, - }, + Resolvers: []*dnstype.Resolver{{Addr: "100.100.100.100"}, {Addr: "8.8.8.8"}}, Domains: []string{"record"}, Proxied: true, ExtraRecords: []tailcfg.DNSRecord{{Name: "extratest.record", Type: "A", Value: "1.2.3.4"}}, diff --git a/types/dnstype/dnstype.go b/types/dnstype/dnstype.go index b7f5b9d02..a3ba1b0a9 100644 --- a/types/dnstype/dnstype.go +++ b/types/dnstype/dnstype.go @@ -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 } diff --git a/types/dnstype/dnstype_clone.go b/types/dnstype/dnstype_clone.go index 86ca0535f..3985704aa 100644 --- a/types/dnstype/dnstype_clone.go +++ b/types/dnstype/dnstype_clone.go @@ -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. diff --git a/types/dnstype/dnstype_test.go b/types/dnstype/dnstype_test.go index e3a941a20..ada5f687d 100644 --- a/types/dnstype/dnstype_test.go +++ b/types/dnstype/dnstype_test.go @@ -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 { diff --git a/types/dnstype/dnstype_view.go b/types/dnstype/dnstype_view.go index c77ff9a40..8372ca635 100644 --- a/types/dnstype/dnstype_view.go +++ b/types/dnstype/dnstype_view.go @@ -64,10 +64,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 }{}) diff --git a/util/deephash/tailscale_types_test.go b/util/deephash/tailscale_types_test.go index fb2a20d1f..d76025399 100644 --- a/util/deephash/tailscale_types_test.go +++ b/util/deephash/tailscale_types_test.go @@ -142,8 +142,8 @@ func getVal() *tailscaleTypes { }, }, DNSConfig: &tailcfg.DNSConfig{ - Resolvers: []*tailcfg.DNSResolver{ - {Resolver: dnstype.Resolver{Addr: "10.0.0.1"}}, + Resolvers: []*dnstype.Resolver{ + {Addr: "10.0.0.1"}, }, }, PacketFilter: []tailcfg.FilterRule{