diff --git a/ipn/ipnlocal/dnsconfig_test.go b/ipn/ipnlocal/dnsconfig_test.go index 71f175148..9aeea2f11 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: []*dnstype.Resolver{ - {Addr: "8.8.8.8"}, + Resolvers: []*tailcfg.DNSResolver{ + {Resolver: dnstype.Resolver{Addr: "8.8.8.8"}}, }, FallbackResolvers: []*dnstype.Resolver{ {Addr: "8.8.4.4"}, }, - Routes: map[string][]*dnstype.Resolver{ - "foo.com.": {{Addr: "1.2.3.4"}}, + Routes: map[string][]*tailcfg.DNSResolver{ + "foo.com.": {{Resolver: dnstype.Resolver{Addr: "1.2.3.4"}}}, }, }, }, diff --git a/ipn/ipnlocal/local_test.go b/ipn/ipnlocal/local_test.go index 30833e748..5cfe9d01c 100644 --- a/ipn/ipnlocal/local_test.go +++ b/ipn/ipnlocal/local_test.go @@ -2080,7 +2080,23 @@ func TestDNSConfigForNetmapForExitNodeConfigs(t *testing.T) { wantRoutes map[dnsname.FQDN][]*dnstype.Resolver } - defaultResolvers := []*dnstype.Resolver{{Addr: "default.example.com"}} + const tsUseWithExitNodeResolverAddr = "usewithexitnode.example.com" + defaultResolvers := []*dnstype.Resolver{ + {Addr: "default.example.com"}, + } + containsFlaggedResolvers := append([]*dnstype.Resolver{{Addr: tsUseWithExitNodeResolverAddr}}, 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{ @@ -2099,16 +2115,51 @@ func TestDNSConfigForNetmapForExitNodeConfigs(t *testing.T) { }).View(), } exitDOH := peerAPIBase(&netmap.NetworkMap{Peers: peers}, peers[0]) + "/dns-query" - routes := map[dnsname.FQDN][]*dnstype.Resolver{ + baseRoutes := map[dnsname.FQDN][]*dnstype.Resolver{ "route.example.com.": {{Addr: "route.example.com"}}, } - stringifyRoutes := func(routes map[dnsname.FQDN][]*dnstype.Resolver) map[string][]*dnstype.Resolver { + containsEmptyRoutes := map[dnsname.FQDN][]*dnstype.Resolver{ + "empty.example.com.": []*dnstype.Resolver{}, + "route.example.com.": {{Addr: "route.example.com"}}, + } + _ = containsEmptyRoutes + containsFlaggedRoutes := map[dnsname.FQDN][]*dnstype.Resolver{ + "route.example.com.": {{Addr: "route.example.com"}}, + "withexit.example.com.": {{Addr: tsUseWithExitNodeResolverAddr}}, + } + containsFlaggedAndEmptyRoutes := map[dnsname.FQDN][]*dnstype.Resolver{ + "empty.example.com.": []*dnstype.Resolver{}, + "route.example.com.": {{Addr: "route.example.com"}}, + "withexit.example.com.": {{Addr: tsUseWithExitNodeResolverAddr}}, + } + _ = containsFlaggedAndEmptyRoutes + flaggedRoutes := map[dnsname.FQDN][]*dnstype.Resolver{ + "withexit.example.com.": {{Addr: tsUseWithExitNodeResolverAddr}}, + } + emptyRoutes := map[dnsname.FQDN][]*dnstype.Resolver{ + "empty.example.com.": []*dnstype.Resolver{}, + } + flaggedAndEmptyRoutes := map[dnsname.FQDN][]*dnstype.Resolver{ + "empty.example.com.": []*dnstype.Resolver{}, + "withexit.example.com.": {{Addr: tsUseWithExitNodeResolverAddr}}, + } + _ = flaggedAndEmptyRoutes + + stringifyRoutes := func(routes map[dnsname.FQDN][]*dnstype.Resolver) map[string][]*tailcfg.DNSResolver { if routes == nil { return nil } - m := make(map[string][]*dnstype.Resolver) + m := make(map[string][]*tailcfg.DNSResolver) for k, v := range routes { - m[string(k)] = v + 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 } return m } @@ -2134,23 +2185,27 @@ func TestDNSConfigForNetmapForExitNodeConfigs(t *testing.T) { name: "tsExit/noRoutes/defaultResolver", exitNode: "ts", peers: peers, - dnsConfig: &tailcfg.DNSConfig{Resolvers: defaultResolvers}, + dnsConfig: &tailcfg.DNSConfig{Resolvers: wrapResolvers(defaultResolvers)}, wantDefaultResolvers: []*dnstype.Resolver{{Addr: exitDOH}}, wantRoutes: nil, }, + { + name: "tsExit/noRoutes/flaggedResolverOnly", + exitNode: "ts", + peers: peers, + dnsConfig: &tailcfg.DNSConfig{Resolvers: wrapResolvers(containsFlaggedResolvers)}, + wantDefaultResolvers: []*dnstype.Resolver{{Addr: tsUseWithExitNodeResolverAddr}}, + wantRoutes: nil, + }, - // The following two cases may need to be revisited. For a shared-in - // exit node split-DNS may effectively break, furthermore in the future - // if different nodes observe different DNS configurations, even a - // tailnet local exit node may present a different DNS configuration, - // which may not meet expectations in some use cases. - // In the case where a default resolver is set, the default resolver - // should also perhaps take precedence also. + // When at tailscale exit node is in use, + // only routes that reference resolvers with the UseWithExitNode should be installed, + // as well as routes with 0-length resolver lists, which should be installed in all cases. { name: "tsExit/routes/noResolver", exitNode: "ts", peers: peers, - dnsConfig: &tailcfg.DNSConfig{Routes: stringifyRoutes(routes)}, + dnsConfig: &tailcfg.DNSConfig{Routes: stringifyRoutes(baseRoutes)}, wantDefaultResolvers: []*dnstype.Resolver{{Addr: exitDOH}}, wantRoutes: nil, }, @@ -2158,10 +2213,58 @@ func TestDNSConfigForNetmapForExitNodeConfigs(t *testing.T) { name: "tsExit/routes/defaultResolver", exitNode: "ts", peers: peers, - dnsConfig: &tailcfg.DNSConfig{Routes: stringifyRoutes(routes), Resolvers: defaultResolvers}, + dnsConfig: &tailcfg.DNSConfig{Routes: stringifyRoutes(baseRoutes), Resolvers: wrapResolvers(defaultResolvers)}, wantDefaultResolvers: []*dnstype.Resolver{{Addr: exitDOH}}, wantRoutes: nil, }, + { + name: "tsExit/routes/flaggedResolverOnly", + exitNode: "ts", + peers: peers, + dnsConfig: &tailcfg.DNSConfig{Routes: stringifyRoutes(baseRoutes), Resolvers: wrapResolvers(containsFlaggedResolvers)}, + wantDefaultResolvers: []*dnstype.Resolver{{Addr: tsUseWithExitNodeResolverAddr}}, + wantRoutes: nil, + }, + { + name: "tsExit/flaggedRoutesOnly/defaultResolver", + exitNode: "ts", + peers: peers, + dnsConfig: &tailcfg.DNSConfig{Routes: stringifyRoutes(containsFlaggedRoutes), Resolvers: wrapResolvers(defaultResolvers)}, + wantDefaultResolvers: []*dnstype.Resolver{{Addr: exitDOH}}, + wantRoutes: flaggedRoutes, + }, + { + name: "tsExit/flaggedRoutesOnly/flaggedResolverOnly", + exitNode: "ts", + peers: peers, + dnsConfig: &tailcfg.DNSConfig{Routes: stringifyRoutes(containsFlaggedRoutes), Resolvers: wrapResolvers(containsFlaggedResolvers)}, + wantDefaultResolvers: []*dnstype.Resolver{{Addr: tsUseWithExitNodeResolverAddr}}, + wantRoutes: flaggedRoutes, + }, + { + name: "tsExit/emptyRoutesOnly/defaultResolver", + exitNode: "ts", + peers: peers, + dnsConfig: &tailcfg.DNSConfig{Routes: stringifyRoutes(containsEmptyRoutes), Resolvers: wrapResolvers(defaultResolvers)}, + wantDefaultResolvers: []*dnstype.Resolver{{Addr: exitDOH}}, + wantRoutes: emptyRoutes, + }, + { + name: "tsExit/flaggedAndEmptyRoutesOnly/defaultResolver", + exitNode: "ts", + peers: peers, + dnsConfig: &tailcfg.DNSConfig{Routes: stringifyRoutes(containsFlaggedAndEmptyRoutes), Resolvers: wrapResolvers(defaultResolvers)}, + wantDefaultResolvers: []*dnstype.Resolver{{Addr: exitDOH}}, + wantRoutes: flaggedAndEmptyRoutes, + }, + { + name: "tsExit/flaggedAndEmptyRoutesOnly/flaggedResolverOnly", + exitNode: "ts", + peers: peers, + dnsConfig: &tailcfg.DNSConfig{Routes: stringifyRoutes(containsFlaggedAndEmptyRoutes), Resolvers: wrapResolvers(containsFlaggedResolvers)}, + wantDefaultResolvers: []*dnstype.Resolver{{Addr: tsUseWithExitNodeResolverAddr}}, + wantRoutes: flaggedAndEmptyRoutes, + }, // WireGuard exit nodes with DNS capabilities provide a "fallback" type // behavior, they have a lower precedence than a default resolver, but @@ -2179,7 +2282,7 @@ func TestDNSConfigForNetmapForExitNodeConfigs(t *testing.T) { name: "wgExit/noRoutes/defaultResolver", exitNode: "wg", peers: peers, - dnsConfig: &tailcfg.DNSConfig{Resolvers: defaultResolvers}, + dnsConfig: &tailcfg.DNSConfig{Resolvers: wrapResolvers(defaultResolvers)}, wantDefaultResolvers: defaultResolvers, wantRoutes: nil, }, @@ -2187,17 +2290,17 @@ func TestDNSConfigForNetmapForExitNodeConfigs(t *testing.T) { name: "wgExit/routes/defaultResolver", exitNode: "wg", peers: peers, - dnsConfig: &tailcfg.DNSConfig{Routes: stringifyRoutes(routes), Resolvers: defaultResolvers}, + dnsConfig: &tailcfg.DNSConfig{Routes: stringifyRoutes(baseRoutes), Resolvers: wrapResolvers(defaultResolvers)}, wantDefaultResolvers: defaultResolvers, - wantRoutes: routes, + wantRoutes: baseRoutes, }, { name: "wgExit/routes/noResolver", exitNode: "wg", peers: peers, - dnsConfig: &tailcfg.DNSConfig{Routes: stringifyRoutes(routes)}, + dnsConfig: &tailcfg.DNSConfig{Routes: stringifyRoutes(baseRoutes)}, wantDefaultResolvers: wgResolvers, - wantRoutes: routes, + wantRoutes: baseRoutes, }, } diff --git a/ipn/ipnlocal/node_backend.go b/ipn/ipnlocal/node_backend.go index ec503f130..aec84ec62 100644 --- a/ipn/ipnlocal/node_backend.go +++ b/ipn/ipnlocal/node_backend.go @@ -578,6 +578,61 @@ 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)) + 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) + } + } + return converted +} + +// 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) + 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) + 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 + } + } + + return converted +} + // dnsConfigForNetmap returns a *dns.Config for the given netmap, // prefs, client OS version, and cloud hosting environment. // @@ -700,10 +755,42 @@ func dnsConfigForNetmap(nm *netmap.NetworkMap, peers map[tailcfg.NodeID]tailcfg. dcfg.DefaultResolvers = append(dcfg.DefaultResolvers, resolvers...) } + addSplitDNSRoutes := func(routes map[string][]*dnstype.Resolver) { + for suffix, resolvers := range routes { + fqdn, err := dnsname.ToFQDN(suffix) + if err != nil { + logf("[unexpected] non-FQDN route suffix %q", suffix) + } + + // Create map entry even if len(resolvers) == 0; Issue 2706. + // This lets the control plane send ExtraRecords for which we + // can authoritatively answer "name not exists" for when the + // control plane also sends this explicit but empty route + // making it as something we handle. + // + // While we're already populating it, might as well size the + // slice appropriately. + // Per #9498 the exact requirements of nil vs empty slice remain + // unclear, this is a haunted graveyard to be resolved. + dcfg.Routes[fqdn] = make([]*dnstype.Resolver, 0, len(resolvers)) + dcfg.Routes[fqdn] = append(dcfg.Routes[fqdn], resolvers...) + } + } + // If we're using an exit node and that exit node is new enough (1.19.x+) - // 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. if dohURL, ok := exitNodeCanProxyDNS(nm, peers, prefs.ExitNodeID()); ok { - addDefault([]*dnstype.Resolver{{Addr: dohURL}}) + filtered := useWithExitNodeResolvers(nm.DNS) + if len(filtered) > 0 { + addDefault(filtered) + } else { + // If no default global resolvers with the override + // are configured, configure the exit node's resolver. + addDefault([]*dnstype.Resolver{{Addr: dohURL}}) + } + + addSplitDNSRoutes(useWithExitNodeRoutes(nm.DNS)) return dcfg } @@ -711,32 +798,15 @@ 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(nm.DNS.Resolvers) + addDefault(resolvers(nm.DNS)) } else { if resolvers, ok := wireguardExitNodeDNSResolvers(nm, peers, prefs.ExitNodeID()); ok { addDefault(resolvers) } } - for suffix, resolvers := range nm.DNS.Routes { - fqdn, err := dnsname.ToFQDN(suffix) - if err != nil { - logf("[unexpected] non-FQDN route suffix %q", suffix) - } - - // Create map entry even if len(resolvers) == 0; Issue 2706. - // This lets the control plane send ExtraRecords for which we - // can authoritatively answer "name not exists" for when the - // control plane also sends this explicit but empty route - // making it as something we handle. - // - // While we're already populating it, might as well size the - // slice appropriately. - // Per #9498 the exact requirements of nil vs empty slice remain - // unclear, this is a haunted graveyard to be resolved. - dcfg.Routes[fqdn] = make([]*dnstype.Resolver, 0, len(resolvers)) - dcfg.Routes[fqdn] = append(dcfg.Routes[fqdn], resolvers...) - } + // Add split DNS routes, with no regard to exit node configuration. + addSplitDNSRoutes(routes(nm.DNS)) // 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 5e3c4e572..f530d97dd 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 --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,DNSResolver --clonefunc import ( "bytes" @@ -168,7 +168,8 @@ 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) -const CurrentCapabilityVersion CapabilityVersion = 123 +// - 124: 2025-08-07: DNSConfig.{Resolvers,Routes} are re-typed to collections of DNSResolver instead of dnstype.Resolver. +const CurrentCapabilityVersion CapabilityVersion = 124 // ID is an integer ID for a user, node, or login allocated by the // control plane. @@ -1698,10 +1699,17 @@ var FilterAllowAll = []FilterRule{ }, } +// DNSResolver embeds dnstype.Resolver and stores +// additional configuration. +type DNSResolver struct { + dnstype.Resolver `json:",omitempty"` + UseWithExitNode bool `json:",omitempty"` +} + // DNSConfig is the DNS configuration. type DNSConfig struct { // Resolvers are the DNS resolvers to use, in order of preference. - Resolvers []*dnstype.Resolver `json:",omitempty"` + Resolvers []*DNSResolver `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 @@ -1713,7 +1721,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][]*dnstype.Resolver `json:",omitempty"` + Routes map[string][]*DNSResolver `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 95f8905b8..07fe4f979 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([]*dnstype.Resolver, len(src.Resolvers)) + dst.Resolvers = make([]*DNSResolver, 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][]*dnstype.Resolver{} + dst.Routes = map[string][]*DNSResolver{} for k := range src.Routes { - dst.Routes[k] = append([]*dnstype.Resolver{}, src.Routes[k]...) + dst.Routes[k] = append([]*DNSResolver{}, 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 []*dnstype.Resolver - Routes map[string][]*dnstype.Resolver + Resolvers []*DNSResolver + Routes map[string][]*DNSResolver FallbackResolvers []*dnstype.Resolver Domains []string Proxied bool @@ -651,9 +651,27 @@ 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. +// 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. func Clone(dst, src any) bool { switch src := src.(type) { case *User: @@ -836,6 +854,15 @@ 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 c40780021..7a41da92d 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 +//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 // 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[*dnstype.Resolver, dnstype.ResolverView] { - return views.SliceOfViews[*dnstype.Resolver, dnstype.ResolverView](v.ж.Resolvers) +func (v DNSConfigView) Resolvers() views.SliceView[*DNSResolver, DNSResolverView] { + return views.SliceOfViews[*DNSResolver, DNSResolverView](v.ж.Resolvers) } -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) 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) 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 []*dnstype.Resolver - Routes map[string][]*dnstype.Resolver + Resolvers []*DNSResolver + Routes map[string][]*DNSResolver FallbackResolvers []*dnstype.Resolver Domains []string Proxied bool @@ -1477,3 +1477,57 @@ 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 256227d6c..3eec1a9bf 100644 --- a/tstest/integration/vms/harness_test.go +++ b/tstest/integration/vms/harness_test.go @@ -62,7 +62,10 @@ 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: []*dnstype.Resolver{{Addr: "100.100.100.100"}, {Addr: "8.8.8.8"}}, + Resolvers: []*tailcfg.DNSResolver{ + {Resolver: dnstype.Resolver{Addr: "100.100.100.100"}}, + {Resolver: dnstype.Resolver{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/util/deephash/tailscale_types_test.go b/util/deephash/tailscale_types_test.go index d76025399..fb2a20d1f 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: []*dnstype.Resolver{ - {Addr: "10.0.0.1"}, + Resolvers: []*tailcfg.DNSResolver{ + {Resolver: dnstype.Resolver{Addr: "10.0.0.1"}}, }, }, PacketFilter: []tailcfg.FilterRule{