mirror of
https://github.com/tailscale/tailscale.git
synced 2025-08-13 22:47:30 +00:00
tailcfg, ipnlocal: tailcfg.DNSResolver controls exit node DNS behavior
Updates #8237 Fixes tailscale/corp#30906 Fixes tailscale/corp#30907 Signed-off-by: Michael Ben-Ami <mzb@tailscale.com>
This commit is contained in:
@@ -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: []*dnstype.Resolver{
|
Resolvers: []*tailcfg.DNSResolver{
|
||||||
{Addr: "8.8.8.8"},
|
{Resolver: dnstype.Resolver{Addr: "8.8.8.8"}},
|
||||||
},
|
},
|
||||||
FallbackResolvers: []*dnstype.Resolver{
|
FallbackResolvers: []*dnstype.Resolver{
|
||||||
{Addr: "8.8.4.4"},
|
{Addr: "8.8.4.4"},
|
||||||
},
|
},
|
||||||
Routes: map[string][]*dnstype.Resolver{
|
Routes: map[string][]*tailcfg.DNSResolver{
|
||||||
"foo.com.": {{Addr: "1.2.3.4"}},
|
"foo.com.": {{Resolver: dnstype.Resolver{Addr: "1.2.3.4"}}},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@@ -2080,7 +2080,23 @@ func TestDNSConfigForNetmapForExitNodeConfigs(t *testing.T) {
|
|||||||
wantRoutes map[dnsname.FQDN][]*dnstype.Resolver
|
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"}}
|
wgResolvers := []*dnstype.Resolver{{Addr: "wg.example.com"}}
|
||||||
peers := []tailcfg.NodeView{
|
peers := []tailcfg.NodeView{
|
||||||
(&tailcfg.Node{
|
(&tailcfg.Node{
|
||||||
@@ -2099,16 +2115,51 @@ func TestDNSConfigForNetmapForExitNodeConfigs(t *testing.T) {
|
|||||||
}).View(),
|
}).View(),
|
||||||
}
|
}
|
||||||
exitDOH := peerAPIBase(&netmap.NetworkMap{Peers: peers}, peers[0]) + "/dns-query"
|
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"}},
|
"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 {
|
if routes == nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
m := make(map[string][]*dnstype.Resolver)
|
m := make(map[string][]*tailcfg.DNSResolver)
|
||||||
for k, v := range routes {
|
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
|
return m
|
||||||
}
|
}
|
||||||
@@ -2134,23 +2185,27 @@ 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: defaultResolvers},
|
dnsConfig: &tailcfg.DNSConfig{Resolvers: wrapResolvers(defaultResolvers)},
|
||||||
wantDefaultResolvers: []*dnstype.Resolver{{Addr: exitDOH}},
|
wantDefaultResolvers: []*dnstype.Resolver{{Addr: exitDOH}},
|
||||||
wantRoutes: nil,
|
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
|
// When at tailscale exit node is in use,
|
||||||
// exit node split-DNS may effectively break, furthermore in the future
|
// only routes that reference resolvers with the UseWithExitNode should be installed,
|
||||||
// if different nodes observe different DNS configurations, even a
|
// as well as routes with 0-length resolver lists, which should be installed in all cases.
|
||||||
// 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.
|
|
||||||
{
|
{
|
||||||
name: "tsExit/routes/noResolver",
|
name: "tsExit/routes/noResolver",
|
||||||
exitNode: "ts",
|
exitNode: "ts",
|
||||||
peers: peers,
|
peers: peers,
|
||||||
dnsConfig: &tailcfg.DNSConfig{Routes: stringifyRoutes(routes)},
|
dnsConfig: &tailcfg.DNSConfig{Routes: stringifyRoutes(baseRoutes)},
|
||||||
wantDefaultResolvers: []*dnstype.Resolver{{Addr: exitDOH}},
|
wantDefaultResolvers: []*dnstype.Resolver{{Addr: exitDOH}},
|
||||||
wantRoutes: nil,
|
wantRoutes: nil,
|
||||||
},
|
},
|
||||||
@@ -2158,10 +2213,58 @@ 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(routes), Resolvers: defaultResolvers},
|
dnsConfig: &tailcfg.DNSConfig{Routes: stringifyRoutes(baseRoutes), Resolvers: wrapResolvers(defaultResolvers)},
|
||||||
wantDefaultResolvers: []*dnstype.Resolver{{Addr: exitDOH}},
|
wantDefaultResolvers: []*dnstype.Resolver{{Addr: exitDOH}},
|
||||||
wantRoutes: nil,
|
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
|
// WireGuard exit nodes with DNS capabilities provide a "fallback" type
|
||||||
// behavior, they have a lower precedence than a default resolver, but
|
// behavior, they have a lower precedence than a default resolver, but
|
||||||
@@ -2179,7 +2282,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: defaultResolvers},
|
dnsConfig: &tailcfg.DNSConfig{Resolvers: wrapResolvers(defaultResolvers)},
|
||||||
wantDefaultResolvers: defaultResolvers,
|
wantDefaultResolvers: defaultResolvers,
|
||||||
wantRoutes: nil,
|
wantRoutes: nil,
|
||||||
},
|
},
|
||||||
@@ -2187,17 +2290,17 @@ 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(routes), Resolvers: defaultResolvers},
|
dnsConfig: &tailcfg.DNSConfig{Routes: stringifyRoutes(baseRoutes), Resolvers: wrapResolvers(defaultResolvers)},
|
||||||
wantDefaultResolvers: defaultResolvers,
|
wantDefaultResolvers: defaultResolvers,
|
||||||
wantRoutes: routes,
|
wantRoutes: baseRoutes,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "wgExit/routes/noResolver",
|
name: "wgExit/routes/noResolver",
|
||||||
exitNode: "wg",
|
exitNode: "wg",
|
||||||
peers: peers,
|
peers: peers,
|
||||||
dnsConfig: &tailcfg.DNSConfig{Routes: stringifyRoutes(routes)},
|
dnsConfig: &tailcfg.DNSConfig{Routes: stringifyRoutes(baseRoutes)},
|
||||||
wantDefaultResolvers: wgResolvers,
|
wantDefaultResolvers: wgResolvers,
|
||||||
wantRoutes: routes,
|
wantRoutes: baseRoutes,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -578,6 +578,61 @@ func (nb *nodeBackend) doShutdown(cause error) {
|
|||||||
nb.eventClient.Close()
|
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,
|
// dnsConfigForNetmap returns a *dns.Config for the given netmap,
|
||||||
// prefs, client OS version, and cloud hosting environment.
|
// 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...)
|
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+)
|
// 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 {
|
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
|
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
|
// 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(nm.DNS.Resolvers)
|
addDefault(resolvers(nm.DNS))
|
||||||
} else {
|
} else {
|
||||||
if resolvers, ok := wireguardExitNodeDNSResolvers(nm, peers, prefs.ExitNodeID()); ok {
|
if resolvers, ok := wireguardExitNodeDNSResolvers(nm, peers, prefs.ExitNodeID()); ok {
|
||||||
addDefault(resolvers)
|
addDefault(resolvers)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for suffix, resolvers := range nm.DNS.Routes {
|
// Add split DNS routes, with no regard to exit node configuration.
|
||||||
fqdn, err := dnsname.ToFQDN(suffix)
|
addSplitDNSRoutes(routes(nm.DNS))
|
||||||
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...)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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
|
||||||
|
@@ -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 --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 (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
@@ -168,7 +168,8 @@ 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)
|
||||||
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
|
// ID is an integer ID for a user, node, or login allocated by the
|
||||||
// control plane.
|
// 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.
|
// 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 []*dnstype.Resolver `json:",omitempty"`
|
Resolvers []*DNSResolver `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
|
||||||
@@ -1713,7 +1721,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][]*dnstype.Resolver `json:",omitempty"`
|
Routes map[string][]*DNSResolver `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
|
||||||
|
@@ -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([]*dnstype.Resolver, len(src.Resolvers))
|
dst.Resolvers = make([]*DNSResolver, 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][]*dnstype.Resolver{}
|
dst.Routes = map[string][]*DNSResolver{}
|
||||||
for k := range src.Routes {
|
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 {
|
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 []*dnstype.Resolver
|
Resolvers []*DNSResolver
|
||||||
Routes map[string][]*dnstype.Resolver
|
Routes map[string][]*DNSResolver
|
||||||
FallbackResolvers []*dnstype.Resolver
|
FallbackResolvers []*dnstype.Resolver
|
||||||
Domains []string
|
Domains []string
|
||||||
Proxied bool
|
Proxied bool
|
||||||
@@ -651,9 +651,27 @@ 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.
|
// 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 {
|
func Clone(dst, src any) bool {
|
||||||
switch src := src.(type) {
|
switch src := src.(type) {
|
||||||
case *User:
|
case *User:
|
||||||
@@ -836,6 +854,15 @@ 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
|
||||||
}
|
}
|
||||||
|
@@ -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
|
//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.
|
// 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[*dnstype.Resolver, dnstype.ResolverView] {
|
func (v DNSConfigView) Resolvers() views.SliceView[*DNSResolver, DNSResolverView] {
|
||||||
return views.SliceOfViews[*dnstype.Resolver, dnstype.ResolverView](v.ж.Resolvers)
|
return views.SliceOfViews[*DNSResolver, DNSResolverView](v.ж.Resolvers)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (v DNSConfigView) Routes() views.MapFn[string, []*dnstype.Resolver, views.SliceView[*dnstype.Resolver, dnstype.ResolverView]] {
|
func (v DNSConfigView) Routes() views.MapFn[string, []*DNSResolver, views.SliceView[*DNSResolver, DNSResolverView]] {
|
||||||
return views.MapFnOf(v.ж.Routes, func(t []*dnstype.Resolver) views.SliceView[*dnstype.Resolver, dnstype.ResolverView] {
|
return views.MapFnOf(v.ж.Routes, func(t []*DNSResolver) views.SliceView[*DNSResolver, DNSResolverView] {
|
||||||
return views.SliceOfViews[*dnstype.Resolver, dnstype.ResolverView](t)
|
return views.SliceOfViews[*DNSResolver, DNSResolverView](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 []*dnstype.Resolver
|
Resolvers []*DNSResolver
|
||||||
Routes map[string][]*dnstype.Resolver
|
Routes map[string][]*DNSResolver
|
||||||
FallbackResolvers []*dnstype.Resolver
|
FallbackResolvers []*dnstype.Resolver
|
||||||
Domains []string
|
Domains []string
|
||||||
Proxied bool
|
Proxied bool
|
||||||
@@ -1477,3 +1477,57 @@ 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
|
||||||
|
}{})
|
||||||
|
@@ -62,7 +62,10 @@ 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: []*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"},
|
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"}},
|
||||||
|
@@ -142,8 +142,8 @@ func getVal() *tailscaleTypes {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
DNSConfig: &tailcfg.DNSConfig{
|
DNSConfig: &tailcfg.DNSConfig{
|
||||||
Resolvers: []*dnstype.Resolver{
|
Resolvers: []*tailcfg.DNSResolver{
|
||||||
{Addr: "10.0.0.1"},
|
{Resolver: dnstype.Resolver{Addr: "10.0.0.1"}},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
PacketFilter: []tailcfg.FilterRule{
|
PacketFilter: []tailcfg.FilterRule{
|
||||||
|
Reference in New Issue
Block a user