uplink

package
v1.0.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Aug 31, 2026 License: Apache-2.0 Imports: 20 Imported by: 0

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

Examples

Constants

View Source
const (
	MsgHello uint8 = iota + 1
	MsgHelloAck
	MsgData
	MsgPing
	MsgPong
	MsgClose
	MsgError
)

Message types (spec §11.2).

View Source
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
)
View Source
const (

	// DefaultMaxFrameSize caps payload length (single frame) to limit memory use.
	DefaultMaxFrameSize = 65536
)
View Source
const DefaultTLSMode = TLSModeSelfSigned

DefaultTLSMode is used when TLS mode is omitted.

View Source
const ProtocolVersion uint8 = 1

Protocol version for Phase 1 framing.

Variables

View Source
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

func ClientIPFromRequest(r *http.Request) string

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

func ComputeHelloProof(ourPriv, peerPub *[32]byte, macInput []byte) (string, error)

ComputeHelloProof builds Proof = base64(HMAC-SHA256(X25519(ourPriv, peerPub), macInput)). ourPriv and peerPub are raw 32-byte WireGuard Curve25519 keys.

func EndpointURL

func EndpointURL(host string, port int) string

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

func NormaliseUplinkURL(addr string) (string, error)

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())
}

func NewClient

func NewClient(opts ClientOptions) (*Client, error)

NewClient validates options and returns a Client.

func (*Client) SendPacket

func (c *Client) SendPacket(ctx context.Context, pkt []byte) error

SendPacket sends a DATA frame with the current session ID. Safe for concurrent use.

func (*Client) Start

func (c *Client) Start(ctx context.Context) error

Start begins the connection supervisor until ctx is cancelled or Stop is called.

func (*Client) State

func (c *Client) State() ClientState

State returns the current client state.

func (*Client) Stop

func (c *Client) Stop(ctx context.Context) error

Stop cancels the client and closes the active connection.

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) Addr

func (s *Server) Addr() net.Addr

Addr returns the bound listen address (e.g. after Start with ":0").

func (*Server) SendToPeer

func (s *Server) SendToPeer(ctx context.Context, peerID string, pkt []byte) error

SendToPeer sends a DATA frame to the attached peer's session.

func (*Server) SessionPeerIDs

func (s *Server) SessionPeerIDs() []string

SessionPeerIDs returns peer IDs with an attached session, if supported.

func (*Server) Start

func (s *Server) Start(ctx context.Context) error

Start binds the listen address and serves WebSocket uplink until ctx is cancelled or Stop.

func (*Server) Stop

func (s *Server) Stop(ctx context.Context) error

Stop closes the HTTP server, attached sessions, and waits for handlers.

func (*Server) TLSMode

func (s *Server) TLSMode() TLSMode

TLSMode returns the configured TLS mode.

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.

const (
	// TLSModeSelfSigned: uplink terminates TLS with a persisted self-signed certificate.
	TLSModeSelfSigned TLSMode = "selfsigned"
	// TLSModeProxy: upstream reverse proxy terminates TLS; uplink serves plain HTTP/WS.
	TLSModeProxy TLSMode = "proxy"
)

func ParseTLSMode

func ParseTLSMode(s string) (TLSMode, error)

ParseTLSMode validates and normalises a TLS mode string. Empty input returns DefaultTLSMode (selfsigned).

func (TLSMode) Validate

func (m TLSMode) Validate() error

Validate reports whether m is a supported TLS mode.

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

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL