diff --git a/ipn/ipnlocal/dnsconfig_test.go b/ipn/ipnlocal/dnsconfig_test.go index dbb40cd76..1717caea9 100644 --- a/ipn/ipnlocal/dnsconfig_test.go +++ b/ipn/ipnlocal/dnsconfig_test.go @@ -14,10 +14,13 @@ import ( "tailscale.com/tailcfg" "tailscale.com/tstest" "tailscale.com/types/dnstype" + "tailscale.com/types/logger" "tailscale.com/types/netmap" "tailscale.com/util/cloudenv" "tailscale.com/util/cmpx" "tailscale.com/util/dnsname" + "tailscale.com/wgengine" + "tailscale.com/wgengine/filter" ) func ipps(ippStrs ...string) (ipps []netip.Prefix) { @@ -327,12 +330,70 @@ func TestDNSConfigForNetmap(t *testing.T) { Routes: map[dnsname.FQDN][]*dnstype.Resolver{}, }, }, + { + name: "ipv6_os_support", + nm: &netmap.NetworkMap{ + Name: "myname.net", + SelfNode: (&tailcfg.Node{ + Addresses: ipps("100.101.101.101"), + }).View(), + PacketFilter: []filter.Match{{ + // TODO(andrew): this looks backwards? + Srcs: []netip.Prefix{netip.MustParsePrefix("100.102.0.1/32")}, + Caps: []filter.CapMatch{ + { + Dst: netip.MustParsePrefix("100.101.101.101/32"), + Cap: tailcfg.PeerCapabilityOSIPv6, + }, + }, + }}, + }, + peers: nodeViews([]*tailcfg.Node{ + { + ID: 1, + Name: "peera.net", + Addresses: ipps("100.102.0.1", "100.102.0.2", "fe75::1001", "fe75::1002"), + }, + { + ID: 2, + Name: "b.net", + Addresses: ipps("100.102.0.3", "100.102.0.4", "fe75::2"), + }, + }), + prefs: &ipn.Prefs{}, + want: &dns.Config{ + Routes: map[dnsname.FQDN][]*dnstype.Resolver{}, + Hosts: map[dnsname.FQDN][]netip.Addr{ + "b.net.": ips("100.102.0.3", "100.102.0.4"), + "myname.net.": ips("100.101.101.101"), + "peera.net.": ips("100.102.0.1", "100.102.0.2", "fe75::1001", "fe75::1002"), + }, + }, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { verOS := cmpx.Or(tt.os, "linux") + + eng, _ := wgengine.NewFakeUserspaceEngine(logger.Discard, 0) + var log tstest.MemLogger - got := dnsConfigForNetmap(tt.nm, peersMap(tt.peers), tt.prefs.View(), log.Logf, verOS) + b := &LocalBackend{ + e: eng, + netMap: tt.nm, + logf: log.Logf, + peers: peersMap(tt.peers), + } + b.mu.Lock() + b.updateFilterLocked(b.netMap, tt.prefs.View()) + + // the updateFilterLocked function logs something; clear it + log.Lock() + log.Reset() + log.Unlock() + + got := b.dnsConfigForNetmapLocked(tt.prefs.View(), verOS) + b.mu.Unlock() if !reflect.DeepEqual(got, tt.want) { gotj, _ := json.MarshalIndent(got, "", "\t") wantj, _ := json.MarshalIndent(tt.want, "", "\t") diff --git a/ipn/ipnlocal/local.go b/ipn/ipnlocal/local.go index c02e7d5ba..c6c81bddd 100644 --- a/ipn/ipnlocal/local.go +++ b/ipn/ipnlocal/local.go @@ -3143,7 +3143,7 @@ func (b *LocalBackend) authReconfig() { hasPAC := b.prevIfState.HasPAC() disableSubnetsIfPAC := hasCapability(nm, tailcfg.NodeAttrDisableSubnetsIfPAC) dohURL, dohURLOK := exitNodeCanProxyDNS(nm, b.peers, prefs.ExitNodeID()) - dcfg := dnsConfigForNetmap(nm, b.peers, prefs, b.logf, version.OS()) + dcfg := b.dnsConfigForNetmapLocked(prefs, version.OS()) b.mu.Unlock() if blocked { @@ -3233,12 +3233,15 @@ func shouldUseOneCGNATRoute(logf logger.Logf, controlKnobs *controlknobs.Knobs, return false } -// dnsConfigForNetmap returns a *dns.Config for the given netmap, +// dnsConfigForNetmapLocked returns a *dns.Config for the given netmap, // prefs, client OS version, and cloud hosting environment. // // The versionOS is a Tailscale-style version ("iOS", "macOS") and not // a runtime.GOOS. -func dnsConfigForNetmap(nm *netmap.NetworkMap, peers map[tailcfg.NodeID]tailcfg.NodeView, prefs ipn.PrefsView, logf logger.Logf, versionOS string) *dns.Config { +// +// b.mu must be held +func (b *LocalBackend) dnsConfigForNetmapLocked(prefs ipn.PrefsView, versionOS string) *dns.Config { + nm := b.netMap if nm == nil { return nil } @@ -3265,10 +3268,21 @@ func dnsConfigForNetmap(nm *netmap.NetworkMap, peers map[tailcfg.NodeID]tailcfg. if err != nil { return // TODO: propagate error? } - var have4 bool + var have4, osHasV6 bool for i := range addrs.LenIter() { - if addrs.At(i).Addr().Is4() { + addr := addrs.At(i).Addr() + if addr.Is4() { have4 = true + } + + // TODO(andrew): PeerCaps depend on the IPProto, so we + // need to do this for every address; can we make this + // less expensive? + if b.peerHasCapLocked(addr, tailcfg.PeerCapabilityOSIPv6) { + osHasV6 = true + } + + if have4 && osHasV6 { break } } @@ -3281,23 +3295,24 @@ func dnsConfigForNetmap(nm *netmap.NetworkMap, peers map[tailcfg.NodeID]tailcfg. } continue } - // If this node has an IPv4 address, then - // remove peers' IPv6 addresses for now, as we - // don't guarantee that the peer node actually - // can speak IPv6 correctly. + + // If this node has an IPv4 address and doesn't have + // host-level IPv6 support, then we don't return IPv6 + // addresses to stop this node from trying to + // communicate with the peer over IPv6. // // https://github.com/tailscale/tailscale/issues/1152 // tracks adding the right capability reporting to // enable AAAA in MagicDNS. - if addr.Addr().Is6() && have4 { + if addr.Addr().Is6() && have4 && !osHasV6 { continue } ips = append(ips, addr.Addr()) } dcfg.Hosts[fqdn] = ips } - set(nm.Name, nm.GetAddresses()) - for _, peer := range peers { + set(nm.Name, nm.GetAddresses()) // TODO(andrew): set osHasV6 properly here? + for _, peer := range b.peers { set(peer.Name(), peer.Addresses()) } for _, rec := range nm.DNS.ExtraRecords { @@ -3327,7 +3342,7 @@ func dnsConfigForNetmap(nm *netmap.NetworkMap, peers map[tailcfg.NodeID]tailcfg. for _, dom := range nm.DNS.Domains { fqdn, err := dnsname.ToFQDN(dom) if err != nil { - logf("[unexpected] non-FQDN search domain %q", dom) + b.logf("[unexpected] non-FQDN search domain %q", dom) } dcfg.SearchDomains = append(dcfg.SearchDomains, fqdn) } @@ -3343,7 +3358,7 @@ func dnsConfigForNetmap(nm *netmap.NetworkMap, peers map[tailcfg.NodeID]tailcfg. // 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. - if dohURL, ok := exitNodeCanProxyDNS(nm, peers, prefs.ExitNodeID()); ok { + if dohURL, ok := exitNodeCanProxyDNS(nm, b.peers, prefs.ExitNodeID()); ok { addDefault([]*dnstype.Resolver{{Addr: dohURL}}) return dcfg } @@ -3354,7 +3369,7 @@ func dnsConfigForNetmap(nm *netmap.NetworkMap, peers map[tailcfg.NodeID]tailcfg. if len(nm.DNS.Resolvers) > 0 { addDefault(nm.DNS.Resolvers) } else { - if resolvers, ok := wireguardExitNodeDNSResolvers(nm, peers, prefs.ExitNodeID()); ok { + if resolvers, ok := wireguardExitNodeDNSResolvers(nm, b.peers, prefs.ExitNodeID()); ok { addDefault(resolvers) } } @@ -3362,7 +3377,7 @@ func dnsConfigForNetmap(nm *netmap.NetworkMap, peers map[tailcfg.NodeID]tailcfg. for suffix, resolvers := range nm.DNS.Routes { fqdn, err := dnsname.ToFQDN(suffix) if err != nil { - logf("[unexpected] non-FQDN route suffix %q", suffix) + b.logf("[unexpected] non-FQDN route suffix %q", suffix) } // Create map entry even if len(resolvers) == 0; Issue 2706. diff --git a/ipn/ipnlocal/local_test.go b/ipn/ipnlocal/local_test.go index 2bb037a30..38f013c71 100644 --- a/ipn/ipnlocal/local_test.go +++ b/ipn/ipnlocal/local_test.go @@ -1112,7 +1112,14 @@ func TestDNSConfigForNetmapForExitNodeConfigs(t *testing.T) { } prefs := &ipn.Prefs{ExitNodeID: tc.exitNode, CorpDNS: true} - got := dnsConfigForNetmap(nm, peersMap(tc.peers), prefs.View(), t.Logf, "") + b := &LocalBackend{ + netMap: nm, + logf: t.Logf, + peers: peersMap(tc.peers), + } + b.mu.Lock() + got := b.dnsConfigForNetmapLocked(prefs.View(), "") + b.mu.Unlock() if !resolversEqual(t, got.DefaultResolvers, tc.wantDefaultResolvers) { t.Errorf("DefaultResolvers: got %#v, want %#v", got.DefaultResolvers, tc.wantDefaultResolvers) } diff --git a/tailcfg/tailcfg.go b/tailcfg/tailcfg.go index 999fabe3c..3c8121120 100644 --- a/tailcfg/tailcfg.go +++ b/tailcfg/tailcfg.go @@ -1322,6 +1322,10 @@ const ( PeerCapabilityWakeOnLAN PeerCapability = "https://tailscale.com/cap/wake-on-lan" // PeerCapabilityIngress grants the ability for a peer to send ingress traffic. PeerCapabilityIngress PeerCapability = "https://tailscale.com/cap/ingress" + // PeerCapabilityOSIPv6 grants the ability for the current node to send + // traffic to the peer over IPv6; this indicates that the node has IPv6 + // support at the OS level. + PeerCapabilityOSIPv6 PeerCapability = "https://tailscale.com/cap/os-ipv6" ) // NodeCapMap is a map of capabilities to their optional values. It is valid for