relay

package
v0.0.0-...-cce531b Latest Latest
Warning

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

Go to latest
Published: Aug 14, 2026 License: BSD-3-Clause Imports: 28 Imported by: 0

Documentation

Overview

Package relay contains the transport-neutral pieces of the outbound relay protocol. The package deliberately does not open sockets: controllers and agents can use these codecs with any WebSocket implementation.

Index

Constants

View Source
const (
	// Version is the outer relay envelope version.  The inner terminal protocol
	// remains protocol.Version (currently v2).
	Version uint16 = 1

	MaxHeaderSize   = 64 << 10
	MaxEnvelopeSize = 64 << 20
	MaxFrameCount   = 3
)
View Source
const (
	FrameHello     = "hello"
	FrameChallenge = "challenge"
	FrameProof     = "proof"
	FrameReady     = "ready"
	FrameResume    = "resume"
	FrameOpen      = "open"
	// FrameData carries one encrypted (blind mode) or trusted inner protocol
	// payload. FrameSession is retained as a wire-compatible alias for older
	// agents that used the initial vertical-slice name.
	FrameData    = "data"
	FrameClose   = "close"
	FrameSession = "session"
)

Standard outer frame types. Unknown types are allowed by the codec so a controller can evolve its control plane without requiring an agent upgrade.

View Source
const (
	// BlindVersion is the version of the end-to-end stream encryption
	// handshake. It is independent of the relay envelope and terminal protocol
	// versions so each layer can evolve without silently changing another.
	BlindVersion uint16 = 1
)

Variables

View Source
var ErrTrustDowngrade = errors.New("relay trust mode downgrade rejected")

Functions

func AcceptBlindClientHello

func AcceptBlindClientHello(binding StreamBinding, hello BlindClientHello, identity ed25519.PrivateKey, minimum TrustMode, random io.Reader) (*BlindAgentHandshake, BlindAgentResponse, error)

AcceptBlindClientHello verifies the negotiated mode and creates an agent-authenticated response. It does not return usable stream keys until the caller has checked BlindClientFinish with Confirm.

func ChallengeTranscript

func ChallengeTranscript(version uint16, origin, deviceID string, agentNonce, controllerNonce []byte) []byte

ChallengeTranscript is deterministic and domain separated so a signature cannot be replayed as a signature for another protocol. Origin, device ID, and both nonces are included as required by the relay handshake.

func EncodeMessage

func EncodeMessage(header Header, msg protocol.Message) ([]byte, error)

func EnforceTrustMode

func EnforceTrustMode(minimum, negotiated TrustMode, allowDowngrade bool) error

EnforceTrustMode is used when the peer selected a mode rather than sending a full offer. It prevents an on-path or controller downgrade from blind mode.

func EqualPayload

func EqualPayload(a, b []byte) bool

EqualPayload is useful to callers that need to assert that an inner frame was relayed without translation.

func GenerateDeviceKey

func GenerateDeviceKey() (ed25519.PublicKey, ed25519.PrivateKey, error)

GenerateDeviceKey creates the long-lived key held by an agent. Callers are responsible for persisting the private key with owner-only permissions.

func HashEnrollmentCode

func HashEnrollmentCode(code string) [32]byte

func MarshalFrames

func MarshalFrames(frames [][]byte) ([]byte, error)

MarshalFrames serializes the existing multipart protocol frames into the envelope payload. Length prefixes preserve frame boundaries exactly.

func NewBlindClientHandshake

func NewBlindClientHandshake(binding StreamBinding, random io.Reader) (*BlindClientHandshake, BlindClientHello, error)

func NewBlindClientStream

func NewBlindClientStream(binding StreamBinding, agentIdentity ed25519.PublicKey) (*BlindStreamSession, Envelope, error)

func NewEnrollmentCode

func NewEnrollmentCode() (string, error)

NewEnrollmentCode returns a URL-safe, single-use bootstrap code. The controller should hash this value with HashEnrollmentCode before storing it.

func SignChallenge

func SignChallenge(priv ed25519.PrivateKey, version uint16, origin, deviceID string, agentNonce, controllerNonce []byte) ([]byte, error)

