diff options
Diffstat (limited to 'net')
| -rw-r--r-- | net/udprelay/server.go | 76 | ||||
| -rw-r--r-- | net/udprelay/status/status.go | 196 |
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" + } +} |
