summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorDylan Bargatze <dylan@tailscale.com>2025-07-25 16:09:16 -0400
committerDylan Bargatze <dylan@tailscale.com>2025-08-06 15:27:55 -0400
commit2153c341836566510ce9cb5b6e95db1d87fcf02a (patch)
tree3db503c15a70ccd89f1aa7cee4b60c4ff1b966c1
parent908f20e0a506f9fe0c3f6479bc6b7c017cab27a1 (diff)
downloadtailscale-dylan/debug-peer-relay-sessions.tar.xz
tailscale-dylan/debug-peer-relay-sessions.zip
client, cmd/tailscale/cli, feature/relayserver, net/udprelay: implement tailscale debug peer-relay-sessionsdylan/debug-peer-relay-sessions
Fixes tailscale/corp#30035 Signed-off-by: Dylan Bargatze <dylan@tailscale.com>
-rw-r--r--client/local/local.go11
-rw-r--r--cmd/derper/depaware.txt1
-rw-r--r--cmd/k8s-operator/depaware.txt1
-rw-r--r--cmd/tailscale/cli/debug-peer-relay.go85
-rw-r--r--cmd/tailscale/cli/debug.go4
-rw-r--r--cmd/tailscale/depaware.txt1
-rw-r--r--cmd/tailscaled/depaware.txt1
-rw-r--r--cmd/tsidp/depaware.txt1
-rw-r--r--feature/relayserver/relayserver.go134
-rw-r--r--net/udprelay/server.go76
-rw-r--r--net/udprelay/status/status.go196
-rw-r--r--tsnet/depaware.txt1
12 files changed, 511 insertions, 1 deletions
diff --git a/client/local/local.go b/client/local/local.go
index 55d14f95e..a1dd88750 100644
--- a/client/local/local.go
+++ b/client/local/local.go
@@ -35,6 +35,7 @@ import (
"tailscale.com/ipn"
"tailscale.com/ipn/ipnstate"
"tailscale.com/net/netutil"
+ "tailscale.com/net/udprelay/status"
"tailscale.com/paths"
"tailscale.com/safesocket"
"tailscale.com/tailcfg"
@@ -1638,6 +1639,16 @@ func (lc *Client) DebugSetExpireIn(ctx context.Context, d time.Duration) error {
return err
}
+// DebugPeerRelaySessions returns debug information about the current peer
+// relay sessions running through this node.
+func (lc *Client) DebugPeerRelaySessions(ctx context.Context) (*status.ServerStatus, error) {
+ body, err := lc.send(ctx, "GET", "/localapi/v0/debug-peer-relay-sessions", 200, nil)
+ if err != nil {
+ return nil, fmt.Errorf("error %w: %s", err, body)
+ }
+ return decodeJSON[*status.ServerStatus](body)
+}
+
// StreamDebugCapture streams a pcap-formatted packet capture.
//
// The provided context does not determine the lifetime of the
diff --git a/cmd/derper/depaware.txt b/cmd/derper/depaware.txt
index 20b6bfb6e..df4499c2b 100644
--- a/cmd/derper/depaware.txt
+++ b/cmd/derper/depaware.txt
@@ -122,6 +122,7 @@ tailscale.com/cmd/derper dependencies: (generated by github.com/tailscale/depawa
tailscale.com/net/tlsdial/blockblame from tailscale.com/net/tlsdial
tailscale.com/net/tsaddr from tailscale.com/ipn+
💣 tailscale.com/net/tshttpproxy from tailscale.com/derp/derphttp+
+ tailscale.com/net/udprelay/status from tailscale.com/client/local
tailscale.com/net/wsconn from tailscale.com/cmd/derper
tailscale.com/paths from tailscale.com/client/local
💣 tailscale.com/safesocket from tailscale.com/client/local
diff --git a/cmd/k8s-operator/depaware.txt b/cmd/k8s-operator/depaware.txt
index 2dbf49d07..e35fa3c27 100644
--- a/cmd/k8s-operator/depaware.txt
+++ b/cmd/k8s-operator/depaware.txt
@@ -877,6 +877,7 @@ tailscale.com/cmd/k8s-operator dependencies: (generated by github.com/tailscale/
💣 tailscale.com/net/tshttpproxy from tailscale.com/clientupdate/distsign+
tailscale.com/net/tstun from tailscale.com/tsd+
tailscale.com/net/udprelay/endpoint from tailscale.com/wgengine/magicsock
+ tailscale.com/net/udprelay/status from tailscale.com/client/local
tailscale.com/omit from tailscale.com/ipn/conffile
tailscale.com/paths from tailscale.com/client/local+
💣 tailscale.com/portlist from tailscale.com/ipn/ipnlocal
diff --git a/cmd/tailscale/cli/debug-peer-relay.go b/cmd/tailscale/cli/debug-peer-relay.go
new file mode 100644
index 000000000..a4a9d0ddf
--- /dev/null
+++ b/cmd/tailscale/cli/debug-peer-relay.go
@@ -0,0 +1,85 @@
+// Copyright (c) Tailscale Inc & AUTHORS
+// SPDX-License-Identifier: BSD-3-Clause
+
+//go:build !ios && !ts_omit_relayserver
+
+package cli
+
+import (
+ "bytes"
+ "cmp"
+ "context"
+ "fmt"
+ "slices"
+
+ "github.com/peterbourgon/ff/v3/ffcli"
+ "tailscale.com/net/udprelay/status"
+)
+
+func init() {
+ debugPeerRelayCmd = mkDebugPeerRelaySessionsCmd
+}
+
+func mkDebugPeerRelaySessionsCmd() *ffcli.Command {
+ return &ffcli.Command{
+ Name: "peer-relay-sessions",
+ ShortUsage: "tailscale debug peer-relay-sessions",
+ Exec: runPeerRelaySessions,
+ ShortHelp: "Print the current set of active peer relay sessions relayed through this node",
+ }
+}
+
+func runPeerRelaySessions(ctx context.Context, args []string) error {
+ srv, err := localClient.DebugPeerRelaySessions(ctx)
+ if err != nil {
+ return err
+ }
+
+ var buf bytes.Buffer
+ f := func(format string, a ...any) { fmt.Fprintf(&buf, format, a...) }
+
+ valid_state := false
+ f("Server status : ")
+ switch srv.State {
+ case status.Disabled:
+ f("disabled (via node capability attribute 'disable-relay-server')")
+ case status.ShutDown:
+ f("shut down")
+ case status.NotConfigured:
+ f("not configured (you can configure the port with 'sudo tailscale set --relay-server-port=<PORT>')")
+ case status.Uninitialized:
+ valid_state = true
+ f("listening on port %v", srv.UDPPort)
+ case status.Running:
+ valid_state = true
+ f("running on port %v", srv.UDPPort)
+ default:
+ panic(fmt.Sprintf("unexpected status.ServerState: %#v", srv.State))
+ }
+
+ f("\n")
+ if !valid_state {
+ Stdout.Write(buf.Bytes())
+ return nil
+ }
+
+ f("Active sessions: %d\n", len(srv.Sessions))
+ if len(srv.Sessions) == 0 {
+ Stdout.Write(buf.Bytes())
+ return nil
+ }
+
+ slices.SortFunc(srv.Sessions, func(s1, s2 status.ServerSession) int { return cmp.Compare(s1.VNI, s2.VNI) })
+ f("\n%-8s %-41s %-55s %-55s\n", "VNI", "Server", "Client 1", "Client 2")
+ for _, s := range srv.Sessions {
+ f("%-8d %-41s %-55s %-55s\n",
+ s.VNI,
+ s.Server.String(),
+ s.Client1.String(),
+ s.Client2.String(),
+ )
+ }
+
+ Stdout.Write(buf.Bytes())
+ return nil
+}
diff --git a/cmd/tailscale/cli/debug.go b/cmd/tailscale/cli/debug.go
index fb062fd17..4fbde7e67 100644
--- a/cmd/tailscale/cli/debug.go
+++ b/cmd/tailscale/cli/debug.go
@@ -49,7 +49,8 @@ import (
)
var (
- debugCaptureCmd func() *ffcli.Command // or nil
+ debugCaptureCmd func() *ffcli.Command // or nil
+ debugPeerRelayCmd func() *ffcli.Command // or nil
)
func debugCmd() *ffcli.Command {
@@ -374,6 +375,7 @@ func debugCmd() *ffcli.Command {
ShortHelp: "Print the current set of candidate peer relay servers",
Exec: runPeerRelayServers,
},
+ ccall(debugPeerRelayCmd),
}...),
}
}
diff --git a/cmd/tailscale/depaware.txt b/cmd/tailscale/depaware.txt
index 020479ebb..28605eefe 100644
--- a/cmd/tailscale/depaware.txt
+++ b/cmd/tailscale/depaware.txt
@@ -138,6 +138,7 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
tailscale.com/net/tlsdial/blockblame from tailscale.com/net/tlsdial
tailscale.com/net/tsaddr from tailscale.com/client/web+
💣 tailscale.com/net/tshttpproxy from tailscale.com/clientupdate/distsign+
+ tailscale.com/net/udprelay/status from tailscale.com/client/local+
tailscale.com/paths from tailscale.com/client/local+
💣 tailscale.com/safesocket from tailscale.com/client/local+
tailscale.com/syncs from tailscale.com/cmd/tailscale/cli+
diff --git a/cmd/tailscaled/depaware.txt b/cmd/tailscaled/depaware.txt
index 7c4885a4b..b6d997fae 100644
--- a/cmd/tailscaled/depaware.txt
+++ b/cmd/tailscaled/depaware.txt
@@ -350,6 +350,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
tailscale.com/net/tstun from tailscale.com/cmd/tailscaled+
tailscale.com/net/udprelay from tailscale.com/feature/relayserver
tailscale.com/net/udprelay/endpoint from tailscale.com/feature/relayserver+
+ tailscale.com/net/udprelay/status from tailscale.com/client/local+
tailscale.com/omit from tailscale.com/ipn/conffile
tailscale.com/paths from tailscale.com/client/local+
💣 tailscale.com/portlist from tailscale.com/ipn/ipnlocal
diff --git a/cmd/tsidp/depaware.txt b/cmd/tsidp/depaware.txt
index b28460352..b18745cba 100644
--- a/cmd/tsidp/depaware.txt
+++ b/cmd/tsidp/depaware.txt
@@ -307,6 +307,7 @@ tailscale.com/cmd/tsidp dependencies: (generated by github.com/tailscale/depawar
💣 tailscale.com/net/tshttpproxy from tailscale.com/clientupdate/distsign+
tailscale.com/net/tstun from tailscale.com/tsd+
tailscale.com/net/udprelay/endpoint from tailscale.com/wgengine/magicsock
+ tailscale.com/net/udprelay/status from tailscale.com/client/local
tailscale.com/omit from tailscale.com/ipn/conffile
tailscale.com/paths from tailscale.com/client/local+
💣 tailscale.com/portlist from tailscale.com/ipn/ipnlocal
diff --git a/feature/relayserver/relayserver.go b/feature/relayserver/relayserver.go
index b90a62345..93481080e 100644
--- a/feature/relayserver/relayserver.go
+++ b/feature/relayserver/relayserver.go
@@ -6,14 +6,19 @@
package relayserver
import (
+ "encoding/json"
+ "fmt"
+ "net/http"
"sync"
"tailscale.com/disco"
"tailscale.com/feature"
"tailscale.com/ipn"
"tailscale.com/ipn/ipnext"
+ "tailscale.com/ipn/localapi"
"tailscale.com/net/udprelay"
"tailscale.com/net/udprelay/endpoint"
+ "tailscale.com/net/udprelay/status"
"tailscale.com/tailcfg"
"tailscale.com/types/key"
"tailscale.com/types/logger"
@@ -29,6 +34,37 @@ const featureName = "relayserver"
func init() {
feature.Register(featureName)
ipnext.RegisterExtension(featureName, newExtension)
+ localapi.Register("debug-peer-relay-sessions", servePeerRelayDebugSessions)
+}
+
+// servePeerRelayDebugSessions is an HTTP handler for the Local API that
+// returns debug/status information for peer relay sessions being relayed by
+// this Tailscale node. It writes a JSON-encoded [status.ServerStatus] into the
+// HTTP response, or returns an HTTP 405/500 with error text as the body.
+func servePeerRelayDebugSessions(h *localapi.Handler, w http.ResponseWriter, r *http.Request) {
+ if r.Method != "GET" {
+ http.Error(w, "GET required", http.StatusMethodNotAllowed)
+ return
+ }
+
+ var e *extension
+ if ok := h.LocalBackend().FindMatchingExtension(&e); !ok {
+ http.Error(w, "peer relay server extension unavailable", http.StatusInternalServerError)
+ return
+ }
+
+ st, err := e.status()
+ if err != nil {
+ http.Error(w, fmt.Sprintf("failed to retrieve peer relay server status: %v", err), http.StatusInternalServerError)
+ return
+ }
+
+ j, err := json.Marshal(st)
+ if err != nil {
+ http.Error(w, fmt.Sprintf("failed to marshal json: %v", err), http.StatusInternalServerError)
+ return
+ }
+ w.Write(j)
}
// newExtension is an [ipnext.NewExtensionFn] that creates a new relay server
@@ -59,6 +95,27 @@ type extension struct {
type relayServer interface {
AllocateEndpoint(discoA key.DiscoPublic, discoB key.DiscoPublic) (endpoint.ServerEndpoint, error)
Close() error
+ GetSessions() ([]status.ServerSession, error)
+}
+
+// PeerRelaySessionsReq is an empty event bus message type, used to send an
+// async request for peer relay status information to the peer relay server's
+// event loop. The server should respond with a [PeerRelaySessionsResp] via the
+// event bus.
+type PeerRelaySessionsReq struct{}
+
+// PeerRelaySessionsResp is an event bus message type containing peer relay
+// status information. Sent by the peer relay server in response to a
+// [PeerRelaySessionsReq] message.
+type PeerRelaySessionsResp struct {
+ // Status is the current status/config of the peer relay server and all of
+ // its peer relay sessions (if any). May be the zero value if Error is
+ // populated.
+ Status status.ServerStatus
+ // Error contains any error generated by the peer relay server while trying
+ // to gather status; it may or may not be populated regardless of whether
+ // the Status field is valid.
+ Error error
}
// Name implements [ipnext.Extension].
@@ -119,6 +176,8 @@ func (e *extension) consumeEventbusTopics(port int) {
defer close(e.busDoneCh)
eventClient := e.bus.Client("relayserver.extension")
+ debugReqSub := eventbus.Subscribe[PeerRelaySessionsReq](eventClient)
+ debugRespPub := eventbus.Publish[PeerRelaySessionsResp](eventClient)
reqSub := eventbus.Subscribe[magicsock.UDPRelayAllocReq](eventClient)
respPub := eventbus.Publish[magicsock.UDPRelayAllocResp](eventClient)
defer eventClient.Close()
@@ -137,6 +196,32 @@ func (e *extension) consumeEventbusTopics(port int) {
// If reqSub is done, the eventClient has been closed, which is a
// signal to return.
return
+ case <-debugReqSub.Events():
+ st := status.ServerStatus{
+ State: status.Uninitialized,
+ UDPPort: port,
+ Sessions: nil,
+ }
+ if rs == nil {
+ // Don't initialize the server simply for a debug request;
+ // return the status as-is.
+ resp := PeerRelaySessionsResp{st, nil}
+ debugRespPub.Publish(resp)
+ continue
+ }
+ // We know the server is [status.Running] because rs != nil, which
+ // can only be the case if the port is configured and peer relaying
+ // isn't disabled by node attribute.
+ st.State = status.Running
+ sessions, err := rs.GetSessions()
+ if err != nil {
+ prs_err := fmt.Errorf("error retrieving peer relay sessions: %v", err)
+ e.logf(prs_err.Error())
+ debugRespPub.Publish(PeerRelaySessionsResp{Error: prs_err})
+ continue
+ }
+ st.Sessions = sessions
+ debugRespPub.Publish(PeerRelaySessionsResp{st, nil})
case req := <-reqSub.Events():
if rs == nil {
var err error
@@ -188,3 +273,52 @@ func (e *extension) Shutdown() error {
e.shutdown = true
return nil
}
+
+// status gathers and returns current peer relay server status information for
+// this Tailscale node, including if this node is disabled/not configured as a
+// peer relay server, and status of each peer relay session this node is
+// relaying (if any).
+func (e *extension) status() (status.ServerStatus, error) {
+ st := status.ServerStatus{
+ State: status.Uninitialized,
+ UDPPort: -1,
+ Sessions: nil,
+ }
+
+ e.mu.Lock()
+ running := e.busDoneCh != nil
+ shutdown := e.shutdown
+ port := e.port
+ disabled := e.hasNodeAttrDisableRelayServer
+ e.mu.Unlock()
+
+ if port == nil {
+ st.State = status.NotConfigured
+ return st, nil
+ }
+
+ st.UDPPort = *port
+ if disabled {
+ st.State = status.Disabled
+ return st, nil
+ }
+
+ if shutdown {
+ st.State = status.ShutDown
+ return st, nil
+ }
+
+ if !running {
+ // Leave state as Uninitialized.
+ return st, nil
+ }
+
+ client := e.bus.Client("relayserver.debug-peer-relay-sessions")
+ defer client.Close()
+ debugReqPub := eventbus.Publish[PeerRelaySessionsReq](client)
+ debugRespSub := eventbus.Subscribe[PeerRelaySessionsResp](client)
+
+ debugReqPub.Publish(PeerRelaySessionsReq{})
+ resp := <-debugRespSub.Events()
+ return resp.Status, resp.Error
+}
diff --git a/net/udprelay/server.go b/net/udprelay/server.go
index aece3bc59..2e43faae5 100644
--- a/net/udprelay/server.go
+++ b/net/udprelay/server.go
@@ -27,6 +27,7 @@ import (
"tailscale.com/net/packet"
"tailscale.com/net/stun"
"tailscale.com/net/udprelay/endpoint"
+ "tailscale.com/net/udprelay/status"
"tailscale.com/tstime"
"tailscale.com/types/key"
"tailscale.com/types/logger"
@@ -90,10 +91,15 @@ type serverEndpoint struct {
boundAddrPorts [2]netip.AddrPort // or zero value if a handshake has never completed for that relay leg
lastSeen [2]time.Time // TODO(jwhited): consider using mono.Time
challenge [2][disco.BindUDPRelayChallengeLen]byte
+ packetsRx [2]uint64 // num packets received from/sent by each client after active state reached
+ bytesRx [2]uint64 // num bytes received from/sent by each client after active state reached
lamportID uint64
vni uint32
allocatedAt time.Time
+
+ // status is the overall state of this server endpoint's peer relay session establishment/operation.
+ status status.SessionStatus
}
func (e *serverEndpoint) handleDiscoControlMsg(from netip.AddrPort, senderIndex int, discoMsg disco.Message, conn *net.UDPConn, serverDisco key.DiscoPublic) {
@@ -150,6 +156,7 @@ func (e *serverEndpoint) handleDiscoControlMsg(from netip.AddrPort, senderIndex
box := e.discoSharedSecrets[senderIndex].Seal(m.AppendMarshal(nil))
reply = append(reply, box...)
conn.WriteMsgUDPAddrPort(reply, nil, from)
+ e.status = status.Binding
return
case *disco.BindUDPRelayEndpointAnswer:
err := validateVNIAndRemoteKey(discoMsg.BindUDPRelayEndpointCommon)
@@ -167,6 +174,13 @@ func (e *serverEndpoint) handleDiscoControlMsg(from netip.AddrPort, senderIndex
}
// Handshake complete. Update the binding for this sender.
e.boundAddrPorts[senderIndex] = from
+
+ // If both clients have bound into the endpoint, we've moved from the
+ // Binding phase to the Pinging phase of peer relay session
+ // establishment.
+ if e.isBound() {
+ e.status = status.Pinging
+ }
e.lastSeen[senderIndex] = time.Now() // record last seen as bound time
return
default:
@@ -220,13 +234,24 @@ func (e *serverEndpoint) handlePacket(from netip.AddrPort, gh packet.GeneveHeade
case from == e.boundAddrPorts[0]:
e.lastSeen[0] = time.Now()
to = e.boundAddrPorts[1]
+ e.packetsRx[0]++
+ e.bytesRx[0] += uint64(len(b))
case from == e.boundAddrPorts[1]:
e.lastSeen[1] = time.Now()
to = e.boundAddrPorts[0]
+ e.packetsRx[1]++
+ e.bytesRx[1] += uint64(len(b))
default:
// unrecognized source
return
}
+
+ // If we reach here and packets are flowing bidirectionally, the
+ // Pinging phase of session establishment is complete and the session
+ // is active.
+ if e.status == status.Pinging && e.packetsRx[0] > 1 && e.packetsRx[1] > 1 {
+ e.status = status.Active
+ }
// Relay the packet towards the other party via the socket associated
// with the destination's address family. If source and destination
// address families are matching we tx on the same socket the packet
@@ -237,6 +262,7 @@ func (e *serverEndpoint) handlePacket(from netip.AddrPort, gh packet.GeneveHeade
} else if otherAFSocket != nil {
otherAFSocket.WriteMsgUDPAddrPort(b, nil, to)
}
+
return
}
@@ -644,6 +670,7 @@ func (s *Server) AllocateEndpoint(discoA, discoB key.DiscoPublic) (endpoint.Serv
discoPubKeys: pair,
lamportID: s.lamportID,
allocatedAt: time.Now(),
+ status: status.Allocating,
}
e.discoSharedSecrets[0] = s.disco.Shared(e.discoPubKeys.Get()[0])
e.discoSharedSecrets[1] = s.disco.Shared(e.discoPubKeys.Get()[1])
@@ -663,3 +690,52 @@ func (s *Server) AllocateEndpoint(discoA, discoB key.DiscoPublic) (endpoint.Serv
SteadyStateLifetime: tstime.GoDuration{Duration: s.steadyStateLifetime},
}, nil
}
+
+// extractClientInfo constructs a [status.ClientInfo] for one of the two peer
+// relay clients involved in this session.
+func extractClientInfo(idx int, ep *serverEndpoint) status.ClientInfo {
+ if idx != 0 && idx != 1 {
+ panic(fmt.Sprintf("idx passed to extractClientInfo() must be 0 or 1; got %d", idx))
+ }
+
+ // If neither the bound or handshake addrports are valid, just pass on the
+ // invalid zero value; users need to call ClientInfo.Endpoint.IsValid()
+ // before use.
+ var ap netip.AddrPort
+ if ep.boundAddrPorts[idx].IsValid() {
+ ap = ep.boundAddrPorts[idx]
+ } else if ep.handshakeAddrPorts[idx].IsValid() {
+ ap = ep.handshakeAddrPorts[idx]
+ }
+ return status.ClientInfo{
+ Endpoint: ap,
+ ShortDisco: ep.discoPubKeys.Get()[idx].ShortString(),
+ PacketsTx: ep.packetsRx[idx],
+ BytesTx: ep.bytesRx[idx],
+ }
+}
+
+// GetSessions returns an array of peer relay session statuses, with each
+// entry containing detailed info about the server and clients involved in
+// each session. This information is intended for debugging/status UX, and
+// should not be relied on for any purpose outside of that.
+func (s *Server) GetSessions() ([]status.ServerSession, error) {
+ var sessions = make([]status.ServerSession, 0)
+ for _, se := range s.byDisco {
+ c1 := extractClientInfo(0, se)
+ c2 := extractClientInfo(1, se)
+ si := status.ServerInfo{
+ // TODO (dylan): Is this the correct addrPort to be using here?
+ Endpoint: s.addrPorts[0],
+ ShortDisco: s.discoPublic.ShortString(),
+ }
+ sessions = append(sessions, status.ServerSession{
+ Status: se.status,
+ VNI: se.vni,
+ Client1: c1,
+ Client2: c2,
+ Server: si,
+ })
+ }
+ return sessions, nil
+}
diff --git a/net/udprelay/status/status.go b/net/udprelay/status/status.go
new file mode 100644
index 000000000..ed6c0d3bb
--- /dev/null
+++ b/net/udprelay/status/status.go
@@ -0,0 +1,196 @@
+// Copyright (c) Tailscale Inc & AUTHORS
+// SPDX-License-Identifier: BSD-3-Clause
+
+// Package status contains types relating to the status of peer relay sessions
+// between peer relay client nodes via a peer relay server.
+package status
+
+import (
+ "fmt"
+ "net/netip"
+)
+
+// ServerState is the current state of the peer relay server extension.
+type ServerState int
+
+const (
+ // Uninitialized indicates the peer relay server hasn't been initialized
+ // yet on this node. It does NOT imply the peer relay server can be
+ // initialized for this node; the node may not be configured as a peer
+ // relay server yet, or may be disabled by node attribute.
+ Uninitialized ServerState = iota
+ // NotConfigured indicates the peer relay server port has not been set for
+ // this node; a node cannot be a peer relay server until the port has been
+ // set.
+ NotConfigured
+ // Disabled indicates the peer relay server has been disabled by a node
+ // attribute pushed via C2N.
+ Disabled
+ // Running indicates the peer relay server has been initialized and can
+ // relay sessions between peers on the configured UDP port.
+ Running
+ // ShutDown indicates the peer relay server extension has been told to
+ // shut down, and can no longer relay sessions between peers.
+ ShutDown
+)
+
+// ServerStatus contains the listening UDP port, state, and active sessions (if
+// any) for this node's peer relay server at a point in time.
+type ServerStatus struct {
+ // State is the current phase/state in the peer relay server's state
+ // machine. See [ServerState].
+ State ServerState
+ // UDPPort is the UDP port number that the peer relay server is listening
+ // for incoming peer relay endpoint allocation requests on, as configured
+ // by the user with 'tailscale set --relay-server-port=<PORT>'. If State is
+ // [NotConfigured], this field will be -1.
+ UDPPort int
+ // Sessions is an array of detailed status information about each peer
+ // relay session that this node's peer relay server is involved with. It
+ // may be empty.
+ Sessions []ServerSession
+}
+
+// ServerInfo contains status-related information about the peer relay server
+// involved in a single peer relay session.
+type ServerInfo struct {
+ // Endpoint is the [netip.AddrPort] for the peer relay server's underlay
+ // endpoint participating in the session. Both clients in a session are
+ // bound into the same endpoint on the server. This may be invalid; check
+ // the value with [netip.AddrPort.IsValid] before using.
+ Endpoint netip.AddrPort
+ // ShortDisco is a string representation of the peer relay server's disco
+ // public key. This can be the empty string.
+ ShortDisco string
+}
+
+// String returns a string representation of the [ServerInfo] containing the
+// endpoint address/port and short disco public key.
+func (i *ServerInfo) String() string {
+ disco := i.ShortDisco
+ if disco == "" {
+ disco = "[d:unknown]"
+ }
+
+ if i.Endpoint.IsValid() {
+ return fmt.Sprintf("%v[%s]", i.Endpoint, disco)
+ } else {
+ return fmt.Sprintf("unknown[%s]", disco)
+ }
+}
+
+// ClientInfo contains status-related information about a single peer relay
+// client involved in a single peer relay session.
+type ClientInfo struct {
+ // Endpoint is the [netip.AddrPort] of this peer relay client's underlay
+ // endpoint participating in the session. This may be invalid; check the
+ // value with [netip.AddrPort.IsValid] before using.
+ Endpoint netip.AddrPort
+ // ShortDisco is a string representation of this peer relay client's disco
+ // public key. This can be the empty string.
+ ShortDisco string
+ // PacketsTx is the number of packets this peer relay client has sent to
+ // the other client via the relay server after completing session
+ // establishment. This is identical to the number of packets that the peer
+ // relay server has received from this client.
+ PacketsTx uint64
+ // BytesTx is the total overlay bytes this peer relay client has sent to
+ // the other client via the relay server after completing session
+ // establishment. This is identical to the total overlay bytes that the
+ // peer relay server has received from this client.
+ BytesTx uint64
+}
+
+// String returns a string representation of the [ClientInfo] containing the
+// endpoint address/port, short disco public key, and packet/byte counts.
+func (i *ClientInfo) String() string {
+ disco := i.ShortDisco
+ if disco == "" {
+ disco = "[d:unknown]"
+ }
+
+ if i.Endpoint.IsValid() {
+ return fmt.Sprintf("%v[%s] tx %v(%vB)", i.Endpoint, i.ShortDisco, i.PacketsTx, i.BytesTx)
+ } else {
+ return fmt.Sprintf("unknown[%s] tx %v(%vB)", disco, i.PacketsTx, i.BytesTx)
+ }
+}
+
+// ServerSession contains status information for a single session between two
+// peer relay clients, which are relayed via one peer relay server. This is the
+// status as seen by the peer relay server; each client node may have a
+// different view of the session's current status based on connectivity and
+// where the client is in the peer relay endpoint setup (allocation, binding,
+// pinging, active).
+type ServerSession struct {
+ // Status is the current state of the session, as seen by the peer relay
+ // server. It contains the status of each phase of session setup and usage:
+ // endpoint allocation, endpoint binding, disco ping/pong, and active.
+ Status SessionStatus
+ // VNI is the Virtual Network Identifier for this peer relay session, which
+ // comes from the Geneve header and is unique to this session.
+ VNI uint32
+ // Server contains status information about the peer relay server involved
+ // in this session.
+ Server ServerInfo
+ // Client1 contains status information about one of the two peer relay
+ // clients involved in this session. Note that 'Client1' does NOT mean this
+ // was/wasn't the allocating client, or the first client to bind, etc; this
+ // is just one client of two.
+ Client1 ClientInfo
+ // Client2 contains status information about one of the two peer relay
+ // clients involved in this session. Note that 'Client2' does NOT mean this
+ // was/wasn't the allocating client, or the second client to bind, etc;
+ // this is just one client of two.
+ Client2 ClientInfo
+}
+
+// SessionStatus is the current state of a peer relay session, as seen by the
+// peer relay server that's relaying the session.
+type SessionStatus int
+
+const (
+ // NotStarted is the default "unknown" state for a session; it should not
+ // be seen outside of initialization.
+ NotStarted SessionStatus = iota
+ // Allocating indicates a peer relay client has contacted the peer relay
+ // server with a valid endpoint allocation request, and the server is in
+ // the process of allocating it. A session remains in this state until one
+ // of the two clients begins the Binding process.
+ Allocating
+ // Binding indicates at least one of the two peer relay clients has started
+ // the endpoint binding handshake with the peer relay server's endpoint for
+ // this session. A session remains in this state until both clients have
+ // completed the binding handshake and are bound into the endpoint.
+ Binding
+ // Pinging indicates the two peer relay clients should be sending disco
+ // ping/pong messages to one another to confirm peer relay session
+ // connectivity via the peer relay server endpoint. We don't actually
+ // monitor the disco ping/pong messages between the clients; we move into
+ // this state when Binding is complete, and move out of this state to
+ // [Active] when we see packets being exchanged bidirectionally over the
+ // session endpoint. As such, Pinging is currently an implicit intermediate
+ // state rather than a "confirmed by looking at disco ping/pong" state.
+ Pinging
+ // Active indicates the peer relay clients are both bound into the peer
+ // relay session, have completed their disco pinging process, and are
+ // bidirectionally exchanging packets via the peer relay server.
+ Active
+)
+
+// String returns a short, human-readable string representation of the current
+// [SessionStatus].
+func (s SessionStatus) String() string {
+ switch s {
+ case Allocating:
+ return "allocating endpoint"
+ case Binding:
+ return "binding endpoint"
+ case Pinging:
+ return "clients pinging"
+ case Active:
+ return "session active"
+ default:
+ return "unknown"
+ }
+}
diff --git a/tsnet/depaware.txt b/tsnet/depaware.txt
index da3175b8c..1729575ae 100644
--- a/tsnet/depaware.txt
+++ b/tsnet/depaware.txt
@@ -303,6 +303,7 @@ tailscale.com/tsnet dependencies: (generated by github.com/tailscale/depaware)
💣 tailscale.com/net/tshttpproxy from tailscale.com/clientupdate/distsign+
tailscale.com/net/tstun from tailscale.com/tsd+
tailscale.com/net/udprelay/endpoint from tailscale.com/wgengine/magicsock
+ tailscale.com/net/udprelay/status from tailscale.com/client/local
tailscale.com/omit from tailscale.com/ipn/conffile
tailscale.com/paths from tailscale.com/client/local+
💣 tailscale.com/portlist from tailscale.com/ipn/ipnlocal