func UnmarshalFrames

func UnmarshalFrames(data []byte) ([][]byte, error)

func VerifyChallenge

func VerifyChallenge(pub ed25519.PublicKey, signature []byte, version uint16, origin, deviceID string, agentNonce, controllerNonce []byte) error

Types

type AgentMetadata

type AgentMetadata struct {
	Labels   map[string]string   `json:"labels,omitempty"`
	Sessions []SessionDescriptor `json:"sessions,omitempty"`
}

AgentMetadata announces an agent's sessions without exposing terminal contents. It is sent during the authenticated tunnel handshake and is used by the controller's session discovery API.

Labels are opaque. shenmux carries them and hands them back; it never reads one, and it has no vocabulary for where the process on the other end runs. Whoever set a label is the only party that knows what it means.

type BackoffPolicy

type BackoffPolicy struct {
	Initial time.Duration
	Maximum time.Duration
	Factor  float64
	Jitter  float64
	Rand    *rand.Rand
}

BackoffPolicy controls bounded reconnect delay. Jitter is a fraction in [0,1], and is applied symmetrically around the exponential delay.

func (BackoffPolicy) Delay

func (p BackoffPolicy) Delay(attempt int) time.Duration

type BlindAgentHandshake

type BlindAgentHandshake struct {
	// contains filtered or unexported fields
}

BlindAgentHandshake retains pending key material until client confirmation. Handshake instances are single-use.

func (*BlindAgentHandshake) Confirm

Confirm verifies client possession of the stream secret and returns the agent side of the directional cipher.

type BlindAgentResponse

type BlindAgentResponse struct {
	Version           uint16 `json:"version"`
	Epoch             uint64 `json:"epoch"`
	EphemeralPublic   []byte `json:"ephemeral_public"`
	Signature         []byte `json:"signature"`
	AgentConfirmation []byte `json:"agent_confirmation"`
}

BlindAgentResponse authenticates the agent ephemeral key with its enrolled Ed25519 identity and proves possession of the resulting X25519 secret.

type BlindClientFinish

type BlindClientFinish struct {
	Version            uint16 `json:"version"`
	Epoch              uint64 `json:"epoch"`
	ClientConfirmation []byte `json:"client_confirmation"`
}

BlindClientFinish gives the agent explicit key confirmation before any terminal contents are accepted on the stream.

type BlindClientHandshake

type BlindClientHandshake struct {
	// contains filtered or unexported fields
}

BlindClientHandshake retains the ephemeral private key only until Finish. Handshake instances are single-use.

func (*BlindClientHandshake) Finish

Finish authenticates the agent response and returns the client cipher plus the confirmation that must be accepted by the agent. A failed attempt also consumes the ephemeral handshake to prevent ambiguous retries.

type BlindClientHello

type BlindClientHello struct {
	Version         uint16    `json:"version"`
	TrustMode       TrustMode `json:"trust_mode"`
	Epoch           uint64    `json:"epoch"`
	EphemeralPublic []byte    `json:"ephemeral_public"`
}

BlindClientHello starts an authenticated ephemeral X25519 exchange. The binding is carried independently by the OPEN request and is included in the signed transcript by both endpoints.

type BlindKeyRotation

type BlindKeyRotation struct {
	Version       uint16   `json:"version"`
	PreviousEpoch uint64   `json:"previous_epoch"`
	Epoch         uint64   `json:"epoch"`
	Salt          [32]byte `json:"salt"`
	Confirmation  [32]byte `json:"confirmation"`
}

BlindKeyRotation is sent through the already encrypted stream before either endpoint switches keys. ApplyRotation returns a new cipher with reset counters; callers atomically replace the old cipher only after the update is acknowledged. A missed or ambiguous update is recovered with a fresh stream and capability-bound handshake, never by reusing nonces or falling back to trusted mode.

type BlindStreamSession

type BlindStreamSession struct {
	// contains filtered or unexported fields
}

BlindStreamSession is an endpoint-side stream state machine. A client is created with NewBlindClientStream and an agent with NewBlindAgentStream. HandleOpen consumes the peer's OPEN handshake message and returns the next OPEN message (or nil once the handshake is complete).

