From 1ecc2f2d232358288f8058108f2ac773787335fb Mon Sep 17 00:00:00 2001 From: Andrew Dunham Date: Thu, 28 Sep 2023 16:08:12 -0400 Subject: [PATCH] ipn/ipnlocal, tailcfg: return AAAA addresses for peers with IPv6 support This adds the peer capability PeerCapabilityOSIPv6, which indicates that a given peer supports IPv6 at the OS level (as sent by the control server) and can thus be communicated with over IPv6. For peers with this capability, we can thus return AAAA addresses from MagicDNS. Updates #1152 Signed-off-by: Andrew Dunham Change-Id: I0d1374c6e47592807f886749fb509a01e1ceb855 --- ipn/ipnlocal/dnsconfig_test.go | 63 +++++++++++++++++++++++++++++++++- ipn/ipnlocal/local.go | 47 ++++++++++++++++--------- ipn/ipnlocal/local_test.go | 9 ++++- tailcfg/tailcfg.go | 4 +++ 4 files changed, 105 insertions(+), 18 deletions(-) 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