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"
"tailscale.com/ipn/ipnstate" "tailscale.com/ipn/ipnstate"
"tailscale.com/net/netutil" "tailscale.com/net/netutil"
"tailscale.com/net/udprelay/endpoint" "tailscale.com/net/udprelay/status"
"tailscale.com/paths" "tailscale.com/paths"
"tailscale.com/safesocket" "tailscale.com/safesocket"
"tailscale.com/tailcfg" "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 // DebugPeerRelaySessions returns debug information about the current peer
// relay sessions running through this node. // 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) body, err := lc.send(ctx, "GET", "/localapi/v0/debug-peer-relay-sessions", 200, nil)
if err != nil { if err != nil {
return nil, fmt.Errorf("error %w: %s", err, body) 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. // StreamDebugCapture streams a pcap-formatted packet capture.

View File

@ -19,6 +19,7 @@ import (
"tailscale.com/ipn/localapi" "tailscale.com/ipn/localapi"
"tailscale.com/net/udprelay" "tailscale.com/net/udprelay"
"tailscale.com/net/udprelay/endpoint" "tailscale.com/net/udprelay/endpoint"
"tailscale.com/net/udprelay/status"
"tailscale.com/tailcfg" "tailscale.com/tailcfg"
"tailscale.com/types/key" "tailscale.com/types/key"
"tailscale.com/types/logger" "tailscale.com/types/logger"
@ -120,7 +121,7 @@ type extension struct {
type relayServer interface { type relayServer interface {
AllocateEndpoint(discoA key.DiscoPublic, discoB key.DiscoPublic) (endpoint.ServerEndpoint, error) AllocateEndpoint(discoA key.DiscoPublic, discoB key.DiscoPublic) (endpoint.ServerEndpoint, error)
Close() error Close() error
GetSessions() ([]endpoint.PeerRelayServerSession, error) GetSessions() ([]status.ServerSession, error)
} }
// TODO (dylan): doc comments // TODO (dylan): doc comments
@ -128,7 +129,7 @@ type PeerRelaySessionsReq struct{}
// TODO (dylan): doc comments // TODO (dylan): doc comments
type PeerRelaySessionsResp struct { type PeerRelaySessionsResp struct {
Sessions []endpoint.PeerRelayServerSession Sessions []status.ServerSession
Error error Error error
} }

View File

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