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=')") + 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='. 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