summaryrefslogtreecommitdiffhomepage
path: root/net
diff options
context:
space:
mode:
Diffstat (limited to 'net')
-rw-r--r--net/udprelay/server.go76
-rw-r--r--net/udprelay/status/status.go196
2 files changed, 272 insertions, 0 deletions
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"
+ }
+}