diff --git a/cmd/tailscale/cli/debug.go b/cmd/tailscale/cli/debug.go index ec8a0700d..8473c4a17 100644 --- a/cmd/tailscale/cli/debug.go +++ b/cmd/tailscale/cli/debug.go @@ -356,6 +356,12 @@ func debugCmd() *ffcli.Command { ShortHelp: "Print Go's runtime/debug.BuildInfo", Exec: runGoBuildInfo, }, + { + Name: "peer-relay-servers", + ShortUsage: "tailscale debug peer-relay-servers", + ShortHelp: "Print the current set of candidate peer relay servers", + Exec: runPeerRelayServers, + }, }...), } } @@ -1327,3 +1333,17 @@ func runDebugResolve(ctx context.Context, args []string) error { } return nil } + +func runPeerRelayServers(ctx context.Context, args []string) error { + if len(args) > 0 { + return errors.New("unexpected arguments") + } + v, err := localClient.DebugResultJSON(ctx, "peer-relay-servers") + if err != nil { + return err + } + e := json.NewEncoder(os.Stdout) + e.SetIndent("", " ") + e.Encode(v) + return nil +} diff --git a/ipn/ipnlocal/local.go b/ipn/ipnlocal/local.go index 9b9bd82b5..62ab6d904 100644 --- a/ipn/ipnlocal/local.go +++ b/ipn/ipnlocal/local.go @@ -6956,6 +6956,10 @@ func (b *LocalBackend) DebugReSTUN() error { return nil } +func (b *LocalBackend) DebugPeerRelayServers() set.Set[netip.AddrPort] { + return b.MagicConn().PeerRelays() +} + // ControlKnobs returns the node's control knobs. func (b *LocalBackend) ControlKnobs() *controlknobs.Knobs { return b.sys.ControlKnobs() diff --git a/ipn/localapi/localapi.go b/ipn/localapi/localapi.go index cd59c54e0..fb024039b 100644 --- a/ipn/localapi/localapi.go +++ b/ipn/localapi/localapi.go @@ -696,6 +696,12 @@ func (h *Handler) serveDebug(w http.ResponseWriter, r *http.Request) { break } h.b.DebugForcePreferDERP(n) + case "peer-relay-servers": + servers := h.b.DebugPeerRelayServers() + err = json.NewEncoder(w).Encode(servers) + if err == nil { + return + } case "": err = fmt.Errorf("missing parameter 'action'") default: diff --git a/wgengine/magicsock/magicsock.go b/wgengine/magicsock/magicsock.go index a8b1c8f15..24a4fc073 100644 --- a/wgengine/magicsock/magicsock.go +++ b/wgengine/magicsock/magicsock.go @@ -3907,3 +3907,8 @@ func (le *lazyEndpoint) FromPeer(peerPublicKey [32]byte) { le.c.peerMap.setNodeKeyForEpAddr(le.src, pubKey) le.c.logf("magicsock: lazyEndpoint.FromPeer(%v) setting epAddr(%v) in peerMap for node(%v)", pubKey.ShortString(), le.src, ep.nodeAddr) } + +// PeerRelays returns the current set of candidate peer relays. +func (c *Conn) PeerRelays() set.Set[netip.AddrPort] { + return c.relayManager.getServers() +} diff --git a/wgengine/magicsock/relaymanager.go b/wgengine/magicsock/relaymanager.go index c8c9ed41b..d7acf80b5 100644 --- a/wgengine/magicsock/relaymanager.go +++ b/wgengine/magicsock/relaymanager.go @@ -57,6 +57,7 @@ type relayManager struct { newServerEndpointCh chan newRelayServerEndpointEvent rxHandshakeDiscoMsgCh chan relayHandshakeDiscoMsgEvent serversCh chan set.Set[netip.AddrPort] + getServersCh chan chan set.Set[netip.AddrPort] discoInfoMu sync.Mutex // guards the following field discoInfoByServerDisco map[key.DiscoPublic]*relayHandshakeDiscoInfo @@ -185,10 +186,29 @@ func (r *relayManager) runLoop() { if !r.hasActiveWorkRunLoop() { return } + case getServersCh := <-r.getServersCh: + r.handleGetServersRunLoop(getServersCh) + if !r.hasActiveWorkRunLoop() { + return + } } } } +func (r *relayManager) handleGetServersRunLoop(getServersCh chan set.Set[netip.AddrPort]) { + servers := make(set.Set[netip.AddrPort], len(r.serversByAddrPort)) + for server := range r.serversByAddrPort { + servers.Add(server) + } + getServersCh <- servers +} + +func (r *relayManager) getServers() set.Set[netip.AddrPort] { + ch := make(chan set.Set[netip.AddrPort]) + relayManagerInputEvent(r, nil, &r.getServersCh, ch) + return <-ch +} + func (r *relayManager) handleServersUpdateRunLoop(update set.Set[netip.AddrPort]) { for k, v := range r.serversByAddrPort { if !update.Contains(k) { @@ -244,6 +264,7 @@ func (r *relayManager) init() { r.newServerEndpointCh = make(chan newRelayServerEndpointEvent) r.rxHandshakeDiscoMsgCh = make(chan relayHandshakeDiscoMsgEvent) r.serversCh = make(chan set.Set[netip.AddrPort]) + r.getServersCh = make(chan chan set.Set[netip.AddrPort]) r.runLoopStoppedCh = make(chan struct{}, 1) r.runLoopStoppedCh <- struct{}{} }) diff --git a/wgengine/magicsock/relaymanager_test.go b/wgengine/magicsock/relaymanager_test.go index 8f9236012..01f9258ad 100644 --- a/wgengine/magicsock/relaymanager_test.go +++ b/wgengine/magicsock/relaymanager_test.go @@ -32,4 +32,19 @@ func TestRelayManagerInitAndIdle(t *testing.T) { rm = relayManager{} rm.handleRelayServersSet(make(set.Set[netip.AddrPort])) <-rm.runLoopStoppedCh + + rm = relayManager{} + rm.getServers() + <-rm.runLoopStoppedCh +} + +func TestRelayManagerGetServers(t *testing.T) { + rm := relayManager{} + servers := make(set.Set[netip.AddrPort], 1) + servers.Add(netip.MustParseAddrPort("192.0.2.1:7")) + rm.handleRelayServersSet(servers) + got := rm.getServers() + if !servers.Equal(got) { + t.Errorf("got %v != want %v", got, servers) + } }