client/local, feature/relayserver, net/udprelay: move session status to its own package

Signed-off-by: Dylan Bargatze <dylan@tailscale.com>
This commit is contained in:
Dylan Bargatze 2025-07-29 16:01:29 -04:00
parent 2705d80902
commit 70569bb937
No known key found for this signature in database
5 changed files with 223 additions and 250 deletions

View File

@ -35,7 +35,7 @@ import (
"tailscale.com/ipn"
"tailscale.com/ipn/ipnstate"
"tailscale.com/net/netutil"
"tailscale.com/net/udprelay/endpoint"
"tailscale.com/net/udprelay/status"
"tailscale.com/paths"
"tailscale.com/safesocket"
"tailscale.com/tailcfg"
@ -1641,12 +1641,12 @@ func (lc *Client) DebugSetExpireIn(ctx context.Context, d time.Duration) error {
// DebugPeerRelaySessions returns debug information about the current peer
// relay sessions running through this node.
func (lc *Client) DebugPeerRelaySessions(ctx context.Context) ([]endpoint.PeerRelayServerSession, error) {
func (lc *Client) DebugPeerRelaySessions(ctx context.Context) ([]status.ServerSession, 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[[]endpoint.PeerRelayServerSession](body)
return decodeJSON[[]status.ServerSession](body)
}
// StreamDebugCapture streams a pcap-formatted packet capture.

View File

@ -19,6 +19,7 @@ import (
"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"
@ -120,7 +121,7 @@ type extension struct {
type relayServer interface {
AllocateEndpoint(discoA key.DiscoPublic, discoB key.DiscoPublic) (endpoint.ServerEndpoint, error)
Close() error
GetSessions() ([]endpoint.PeerRelayServerSession, error)
GetSessions() ([]status.ServerSession, error)
}
// TODO (dylan): doc comments
@ -128,7 +129,7 @@ type PeerRelaySessionsReq struct{}
// TODO (dylan): doc comments
type PeerRelaySessionsResp struct {
Sessions []endpoint.PeerRelayServerSession
Sessions []status.ServerSession
Error error
}

View File

@ -62,216 +62,3 @@ type ServerEndpoint struct {
// bidirectional data flow.
SteadyStateLifetime tstime.GoDuration
}
// TODO (dylan): doc comments
type PeerRelayServerAllocStatus int
const (
EndpointAllocNotStarted PeerRelayServerAllocStatus = iota
// EndpointAllocRequestReceived by the peer relay server from the allocating client
EndpointAllocRequestReceived
// EndpointAllocated on the peer relay server, but response not yet sent to allocating client
EndpointAllocated
// EndpointAllocResponseSent from the peer relay server to allocating client
EndpointAllocResponseSent
// TODO (dylan): Should we have a status here for dead allocs that weren't bound before the
// BindLifetime timer expired?
EndpointAllocExpired
)
func (s PeerRelayServerAllocStatus) String() string {
switch s {
case EndpointAllocNotStarted:
return "alloc not started"
case EndpointAllocRequestReceived:
return "alloc request received"
case EndpointAllocated:
return "endpoint allocated"
case EndpointAllocResponseSent:
return "alloc complete"
case EndpointAllocExpired:
return "expired"
default:
return "unknown"
}
}
// PeerRelayServerBindStatus is the current status of the endpoint binding
// handshake between the peer relay server and a SINGLE peer relay client. Both
// clients need to bind into an endpoint for a peer relay session to be bound,
// so a peer relay server will have two PeerRelayServerBindStatus fields to
// track per session.
type PeerRelayServerBindStatus int
// TODO (dylan): doc comments
const (
EndpointBindNotStarted PeerRelayServerBindStatus = iota
EndpointBindRequestReceived
EndpointBindChallengeSent
EndpointBindAnswerReceived
)
func (s PeerRelayServerBindStatus) String() string {
switch s {
case EndpointBindNotStarted:
return "binding not started"
case EndpointBindRequestReceived:
return "bind request received"
case EndpointBindChallengeSent:
return "bind challenge sent"
case EndpointBindAnswerReceived:
return "bind complete"
default:
return "unknown"
}
}
// PeerRelayServerPingStatus is the current status of a SINGLE SIDE of the
// bidirectional disco ping exchange between two peer relay clients, as seen by
// the peer relay server. As each client will send a disco ping and should
// receive a disco pong from the other client in response, a peer relay server
// will have two PeerRelayServerPingStatus fields to track per session.
type PeerRelayServerPingStatus int
// TODO (dylan): doc comments
const (
DiscoPingNotStarted PeerRelayServerPingStatus = iota
DiscoPingSeen
DiscoPongSeen
)
func (s PeerRelayServerPingStatus) String() string {
switch s {
case DiscoPingNotStarted:
return "ping not started"
case DiscoPingSeen:
return "disco ping seen"
case DiscoPongSeen:
return "disco pong seen"
default:
return "unknown"
}
}
// TODO (dylan): doc comments
type PeerRelayServerStatus int
// TODO (dylan): doc comments
const (
AllocatingEndpoint PeerRelayServerStatus = iota
BindingEndpoint
BidirectionalPinging
ServerSessionEstablished
)
func (s PeerRelayServerStatus) String() string {
switch s {
case AllocatingEndpoint:
return "allocating endpoint allocation"
case BindingEndpoint:
return "binding endpoint"
case BidirectionalPinging:
return "clients pinging"
case ServerSessionEstablished:
return "session established"
default:
return "unknown"
}
}
// TODO (dylan): doc comments
type PeerRelayServerSessionStatus struct {
AllocStatus PeerRelayServerAllocStatus
ClientBindStatus [2]PeerRelayServerBindStatus
ClientPingStatus [2]PeerRelayServerPingStatus
ClientPacketsRx [2]uint64
ClientPacketsFwd [2]uint64
OverallStatus PeerRelayServerStatus
}
func NewPeerRelayServerSessionStatus() PeerRelayServerSessionStatus {
return PeerRelayServerSessionStatus{
AllocStatus: EndpointAllocNotStarted,
ClientBindStatus: [2]PeerRelayServerBindStatus{EndpointBindNotStarted, EndpointBindNotStarted},
ClientPingStatus: [2]PeerRelayServerPingStatus{DiscoPingNotStarted, DiscoPingNotStarted},
OverallStatus: AllocatingEndpoint,
}
}
// TODO (dylan): doc comments
type PeerRelayClientAllocStatus int
const (
// EndpointAllocRequestSent from the allocating client to the peer relay server via DERP
EndpointAllocRequestSent PeerRelayClientAllocStatus = iota
// EndpointAllocResponseReceived by the allocating client from the peer relay server via DERP
EndpointAllocResponseReceived
// CallMeMaybeViaSent from the allocating client to the target client via DERP
CallMeMaybeViaSent
// CallMeMaybeViaReceived by the target client from the allocating client via DERP
CallMeMaybeViaReceived
)
// TODO (dylan): doc comments
type PeerRelayClientBindStatus int
const (
// EndpointBindHandshakeSent from this client to the peer relay server
EndpointBindHandshakeSent PeerRelayClientBindStatus = iota
// EndpointBindChallengeReceived by this client from the peer relay server
EndpointBindChallengeReceived
// EndpointBindAnswerSent from this client to the peer relay server
EndpointBindAnswerSent
)
// TODO (dylan): doc comments
type PeerRelayClientPingStatus int
// TODO (dylan): doc comments
const (
DiscoPingSent PeerRelayClientPingStatus = iota
DiscoPingReceived
)
// TODO (dylan): doc comments
type PeerRelayClientStatus int
// TODO (dylan): doc comments
const (
EndpointAllocation PeerRelayClientStatus = iota
EndpointBinding
Pinging
ClientSessionEstablished
)
// TODO (dylan): doc comments
type PeerRelayClientSessionStatus struct {
AllocStatus PeerRelayClientAllocStatus
BindStatus PeerRelayClientBindStatus
PingStatus PeerRelayClientPingStatus
OverallStatus PeerRelayClientStatus
}
// TODO (dylan): doc comments
type PeerRelaySessionBaseStatus struct {
VNI uint32
ClientShortDisco [2]string
ClientEndpoint [2]netip.AddrPort
ServerShortDisco string
ServerEndpoint netip.AddrPort
}
// TODO (dylan): doc comments
type PeerRelayServerSession struct {
Status PeerRelayServerSessionStatus
PeerRelaySessionBaseStatus
}
// TODO (dylan): doc comments
type PeerRelayClientSession struct {
Status PeerRelayClientStatus
PeerRelaySessionBaseStatus
}

View File

@ -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"
@ -95,7 +96,7 @@ type serverEndpoint struct {
vni uint32
allocatedAt time.Time
status endpoint.PeerRelayServerSessionStatus
status status.SessionStatus
}
func (e *serverEndpoint) handleDiscoControlMsg(from netip.AddrPort, senderIndex int, discoMsg disco.Message, conn *net.UDPConn, serverDisco key.DiscoPublic) {
@ -137,7 +138,7 @@ func (e *serverEndpoint) handleDiscoControlMsg(from netip.AddrPort, senderIndex
e.handshakeAddrPorts[senderIndex] = from
// TODO (dylan): assert current e.status.AllocStatus is EndpointAllocated
// TODO (dylan): assert e.status.ClientBindStatus[senderIndex] is not already EndpointBindRequestReceived or later
e.status.ClientBindStatus[senderIndex] = endpoint.EndpointBindRequestReceived
e.status.ClientBindStatus[senderIndex] = status.EndpointBindRequestReceived
m := new(disco.BindUDPRelayEndpointChallenge)
m.VNI = e.vni
m.Generation = discoMsg.Generation
@ -155,8 +156,8 @@ 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.ClientBindStatus[senderIndex] = endpoint.EndpointBindChallengeSent
e.status.OverallStatus = endpoint.BindingEndpoint
e.status.ClientBindStatus[senderIndex] = status.EndpointBindChallengeSent
e.status.OverallStatus = status.Binding
return
case *disco.BindUDPRelayEndpointAnswer:
err := validateVNIAndRemoteKey(discoMsg.BindUDPRelayEndpointCommon)
@ -176,11 +177,11 @@ func (e *serverEndpoint) handleDiscoControlMsg(from netip.AddrPort, senderIndex
e.boundAddrPorts[senderIndex] = from
// TODO (dylan): assert e.status.AllocStatus is EndpointAllocated
// TODO (dylan): assert e.status.ClientBindStatus[senderIndex] is endpoint.EndpointBindChallengeSent
// TODO (dylan): assert e.status.ClientBindStatus[senderIndex] is status.EndpointBindChallengeSent
// TODO (dylan): assert e.status.ClientBindStatus[senderIndex] is not already EndpointBindAnswerReceived or later
e.status.ClientBindStatus[senderIndex] = endpoint.EndpointBindAnswerReceived
e.status.ClientBindStatus[senderIndex] = status.EndpointBindAnswerReceived
if e.isBound() {
e.status.OverallStatus = endpoint.BidirectionalPinging
e.status.OverallStatus = status.Pinging
}
e.lastSeen[senderIndex] = time.Now() // record last seen as bound time
return
@ -237,10 +238,10 @@ func (e *serverEndpoint) handlePacket(from netip.AddrPort, gh packet.GeneveHeade
to = e.boundAddrPorts[1]
e.status.ClientPacketsRx[0]++
switch e.status.ClientPingStatus[0] {
case endpoint.DiscoPingNotStarted:
e.status.ClientPingStatus[0] = endpoint.DiscoPingSeen
case endpoint.DiscoPingSeen:
e.status.ClientPingStatus[0] = endpoint.DiscoPongSeen
case status.DiscoPingNotStarted:
e.status.ClientPingStatus[0] = status.DiscoPingSeen
case status.DiscoPingSeen:
e.status.ClientPingStatus[0] = status.DiscoPongSeen
default:
break
}
@ -250,10 +251,10 @@ func (e *serverEndpoint) handlePacket(from netip.AddrPort, gh packet.GeneveHeade
to = e.boundAddrPorts[0]
e.status.ClientPacketsRx[1]++
switch e.status.ClientPingStatus[1] {
case endpoint.DiscoPingNotStarted:
e.status.ClientPingStatus[1] = endpoint.DiscoPingSeen
case endpoint.DiscoPingSeen:
e.status.ClientPingStatus[1] = endpoint.DiscoPongSeen
case status.DiscoPingNotStarted:
e.status.ClientPingStatus[1] = status.DiscoPingSeen
case status.DiscoPingSeen:
e.status.ClientPingStatus[1] = status.DiscoPongSeen
default:
break
}
@ -263,8 +264,8 @@ func (e *serverEndpoint) handlePacket(from netip.AddrPort, gh packet.GeneveHeade
return
}
if e.status.OverallStatus == endpoint.BidirectionalPinging && e.status.ClientPingStatus[0] == endpoint.DiscoPongSeen && e.status.ClientPingStatus[1] == endpoint.DiscoPongSeen {
e.status.OverallStatus = endpoint.ServerSessionEstablished
if e.status.OverallStatus == status.Pinging && e.status.ClientPingStatus[0] == status.DiscoPongSeen && e.status.ClientPingStatus[1] == status.DiscoPongSeen {
e.status.OverallStatus = status.Established
}
// Relay the packet towards the other party via the socket associated
// with the destination's address family. If source and destination
@ -680,13 +681,14 @@ func (s *Server) AllocateEndpoint(discoA, discoB key.DiscoPublic) (endpoint.Serv
}
s.lamportID++
status := endpoint.NewPeerRelayServerSessionStatus()
status.AllocStatus = endpoint.EndpointAllocRequestReceived
st := status.NewSessionStatus()
st.AllocStatus = status.EndpointAllocRequestReceived
st.OverallStatus = status.Allocating
e = &serverEndpoint{
discoPubKeys: pair,
lamportID: s.lamportID,
allocatedAt: time.Now(),
status: status,
status: st,
}
e.discoSharedSecrets[0] = s.disco.Shared(e.discoPubKeys.Get()[0])
e.discoSharedSecrets[1] = s.disco.Shared(e.discoPubKeys.Get()[1])
@ -707,8 +709,9 @@ func (s *Server) AllocateEndpoint(discoA, discoB key.DiscoPublic) (endpoint.Serv
}, nil
}
func (s *Server) GetSessions() ([]endpoint.PeerRelayServerSession, error) {
var sessions = make([]endpoint.PeerRelayServerSession, 0)
// TODO (dylan): doc comments
func (s *Server) GetSessions() ([]status.ServerSession, error) {
var sessions = make([]status.ServerSession, 0)
for k, v := range s.byDisco {
var c1Ep, c2Ep netip.AddrPort
@ -725,17 +728,15 @@ func (s *Server) GetSessions() ([]endpoint.PeerRelayServerSession, error) {
} else if v.handshakeAddrPorts[1].IsValid() {
c2Ep = v.handshakeAddrPorts[1]
}
sessions = append(sessions, endpoint.PeerRelayServerSession{
sessions = append(sessions, status.ServerSession{
// TODO (dylan): fix overall status
Status: v.status,
PeerRelaySessionBaseStatus: endpoint.PeerRelaySessionBaseStatus{
VNI: v.vni,
ClientShortDisco: [2]string{c1Disco, c2Disco},
ClientEndpoint: [2]netip.AddrPort{c1Ep, c2Ep},
ServerShortDisco: s.discoPublic.ShortString(),
// TODO (dylan): disambiguate which addrPort to use here
ServerEndpoint: s.addrPorts[0],
},
Status: v.status,
VNI: v.vni,
ClientShortDisco: [2]string{c1Disco, c2Disco},
ClientEndpoint: [2]netip.AddrPort{c1Ep, c2Ep},
ServerShortDisco: s.discoPublic.ShortString(),
// TODO (dylan): disambiguate which addrPort to use here
ServerEndpoint: s.addrPorts[0],
})
}
return sessions, nil

View File

@ -0,0 +1,184 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
// Package status contains types relating to the status of peer relay sessions
// between nodes via a peer relay server.
package status
import "net/netip"
// ServerSession contains status information for a single session between two
// peer relay clients relayed via a 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.
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.
// TODO (dylan): confirm these statuses/state machines
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
// ClientShortDisco is a string representation of each peer relay client's
// disco public key (one string for each of the two clients).
// TODO (dylan): can either of these ever be nil?
ClientShortDisco [2]string
// ClientEndpoint is the [netip.AddrPort] of each peer relay client's
// endpoint participating in the session (one endpoint for each of the two
// clients).
// TODO (dylan): can either of these ever be nil?
ClientEndpoint [2]netip.AddrPort
// ServerShortDisco is a string representation of the peer relay server's
// disco public key.
// TODO (dylan): can there be a different disco key per-client?
ServerShortDisco string
// ServerEndpoint is the [netip.AddrPort] for the peer relay server's
// endpoint participating in the session (one endpoint for each of the two
// clients).
// TODO (dylan): can there be a different endpoint per-client?
ServerEndpoint netip.AddrPort
}
// TODO (dylan): doc comments
type SessionStatus struct {
AllocStatus AllocStatus
ClientBindStatus [2]BindStatus
ClientPingStatus [2]PingStatus
ClientPacketsRx [2]uint64
ClientPacketsFwd [2]uint64
OverallStatus OverallSessionStatus
}
// TODO (dylan): doc comments
func NewSessionStatus() SessionStatus {
return SessionStatus{
AllocStatus: EndpointAllocNotStarted,
ClientBindStatus: [2]BindStatus{EndpointBindNotStarted, EndpointBindNotStarted},
ClientPingStatus: [2]PingStatus{DiscoPingNotStarted, DiscoPingNotStarted},
OverallStatus: Allocating,
}
}
// TODO (dylan): doc comments
type AllocStatus int
// TODO (dylan): doc comments
const (
EndpointAllocNotStarted AllocStatus = iota
// EndpointAllocRequestReceived by the peer relay server from the allocating client
EndpointAllocRequestReceived
// EndpointAllocated on the peer relay server, but response not yet sent to allocating client
EndpointAllocated
// EndpointAllocResponseSent from the peer relay server to allocating client
EndpointAllocResponseSent
// TODO (dylan): Should we have a status here for dead allocs that weren't bound before the
// BindLifetime timer expired?
EndpointAllocExpired
)
func (s AllocStatus) String() string {
switch s {
case EndpointAllocNotStarted:
return "alloc not started"
case EndpointAllocRequestReceived:
return "alloc request received"
case EndpointAllocated:
return "endpoint allocated"
case EndpointAllocResponseSent:
return "alloc complete"
case EndpointAllocExpired:
return "expired"
default:
return "unknown"
}
}
// BindStatus is the current status of the endpoint binding handshake between
// the peer relay server and a SINGLE peer relay client. Both clients need to
// bind into an endpoint for a peer relay session to be bound, so a peer relay
// server will have two BindStatus fields to track per session.
type BindStatus int
// TODO (dylan): doc comments
const (
EndpointBindNotStarted BindStatus = iota
EndpointBindRequestReceived
EndpointBindChallengeSent
EndpointBindAnswerReceived
)
func (s BindStatus) String() string {
switch s {
case EndpointBindNotStarted:
return "binding not started"
case EndpointBindRequestReceived:
return "bind request received"
case EndpointBindChallengeSent:
return "bind challenge sent"
case EndpointBindAnswerReceived:
return "bind complete"
default:
return "unknown"
}
}
// PingStatus is the current status of a SINGLE SIDE of the
// bidirectional disco ping exchange between two peer relay clients, as seen by
// the peer relay server. As each client will send a disco ping and should
// receive a disco pong from the other client in response, a peer relay server
// will have two PingStatus fields to track per session.
type PingStatus int
// TODO (dylan): doc comments
const (
DiscoPingNotStarted PingStatus = iota
DiscoPingSeen
DiscoPongSeen
)
// TODO (dylan): doc comments
func (s PingStatus) String() string {
switch s {
case DiscoPingNotStarted:
return "ping not started"
case DiscoPingSeen:
return "disco ping seen"
case DiscoPongSeen:
return "disco pong seen"
default:
return "unknown"
}
}
// TODO (dylan): doc comments
type OverallSessionStatus int
// TODO (dylan): doc comments
const (
NotStarted OverallSessionStatus = iota
Allocating
Binding
Pinging
Established
Idle
)
// String returns a short, human-readable string representation of the current
// [OverallSessionStatus].
func (s OverallSessionStatus) String() string {
switch s {
case Allocating:
return "allocating endpoint"
case Binding:
return "binding endpoint"
case Pinging:
return "clients pinging"
case Established:
return "session established"
default:
return "unknown"
}
}