func NewBlindAgentStream

func NewBlindAgentStream(binding StreamBinding, agentIdentity ed25519.PrivateKey, minimum TrustMode) (*BlindStreamSession, error)

func (*BlindStreamSession) ApplyRotation

func (s *BlindStreamSession) ApplyRotation(rotation BlindKeyRotation) error

func (*BlindStreamSession) HandleOpen

func (s *BlindStreamSession) HandleOpen(env Envelope) (Envelope, error)

func (*BlindStreamSession) NewRotation

func (s *BlindStreamSession) NewRotation(random io.Reader) (BlindKeyRotation, error)

NewRotation advances this endpoint to the next epoch. Callers should send the returned rotation (inside the encrypted stream) and have the peer call ApplyRotation before sending data in the new epoch.

func (*BlindStreamSession) OpenData

func (s *BlindStreamSession) OpenData(env Envelope) (Envelope, error)

func (*BlindStreamSession) Ready

func (s *BlindStreamSession) Ready() bool

func (*BlindStreamSession) SealData

func (s *BlindStreamSession) SealData(env Envelope) (Envelope, error)

type BrowserCapability

type BrowserCapability struct {
	ID         string            `json:"id"`
	Token      string            `json:"token"`
	DeviceID   string            `json:"device_id"`
	SessionID  string            `json:"session_id"`
	Permission policy.Permission `json:"permission"`
	ExpiresAt  time.Time         `json:"expires_at"`
}

BrowserCapability is the bearer value returned by POST /capabilities and presented once in the payload of the browser's first OPEN envelope.

type ConnectionState

type ConnectionState uint8
const (
	StateDisconnected ConnectionState = iota
	StateConnecting
	StateConnected
	StateBackoff
)

type Controller

type Controller struct {
	Store               *EnrollmentStore
	Policy              *policy.Store
	Origin              string
	Upgrader            websocket.Upgrader
	CapabilityTTL       time.Duration
	ControlLeaseTTL     time.Duration
	AuthenticateBrowser func(*http.Request) (string, error)
	// DevBrowserSubject lets a browser WebSocket, whose API cannot set
	// arbitrary HTTP headers, authenticate from a query parameter. Keep empty
	// in hosted deployments and provide real AuthenticateBrowser integration.
	DevBrowserSubject string
	// contains filtered or unexported fields
}

Controller is a minimal authenticated relay endpoint. It implements the enrollment and agent handshake, leaving session multiplexing to callers.

func NewController

func NewController(store *EnrollmentStore, origin string) *Controller

func (*Controller) EnforceRevocations

func (c *Controller) EnforceRevocations()

EnforceRevocations immediately revalidates all connected devices and browser streams. It is called by the controller revocation helpers and is also complemented by per-stream watchers for callers mutating Policy directly.

func (*Controller) RevokeDevice

func (c *Controller) RevokeDevice(deviceID, reason string) error

func (*Controller) RevokeGrant

func (c *Controller) RevokeGrant(grantID, reason string) error

func (*Controller) RevokeSubject

func (c *Controller) RevokeSubject(subject, reason string) error

func (*Controller) ServeHTTP

func (c *Controller) ServeHTTP(w http.ResponseWriter, r *http.Request)

type CounterTracker

type CounterTracker struct {
	// contains filtered or unexported fields
}

CounterTracker enforces strictly increasing outer transport counters for a stream. It is intentionally independent of the inner checkpoint sequence; reconnecting streams should use a fresh tracker.

func (*CounterTracker) Accept

func (t *CounterTracker) Accept(counter uint64) error

func (*CounterTracker) Last

func (t *CounterTracker) Last() (uint64, bool)

type DeviceCredential

type DeviceCredential struct {
	DeviceID  string    `json:"device_id"`
	Token     []byte    `json:"token"`
	ExpiresAt time.Time `json:"expires_at"`
}

DeviceCredential is the opaque credential returned by enrollment. The controller may rotate Token before ExpiresAt; the agent never sends its private key over the network.

func (DeviceCredential) Validate

