From 008a238acddcf1cb73c544eee41f689392b74494 Mon Sep 17 00:00:00 2001 From: Jordan Whited Date: Wed, 9 Jul 2025 09:16:29 -0700 Subject: [PATCH] wgengine/magicsock: support self as candidate peer relay (#16499) Updates tailscale/corp#30247 Signed-off-by: Jordan Whited --- wgengine/magicsock/magicsock.go | 51 ++++++------ wgengine/magicsock/magicsock_test.go | 114 ++++++++++++++++++--------- 2 files changed, 102 insertions(+), 63 deletions(-) diff --git a/wgengine/magicsock/magicsock.go b/wgengine/magicsock/magicsock.go index ab7c2102f..1978867fa 100644 --- a/wgengine/magicsock/magicsock.go +++ b/wgengine/magicsock/magicsock.go @@ -2618,8 +2618,8 @@ func (c *Conn) onFilterUpdate(f FilterUpdate) { c.updateRelayServersSet(f.Filter, self, peers) } -// updateRelayServersSet iterates all peers, evaluating filt for each one in -// order to determine which peers are relay server candidates. filt, self, and +// updateRelayServersSet iterates all peers and self, evaluating filt for each +// one in order to determine which are relay server candidates. filt, self, and // peers are passed as args (vs c.mu-guarded fields) to enable callers to // release c.mu before calling as this is O(m * n) (we iterate all cap rules 'm' // in filt for every peer 'n'). @@ -2631,8 +2631,9 @@ func (c *Conn) onFilterUpdate(f FilterUpdate) { // the computed result over the eventbus instead. func (c *Conn) updateRelayServersSet(filt *filter.Filter, self tailcfg.NodeView, peers views.Slice[tailcfg.NodeView]) { relayServers := make(set.Set[netip.AddrPort]) - for _, peer := range peers.All() { - peerAPI := peerAPIIfCandidateRelayServer(filt, self, peer) + nodes := append(peers.AsSlice(), self) + for _, maybeCandidate := range nodes { + peerAPI := peerAPIIfCandidateRelayServer(filt, self, maybeCandidate) if peerAPI.IsValid() { relayServers.Add(peerAPI) } @@ -2640,33 +2641,34 @@ func (c *Conn) updateRelayServersSet(filt *filter.Filter, self tailcfg.NodeView, c.relayManager.handleRelayServersSet(relayServers) } -// peerAPIIfCandidateRelayServer returns the peer API address of peer if it -// is considered to be a candidate relay server upon evaluation against filt and -// self, otherwise it returns a zero value. -func peerAPIIfCandidateRelayServer(filt *filter.Filter, self, peer tailcfg.NodeView) netip.AddrPort { +// peerAPIIfCandidateRelayServer returns the peer API address of maybeCandidate +// if it is considered to be a candidate relay server upon evaluation against +// filt and self, otherwise it returns a zero value. self and maybeCandidate +// may be equal. +func peerAPIIfCandidateRelayServer(filt *filter.Filter, self, maybeCandidate tailcfg.NodeView) netip.AddrPort { if filt == nil || !self.Valid() || - !peer.Valid() || - !capVerIsRelayServerCapable(peer.Cap()) || - !peer.Hostinfo().Valid() { + !maybeCandidate.Valid() || + !capVerIsRelayServerCapable(maybeCandidate.Cap()) || + !maybeCandidate.Hostinfo().Valid() { return netip.AddrPort{} } - for _, peerPrefix := range peer.Addresses().All() { - if !peerPrefix.IsSingleIP() { + for _, maybeCandidatePrefix := range maybeCandidate.Addresses().All() { + if !maybeCandidatePrefix.IsSingleIP() { continue } - peerAddr := peerPrefix.Addr() + maybeCandidateAddr := maybeCandidatePrefix.Addr() for _, selfPrefix := range self.Addresses().All() { if !selfPrefix.IsSingleIP() { continue } selfAddr := selfPrefix.Addr() - if selfAddr.BitLen() == peerAddr.BitLen() { // same address family - if filt.CapsWithValues(peerAddr, selfAddr).HasCapability(tailcfg.PeerCapabilityRelayTarget) { - for _, s := range peer.Hostinfo().Services().All() { - if peerAddr.Is4() && s.Proto == tailcfg.PeerAPI4 || - peerAddr.Is6() && s.Proto == tailcfg.PeerAPI6 { - return netip.AddrPortFrom(peerAddr, s.Port) + if selfAddr.BitLen() == maybeCandidateAddr.BitLen() { // same address family + if filt.CapsWithValues(maybeCandidateAddr, selfAddr).HasCapability(tailcfg.PeerCapabilityRelayTarget) { + for _, s := range maybeCandidate.Hostinfo().Services().All() { + if maybeCandidateAddr.Is4() && s.Proto == tailcfg.PeerAPI4 || + maybeCandidateAddr.Is6() && s.Proto == tailcfg.PeerAPI6 { + return netip.AddrPortFrom(maybeCandidateAddr, s.Port) } } return netip.AddrPort{} // no peerAPI @@ -2674,10 +2676,11 @@ func peerAPIIfCandidateRelayServer(filt *filter.Filter, self, peer tailcfg.NodeV // [nodeBackend.peerCapsLocked] only returns/considers the // [tailcfg.PeerCapMap] between the passed src and the // _first_ host (/32 or /128) address for self. We are - // consistent with that behavior here. If self and peer - // host addresses are of the same address family they either - // have the capability or not. We do not check against - // additional host addresses of the same address family. + // consistent with that behavior here. If self and + // maybeCandidate host addresses are of the same address + // family they either have the capability or not. We do not + // check against additional host addresses of the same + // address family. return netip.AddrPort{} } } diff --git a/wgengine/magicsock/magicsock_test.go b/wgengine/magicsock/magicsock_test.go index c388e9ed1..aea2de17d 100644 --- a/wgengine/magicsock/magicsock_test.go +++ b/wgengine/magicsock/magicsock_test.go @@ -3385,16 +3385,7 @@ func Test_virtualNetworkID(t *testing.T) { } func Test_peerAPIIfCandidateRelayServer(t *testing.T) { - selfOnlyIPv4 := &tailcfg.Node{ - Cap: math.MinInt32, - Addresses: []netip.Prefix{ - netip.MustParsePrefix("1.1.1.1/32"), - }, - } - selfOnlyIPv6 := selfOnlyIPv4.Clone() - selfOnlyIPv6.Addresses[0] = netip.MustParsePrefix("::1/128") - - peerHostinfo := &tailcfg.Hostinfo{ + hostInfo := &tailcfg.Hostinfo{ Services: []tailcfg.Service{ { Proto: tailcfg.PeerAPI4, @@ -3406,12 +3397,23 @@ func Test_peerAPIIfCandidateRelayServer(t *testing.T) { }, }, } + + selfOnlyIPv4 := &tailcfg.Node{ + Cap: math.MinInt32, + Addresses: []netip.Prefix{ + netip.MustParsePrefix("1.1.1.1/32"), + }, + Hostinfo: hostInfo.View(), + } + selfOnlyIPv6 := selfOnlyIPv4.Clone() + selfOnlyIPv6.Addresses[0] = netip.MustParsePrefix("::1/128") + peerOnlyIPv4 := &tailcfg.Node{ Cap: math.MinInt32, Addresses: []netip.Prefix{ netip.MustParsePrefix("2.2.2.2/32"), }, - Hostinfo: peerHostinfo.View(), + Hostinfo: hostInfo.View(), } peerOnlyIPv6 := peerOnlyIPv4.Clone() @@ -3424,11 +3426,11 @@ func Test_peerAPIIfCandidateRelayServer(t *testing.T) { peerOnlyIPv4NilHostinfo.Hostinfo = tailcfg.HostinfoView{} tests := []struct { - name string - filt *filter.Filter - self tailcfg.NodeView - peer tailcfg.NodeView - want netip.AddrPort + name string + filt *filter.Filter + self tailcfg.NodeView + maybeCandidate tailcfg.NodeView + want netip.AddrPort }{ { name: "match v4", @@ -3443,9 +3445,26 @@ func Test_peerAPIIfCandidateRelayServer(t *testing.T) { }, }, }, nil, nil, nil, nil, nil), - self: selfOnlyIPv4.View(), - peer: peerOnlyIPv4.View(), - want: netip.MustParseAddrPort("2.2.2.2:4"), + self: selfOnlyIPv4.View(), + maybeCandidate: peerOnlyIPv4.View(), + want: netip.MustParseAddrPort("2.2.2.2:4"), + }, + { + name: "match v4 self", + filt: filter.New([]filtertype.Match{ + { + Srcs: []netip.Prefix{selfOnlyIPv4.Addresses[0]}, + Caps: []filtertype.CapMatch{ + { + Dst: selfOnlyIPv4.Addresses[0], + Cap: tailcfg.PeerCapabilityRelayTarget, + }, + }, + }, + }, nil, nil, nil, nil, nil), + self: selfOnlyIPv4.View(), + maybeCandidate: selfOnlyIPv4.View(), + want: netip.AddrPortFrom(selfOnlyIPv4.Addresses[0].Addr(), 4), }, { name: "match v6", @@ -3460,9 +3479,26 @@ func Test_peerAPIIfCandidateRelayServer(t *testing.T) { }, }, }, nil, nil, nil, nil, nil), - self: selfOnlyIPv6.View(), - peer: peerOnlyIPv6.View(), - want: netip.MustParseAddrPort("[::2]:6"), + self: selfOnlyIPv6.View(), + maybeCandidate: peerOnlyIPv6.View(), + want: netip.MustParseAddrPort("[::2]:6"), + }, + { + name: "match v6 self", + filt: filter.New([]filtertype.Match{ + { + Srcs: []netip.Prefix{selfOnlyIPv6.Addresses[0]}, + Caps: []filtertype.CapMatch{ + { + Dst: selfOnlyIPv6.Addresses[0], + Cap: tailcfg.PeerCapabilityRelayTarget, + }, + }, + }, + }, nil, nil, nil, nil, nil), + self: selfOnlyIPv6.View(), + maybeCandidate: selfOnlyIPv6.View(), + want: netip.AddrPortFrom(selfOnlyIPv6.Addresses[0].Addr(), 6), }, { name: "no match dst", @@ -3477,8 +3513,8 @@ func Test_peerAPIIfCandidateRelayServer(t *testing.T) { }, }, }, nil, nil, nil, nil, nil), - self: selfOnlyIPv6.View(), - peer: peerOnlyIPv6.View(), + self: selfOnlyIPv6.View(), + maybeCandidate: peerOnlyIPv6.View(), }, { name: "no match peer cap", @@ -3493,8 +3529,8 @@ func Test_peerAPIIfCandidateRelayServer(t *testing.T) { }, }, }, nil, nil, nil, nil, nil), - self: selfOnlyIPv6.View(), - peer: peerOnlyIPv6.View(), + self: selfOnlyIPv6.View(), + maybeCandidate: peerOnlyIPv6.View(), }, { name: "cap ver not relay capable", @@ -3509,14 +3545,14 @@ func Test_peerAPIIfCandidateRelayServer(t *testing.T) { }, }, }, nil, nil, nil, nil, nil), - self: peerOnlyIPv4.View(), - peer: peerOnlyIPv4ZeroCapVer.View(), + self: peerOnlyIPv4.View(), + maybeCandidate: peerOnlyIPv4ZeroCapVer.View(), }, { - name: "nil filt", - filt: nil, - self: selfOnlyIPv4.View(), - peer: peerOnlyIPv4.View(), + name: "nil filt", + filt: nil, + self: selfOnlyIPv4.View(), + maybeCandidate: peerOnlyIPv4.View(), }, { name: "nil self", @@ -3531,8 +3567,8 @@ func Test_peerAPIIfCandidateRelayServer(t *testing.T) { }, }, }, nil, nil, nil, nil, nil), - self: tailcfg.NodeView{}, - peer: peerOnlyIPv4.View(), + self: tailcfg.NodeView{}, + maybeCandidate: peerOnlyIPv4.View(), }, { name: "nil peer", @@ -3547,8 +3583,8 @@ func Test_peerAPIIfCandidateRelayServer(t *testing.T) { }, }, }, nil, nil, nil, nil, nil), - self: selfOnlyIPv4.View(), - peer: tailcfg.NodeView{}, + self: selfOnlyIPv4.View(), + maybeCandidate: tailcfg.NodeView{}, }, { name: "nil peer hostinfo", @@ -3563,13 +3599,13 @@ func Test_peerAPIIfCandidateRelayServer(t *testing.T) { }, }, }, nil, nil, nil, nil, nil), - self: selfOnlyIPv4.View(), - peer: peerOnlyIPv4NilHostinfo.View(), + self: selfOnlyIPv4.View(), + maybeCandidate: peerOnlyIPv4NilHostinfo.View(), }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - if got := peerAPIIfCandidateRelayServer(tt.filt, tt.self, tt.peer); !reflect.DeepEqual(got, tt.want) { + if got := peerAPIIfCandidateRelayServer(tt.filt, tt.self, tt.maybeCandidate); !reflect.DeepEqual(got, tt.want) { t.Errorf("peerAPIIfCandidateRelayServer() = %v, want %v", got, tt.want) } })