Documentation
¶
Overview ¶
Package uplink provides a WebSocket Secure (WSS) framed transport for carrying WireGuard packet payloads between a relay-attached peer and its relay/gateway.
The application transport is always WebSocket (/uplink/v1). TLS termination is configurable independently:
- selfsigned (default): uplink terminates TLS with a persisted certificate
- proxy: an upstream reverse proxy terminates TLS; uplink serves plain WS
Each WebSocket binary message carries one uplink protocol frame (12-byte header + payload). The length prefix is retained as compatibility framing.
It owns connection setup, framing, session lifecycle, keepalive, and peer→session registration for reverse traffic. It does not implement routing policy, relay selection, or Netmaker control-plane logic—integrate those in a separate adapter.
For application-layer (HTTP CONNECT) egress, see package l7.
Index ¶
- Constants
- Variables
- func ClientIPFromRequest(r *http.Request) string
- func ComputeHelloProof(ourPriv, peerPub *[32]byte, macInput []byte) (string, error)
- func EndpointURL(host string, port int) string
- func HelloMACInput(h ClientHello) []byte
- func NormaliseUplinkURL(addr string) (string, error)
- func VerifyHelloProof(ourPriv, peerPub *[32]byte, h ClientHello) bool
- type AuthResult
- type Authenticator
- type BackoffConfig
- type Client
- type ClientHello
- type ClientOptions
- type ClientState
- type Conn
- type FrameHeader
- type InMemoryRegistry
- func (r *InMemoryRegistry) Attach(peerID string, sess Session) error
- func (r *InMemoryRegistry) CloseAll()
- func (r *InMemoryRegistry) Detach(peerID string)
- func (r *InMemoryRegistry) DetachSession(peerID string, sess Session)
- func (r *InMemoryRegistry) Get(peerID string) (Session, bool)
- func (r *InMemoryRegistry) Len() int
- func (r *InMemoryRegistry) PeerIDs() []string
- type Logger
- type MetricsSink
- type PacketHandler
- type Server
- type ServerOptions
- type Session
- type SessionRegistry
- type SessionState
- type TLSMode
- type WebSocketConn
- func (c *WebSocketConn) Close() error
- func (c *WebSocketConn) HandshakeStatus() int
- func (c *WebSocketConn) ReadFrame(maxPayload uint32) (FrameHeader, []byte, error)
- func (c *WebSocketConn) RemoteAddr() string
- func (c *WebSocketConn) SetReadDeadline(t time.Time) error
- func (c *WebSocketConn) SetRemoteAddr(addr string)
- func (c *WebSocketConn) SetWriteDeadline(t time.Time) error
- func (c *WebSocketConn) StartPingLoop(ctx context.Context)
- func (c *WebSocketConn) WriteFrame(h FrameHeader, payload []byte) error
Examples ¶
Constants ¶
const ( MsgHello uint8 = iota + 1 MsgHelloAck MsgData MsgPing MsgPong MsgClose MsgError )
Message types (spec §11.2).
const ( // UplinkWSPath is the versioned WebSocket endpoint for TCP uplink. UplinkWSPath = "/uplink/v1" // DefaultMaxMessageSize caps a single WebSocket binary message (frame header + payload). DefaultMaxMessageSize = frameHeaderSize + DefaultMaxFrameSize )
const (
// DefaultMaxFrameSize caps payload length (single frame) to limit memory use.
DefaultMaxFrameSize = 65536
)
const DefaultTLSMode = TLSModeSelfSigned
DefaultTLSMode is used when TLS mode is omitted.
const ProtocolVersion uint8 = 1
Protocol version for Phase 1 framing.
Variables ¶
var ( // ErrNoSession is returned when SendToPeer cannot find an active session for the peer. ErrNoSession = errors.New("proxy: no active session for peer") // ErrSessionClosed is returned when writing to a closed session. ErrSessionClosed = errors.New("proxy: session closed") // ErrInvalidFrame is returned for malformed or oversized frames. ErrInvalidFrame = errors.New("proxy: invalid frame") // ErrProtocolVersion is returned when the peer uses an unsupported protocol version. ErrProtocolVersion = errors.New("proxy: unsupported protocol version") // ErrAuthFailed is returned when authentication fails (client-side). ErrAuthFailed = errors.New("proxy: authentication failed") // ErrServerClosed is returned when the server is not running. ErrServerClosed = errors.New("proxy: server closed") // ErrClientClosed is returned when the client is not running or connection is gone. ErrClientClosed = errors.New("proxy: client not connected") )
Functions ¶
func ClientIPFromRequest ¶
ClientIPFromRequest extracts the connecting client IP for logging. Prefers X-Forwarded-For (first hop) / X-Real-IP when present (reverse-proxy deployments), otherwise uses the TCP RemoteAddr host. Never used for authentication.
func ComputeHelloProof ¶
ComputeHelloProof builds Proof = base64(HMAC-SHA256(X25519(ourPriv, peerPub), macInput)). ourPriv and peerPub are raw 32-byte WireGuard Curve25519 keys.
func EndpointURL ¶
EndpointURL builds a client-facing wss:// URL for the given host and port.
func HelloMACInput ¶
func HelloMACInput(h ClientHello) []byte
HelloMACInput returns the stable byte sequence covered by ClientHello.Proof. Proof itself is excluded so the client can compute the MAC before setting it.
Encoding is length-prefixed to avoid delimiter ambiguity:
version (uint32 BE) || len||node_id || len||relay_peer_id || len||network_id || len||public_key || timestamp (int64 BE)
where each len is a uint32 big-endian byte length of the following field.
func NormaliseUplinkURL ¶
NormaliseUplinkURL accepts wss://…/uplink/v1 or legacy host:port.
func VerifyHelloProof ¶
func VerifyHelloProof(ourPriv, peerPub *[32]byte, h ClientHello) bool
VerifyHelloProof checks Proof against the expected MAC for this hello.
Types ¶
type AuthResult ¶
type AuthResult struct {
PeerID string
RelayPeerID string
NetworkID string
SessionScope map[string]string
}
AuthResult is produced by Authenticator after validating ClientHello.
type Authenticator ¶
type Authenticator interface {
ValidateClientHello(ctx context.Context, hello ClientHello) (*AuthResult, error)
}
Authenticator validates ClientHello after MsgHello (spec §10.2).
type BackoffConfig ¶
type BackoffConfig struct {
Initial time.Duration
Max time.Duration
Factor float64 // >= 1.0; multiplier applied after each failed attempt
}
BackoffConfig controls reconnect delays on the client.
type Client ¶
type Client struct {
// contains filtered or unexported fields
}
Client maintains a WebSocket session to the relay/gateway.
Example ¶
package main
import (
"context"
"crypto/tls"
"fmt"
"time"
"github.com/gravitl/proxy/uplink"
)
func main() {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
c, err := uplink.NewClient(uplink.ClientOptions{
Addr: "wss://relay.example.com/uplink/v1",
ServerName: "relay.example.com",
TLSConfig: &tls.Config{MinVersion: tls.VersionTLS12},
HelloFactory: func() (uplink.ClientHello, error) {
return uplink.ClientHello{
Version: 1, NodeID: "node", RelayPeerID: "relay",
PublicKey: "wg-pubkey", Proof: "proof",
Timestamp: time.Now().Unix(),
}, nil
},
PacketHandler: func(pkt []byte) error {
_ = pkt
return nil
},
})
if err != nil {
panic(err)
}
_ = c.Start(ctx)
out := []byte{0x01}
_ = c.SendPacket(ctx, out)
_ = c.Stop(context.Background())
fmt.Println(c.State())
}
Output:
func NewClient ¶
func NewClient(opts ClientOptions) (*Client, error)
NewClient validates options and returns a Client.
func (*Client) SendPacket ¶
SendPacket sends a DATA frame with the current session ID. Safe for concurrent use.
type ClientHello ¶
type ClientHello struct {
Version int `json:"version"`
NodeID string `json:"node_id"`
RelayPeerID string `json:"relay_peer_id"`
NetworkID string `json:"network_id,omitempty"`
PublicKey string `json:"public_key"` // client WireGuard public key (wgtypes base64)
Timestamp int64 `json:"timestamp"`
Proof string `json:"proof"` // base64 MAC proving possession of PublicKey's private key
}
ClientHello is serialized as JSON in MsgHello payload (spec §12). Identity is proven with WireGuard Curve25519 keys (PublicKey + Proof), not control-plane JWTs. Proof is base64(HMAC-SHA256(X25519(client_priv, gateway_pub), HelloMACInput(...))).
type ClientOptions ¶
type ClientOptions struct {
// Addr is the uplink endpoint. Prefer a full URL:
// wss://gateway.example.com/uplink/v1
// Legacy host:port is accepted and rewritten to wss://host:port/uplink/v1.
Addr string
// ServerName is TLS SNI (optional; derived from URL host when empty).
ServerName string
// TLSConfig configures TLS verification for wss:// dials.
// Required for wss; ignored for plain ws:// (proxy-mode internal tests).
TLSConfig *tls.Config
HelloFactory func() (ClientHello, error)
PacketHandler func([]byte) error
Logger Logger
Metrics MetricsSink
KeepAlivePeriod time.Duration
WriteTimeout time.Duration
ReconnectBackoff BackoffConfig
MaxFrameSize int
PingInterval time.Duration
PongWait time.Duration
HandshakeTimeout time.Duration
}
ClientOptions configures the WebSocket uplink client.
type ClientState ¶
type ClientState string
ClientState (spec §13.1).
const ( StateDisconnected ClientState = "disconnected" StateConnecting ClientState = "connecting" StateTLSReady ClientState = "tls_ready" StateAuthenticating ClientState = "authenticating" StateActive ClientState = "active" StateClosing ClientState = "closing" StateFailed ClientState = "failed" )
type Conn ¶
type Conn interface {
// ReadFrame reads the next uplink protocol frame.
ReadFrame(maxPayload uint32) (FrameHeader, []byte, error)
// WriteFrame writes one uplink protocol frame.
WriteFrame(h FrameHeader, payload []byte) error
// Close closes the underlying transport.
Close() error
SetReadDeadline(t time.Time) error
SetWriteDeadline(t time.Time) error
}
Conn is the transport used by uplink session logic. Implementations must be safe for one reader and one writer concurrently (writer serialisation is the caller's responsibility unless documented otherwise).
type FrameHeader ¶
type FrameHeader struct {
Version uint8
MsgType uint8
Flags uint16
SessionID uint32
PayloadLen uint32
}
FrameHeader is the 12-byte header (spec §11.1), big-endian.
type InMemoryRegistry ¶
type InMemoryRegistry struct {
// contains filtered or unexported fields
}
InMemoryRegistry is a thread-safe SessionRegistry with replace-on-attach semantics. If a new session is attached for an existing peer ID, the previous session's Close() is called when the previous value implements interface{ Close() error }.
func NewInMemoryRegistry ¶
func NewInMemoryRegistry() *InMemoryRegistry
NewInMemoryRegistry returns an empty registry.
func (*InMemoryRegistry) Attach ¶
func (r *InMemoryRegistry) Attach(peerID string, sess Session) error
Attach registers a session for peerID, replacing any existing session.
func (*InMemoryRegistry) CloseAll ¶
func (r *InMemoryRegistry) CloseAll()
CloseAll closes every registered session and clears the registry. Used by Server.Stop so clients drop and re-HELLO after a gateway restart (closing the listener alone leaves accepted TLS conns answering PING forever).
func (*InMemoryRegistry) Detach ¶
func (r *InMemoryRegistry) Detach(peerID string)
Detach removes a peer from the registry, whichever session is registered.
func (*InMemoryRegistry) DetachSession ¶
func (r *InMemoryRegistry) DetachSession(peerID string, sess Session)
DetachSession removes peerID only if sess is still the registered session. A reconnecting client attaches its new session before the old session's read loop finishes unwinding, so an unconditional Detach from the old session would evict the live one and leave the peer looking session-less.
func (*InMemoryRegistry) Get ¶
func (r *InMemoryRegistry) Get(peerID string) (Session, bool)
Get returns the session for peerID.
func (*InMemoryRegistry) Len ¶
func (r *InMemoryRegistry) Len() int
Len returns the number of registered sessions (for metrics / tests).
func (*InMemoryRegistry) PeerIDs ¶
func (r *InMemoryRegistry) PeerIDs() []string
PeerIDs returns the peer IDs with a registered session.
type Logger ¶
type Logger interface {
Debug(msg string, kv ...any)
Info(msg string, kv ...any)
Warn(msg string, kv ...any)
Error(msg string, kv ...any)
}
Logger is a structured logging facade (spec §10.4).
type MetricsSink ¶
type MetricsSink interface {
IncCounter(name string, labels map[string]string)
ObserveHistogram(name string, value float64, labels map[string]string)
SetGauge(name string, value float64, labels map[string]string)
}
MetricsSink is optional telemetry (spec §10.5).
type PacketHandler ¶
type PacketHandler interface {
HandleInboundPacket(ctx context.Context, peerID string, pkt []byte) error
}
PacketHandler receives inbound DATA frames on the server (spec §10.1).
type Server ¶
type Server struct {
// contains filtered or unexported fields
}
Server serves /uplink/v1 over WebSocket and manages framed sessions.
func NewServer ¶
func NewServer(opts ServerOptions) (*Server, error)
NewServer validates options and constructs a Server.
func (*Server) SendToPeer ¶
SendToPeer sends a DATA frame to the attached peer's session.
func (*Server) SessionPeerIDs ¶
SessionPeerIDs returns peer IDs with an attached session, if supported.
func (*Server) Start ¶
Start binds the listen address and serves WebSocket uplink until ctx is cancelled or Stop.
type ServerOptions ¶
type ServerOptions struct {
ListenAddr string
// TLSMode selects TLS termination. Empty defaults to TLSModeSelfSigned.
TLSMode TLSMode
// TLSConfig is required when TLSMode is selfsigned; ignored in proxy mode.
TLSConfig *tls.Config
Authenticator Authenticator
PacketHandler PacketHandler
SessionRegistry SessionRegistry
Logger Logger
Metrics MetricsSink
KeepAlivePeriod time.Duration // used for app MsgPing fallback / read idle
WriteTimeout time.Duration
MaxFrameSize int
// PingInterval for WebSocket Ping control frames (default 25s).
PingInterval time.Duration
// PongWait is how long after a Ping to allow for a Pong / read idle slack
// (default 60s). Effective read deadline is PingInterval+PongWait.
PongWait time.Duration
}
ServerOptions configures the WebSocket uplink server.
type Session ¶
type Session interface {
PeerID() string
State() SessionState
}
Session is the handle stored per attached peer (spec §14).
type SessionRegistry ¶
type SessionRegistry interface {
Attach(peerID string, sess Session) error
Get(peerID string) (Session, bool)
Detach(peerID string)
}
SessionRegistry tracks peer ID to active session (spec §10.3).
type SessionState ¶
type SessionState string
SessionState (spec §13.2).
const ( SessionPending SessionState = "pending" SessionAuthenticated SessionState = "authenticated" SessionAttached SessionState = "attached" SessionStale SessionState = "stale" SessionClosed SessionState = "closed" )
type TLSMode ¶
type TLSMode string
TLSMode controls where TLS is terminated for the uplink HTTP/WebSocket listener.
func ParseTLSMode ¶
ParseTLSMode validates and normalises a TLS mode string. Empty input returns DefaultTLSMode (selfsigned).
type WebSocketConn ¶
type WebSocketConn struct {
// contains filtered or unexported fields
}
WebSocketConn implements Conn over a gorilla WebSocket connection. One reader and one writer may run concurrently; WriteFrame is serialised internally.
Each WebSocket binary message carries one complete uplink frame (12-byte header + payload). The length prefix inside the message is retained as compatibility framing.
func DialWebSocket ¶
func DialWebSocket(ctx context.Context, urlStr string, tlsConfig *tls.Config, handshakeTimeout time.Duration) (*WebSocketConn, error)
DialWebSocket dials a WSS (or WS) uplink endpoint and returns a WebSocketConn. urlStr should be like wss://gateway.example.com/uplink/v1 (path defaults if omitted).
func NewWebSocketConn ¶
func NewWebSocketConn(ws *websocket.Conn, maxMsgSize int) *WebSocketConn
NewWebSocketConn wraps an upgraded WebSocket connection.
func UpgradeWebSocket ¶
func UpgradeWebSocket(w http.ResponseWriter, r *http.Request, maxMsgSize int) (*WebSocketConn, error)
UpgradeWebSocket upgrades an HTTP request to a WebSocketConn.
func (*WebSocketConn) Close ¶
func (c *WebSocketConn) Close() error
func (*WebSocketConn) HandshakeStatus ¶
func (c *WebSocketConn) HandshakeStatus() int
HandshakeStatus returns the HTTP status from the WebSocket upgrade (typically 101).
func (*WebSocketConn) ReadFrame ¶
func (c *WebSocketConn) ReadFrame(maxPayload uint32) (FrameHeader, []byte, error)
func (*WebSocketConn) RemoteAddr ¶
func (c *WebSocketConn) RemoteAddr() string
RemoteAddr returns the peer address associated with this connection.
func (*WebSocketConn) SetReadDeadline ¶
func (c *WebSocketConn) SetReadDeadline(t time.Time) error
func (*WebSocketConn) SetRemoteAddr ¶
func (c *WebSocketConn) SetRemoteAddr(addr string)
SetRemoteAddr overrides the logged peer address (e.g. client IP behind a reverse proxy).
func (*WebSocketConn) SetWriteDeadline ¶
func (c *WebSocketConn) SetWriteDeadline(t time.Time) error
func (*WebSocketConn) StartPingLoop ¶
func (c *WebSocketConn) StartPingLoop(ctx context.Context)
StartPingLoop sends WebSocket Ping frames on interval until ctx is cancelled or Close.
func (*WebSocketConn) WriteFrame ¶
func (c *WebSocketConn) WriteFrame(h FrameHeader, payload []byte) error