func (c DeviceCredential) Validate(now time.Time) error

type DiscoveredSession

type DiscoveredSession struct {
	DeviceID    string            `json:"device_id"`
	Online      bool              `json:"online"`
	ConnectedAt time.Time         `json:"connected_at,omitempty"`
	Metadata    AgentMetadata     `json:"metadata"`
	Session     SessionDescriptor `json:"session"`
}

DiscoveredSession is returned by GET /sessions. It is deliberately metadata-only; terminal contents remain on the agent/browser stream.

type EnrollmentRequest

type EnrollmentRequest struct {
	Code      string            `json:"code"`
	PublicKey ed25519.PublicKey `json:"public_key"`
}

EnrollmentRequest is the HTTPS bootstrap payload. Code is single-use and short-lived; the controller must consume it atomically before issuing a credential. PublicKey is copied by callers before storing it.

type EnrollmentStore

type EnrollmentStore struct {
	// contains filtered or unexported fields
}

EnrollmentStore is a small in-memory store suitable for tests and a development controller. Production controllers should replace it with a durable implementation while retaining the atomic Consume semantics.

func NewEnrollmentStore

func NewEnrollmentStore() *EnrollmentStore

func NewPersistentEnrollmentStore

func NewPersistentEnrollmentStore(path string) (*EnrollmentStore, error)

NewPersistentEnrollmentStore keeps enrollment codes and device credentials across controller restarts. The file is owner-readable only and writes are atomic, so a restart cannot resurrect a consumed code or truncate state.

func (*EnrollmentStore) Consume

func (s *EnrollmentStore) Consume(code string, pub ed25519.PublicKey) (DeviceCredential, error)

func (*EnrollmentStore) Create

func (s *EnrollmentStore) Create(ttl time.Duration) (string, error)

type Envelope

type Envelope struct {
	Header  Header
	Payload []byte
}

Envelope is one binary WebSocket message. The wire format is a four-byte big-endian header length, a JSON Header, and an inner payload. Payload is usually MarshalFrames output, preserving the existing v2 multipart message byte-for-byte (including its kind, metadata, and optional binary payload).

func Decode

func Decode(data []byte) (Envelope, error)

func DecodeMessage

func DecodeMessage(data []byte) (Envelope, protocol.Message, error)

func (Envelope) Encode

func (e Envelope) Encode() ([]byte, error)
type Header struct {
	Version   uint16 `json:"v"`
	FrameType string `json:"type"`
	RequestID uint64 `json:"request_id,omitempty"`
	DeviceID  string `json:"device_id,omitempty"`
	SessionID string `json:"session_id,omitempty"`
	StreamID  string `json:"stream_id,omitempty"`
	Counter   uint64 `json:"counter"`
}

Header identifies a relay stream and protects it from cross-stream replay. FrameType is intentionally opaque to this package; controllers may add control-plane frame types without changing terminal framing.

type Reconnector

type Reconnector struct {
	// contains filtered or unexported fields
}

Reconnector is a small, concurrency-safe state machine. Networking code drives it from callbacks, making reconnect behavior deterministic and easy to test without a live WebSocket server.

func NewReconnector

func NewReconnector(policy BackoffPolicy) *Reconnector

func (*Reconnector) Attempt

func (r *Reconnector) Attempt() int

func (*Reconnector) Connected

func (r *Reconnector) Connected()

func (*Reconnector) Disconnected

func (r *Reconnector) Disconnected()

func (*Reconnector) Failed

func (r *Reconnector) Failed() time.Duration

Failed records a failed attempt, enters backoff, and returns the delay until the next Start call. The first failure uses Initial.

func (*Reconnector) Start

func (r *Reconnector) Start()

func (*Reconnector) State

func (r *Reconnector) State() ConnectionState

type SessionDescriptor

type SessionDescriptor struct {
	ID   string `json:"id"`
	Name string `json:"name,omitempty"`
}

SessionDescriptor is the discoverable identity of one PTY attached to an agent. Every session shenmux knows about is a terminal. The controller never stores terminal data.

type StreamBinding

