diff options
| author | Andrew Dunham <andrew@du.nham.ca> | 2023-09-28 16:08:12 -0400 |
|---|---|---|
| committer | Andrew Dunham <andrew@du.nham.ca> | 2023-10-05 15:54:57 -0400 |
| commit | 1ecc2f2d232358288f8058108f2ac773787335fb (patch) | |
| tree | 98f791cad1f87b8888e2b14bc07cb61efb2c31af | |
| parent | b4816e19b63a1999d86cbe4991458a6bacaa752d (diff) | |
| download | tailscale-andrew/peercap-ipv6-aaaa.tar.xz tailscale-andrew/peercap-ipv6-aaaa.zip | |
ipn/ipnlocal, tailcfg: return AAAA addresses for peers with IPv6 supportandrew/peercap-ipv6-aaaa
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 <andrew@du.nham.ca>
Change-Id: I0d1374c6e47592807f886749fb509a01e1ceb855
| -rw-r--r-- | ipn/ipnlocal/dnsconfig_test.go | 63 | ||||
| -rw-r--r-- | ipn/ipnlocal/local.go | 47 | ||||
| -rw-r--r-- | ipn/ipnlocal/local_test.go | 9 | ||||
| -rw-r--r-- | 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 |