type StreamBinding struct {
	DeviceID         string
	SessionID        string
	StreamID         string
	Subject          string
	Permission       string
	CapabilityDigest [32]byte
	Epoch            uint64
}

StreamBinding is the authorization and routing context authenticated by the key exchange. CapabilityDigest is SHA-256 over the exact opaque attach capability received from the controller. The capability is never used as key material and need not be disclosed to the relay as part of the handshake. Epoch starts at one and increases on rotation.

func NewRecoveryBinding

func NewRecoveryBinding(previous StreamBinding, newStreamID string, capability []byte) (StreamBinding, error)

NewRecoveryBinding creates the context for recovery after a lost key update or reconnect. Recovery always uses a new single-use capability, a new stream ID, epoch one, and a complete handshake. This keeps old ciphertext and counters cryptographically disjoint from the recovered stream.

func NewStreamBinding

func NewStreamBinding(deviceID, sessionID, streamID, subject, permission string, capability []byte) (StreamBinding, error)

func (StreamBinding) Validate

func (b StreamBinding) Validate() error

type StreamCipher

type StreamCipher struct {
	// contains filtered or unexported fields
}

StreamCipher encrypts the complete inner protocol payload. Its AEAD additional data authenticates every outer routing field, the stream binding, direction, and key epoch. Counters are strictly increasing in each direction, which rejects replay and nonce reuse.

func (*StreamCipher) ApplyRotation

func (s *StreamCipher) ApplyRotation(rotation BlindKeyRotation) (*StreamCipher, error)

func (*StreamCipher) Epoch

func (s *StreamCipher) Epoch() uint64

func (*StreamCipher) NewRotation

func (s *StreamCipher) NewRotation(random io.Reader) (BlindKeyRotation, *StreamCipher, error)

func (*StreamCipher) Open

func (s *StreamCipher) Open(header Header, ciphertext []byte) ([]byte, error)

Open authenticates and decrypts payload. The replay counter advances only after successful authentication, so corrupted packets cannot consume a valid future counter.

func (*StreamCipher) OpenEnvelope

func (s *StreamCipher) OpenEnvelope(env Envelope) (Envelope, error)

func (*StreamCipher) Seal

func (s *StreamCipher) Seal(header Header, payload []byte) ([]byte, error)

Seal encrypts payload for header. Counter zero and repeated/rolled-back counters are rejected before encryption to guarantee unique AEAD nonces.

func (*StreamCipher) SealEnvelope

func (s *StreamCipher) SealEnvelope(env Envelope) (Envelope, error)

SealEnvelope preserves the outer routing header and encrypts only its inner payload, leaving the controller with the minimum metadata needed to route.

type TrustMode

type TrustMode string

TrustMode is an explicit transport-content trust decision. TLS is required in both modes; Blind additionally encrypts the inner terminal frames between the client and agent.

const (
	TrustTrusted TrustMode = "trusted"
	TrustBlind   TrustMode = "blind"
)

func NegotiateTrustMode

func NegotiateTrustMode(minimum TrustMode, local, peer []TrustMode, allowDowngrade bool) (TrustMode, error)

NegotiateTrustMode chooses the strongest mutually supported mode and then enforces minimum. Falling below a persisted blind minimum is allowed only when allowDowngrade was explicitly set by the user for this negotiation.

func (TrustMode) Validate

func (m TrustMode) Validate() error

type Tunnel

type Tunnel struct {
	URL string
	// Endpoints optionally lists direct/tailnet URLs followed by the relay URL.
	// URL remains the compatibility field and is used when Endpoints is empty.
	Endpoints  []string
	Origin     string
	Credential DeviceCredential
	PrivateKey ed25519.PrivateKey
	Metadata   AgentMetadata
	Dialer     *websocket.Dialer
	Policy     BackoffPolicy
}

Tunnel is an authenticated, outbound-only agent connection.

func (*Tunnel) Connect

func (t *Tunnel) Connect(ctx context.Context) (*websocket.Conn, error)

func (*Tunnel) Run

func (t *Tunnel) Run(ctx context.Context, connected func(*websocket.Conn) error) error

Jump to

Keyboard shortcuts

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