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
- Variables
- func AcceptBlindClientHello(binding StreamBinding, hello BlindClientHello, identity ed25519.PrivateKey, ...) (*BlindAgentHandshake, BlindAgentResponse, error)
- func ChallengeTranscript(version uint16, origin, deviceID string, agentNonce, controllerNonce []byte) []byte
- func EncodeMessage(header Header, msg protocol.Message) ([]byte, error)
- func EnforceTrustMode(minimum, negotiated TrustMode, allowDowngrade bool) error
- func EqualPayload(a, b []byte) bool
- func GenerateDeviceKey() (ed25519.PublicKey, ed25519.PrivateKey, error)
- func HashEnrollmentCode(code string) [32]byte
- func MarshalFrames(frames [][]byte) ([]byte, error)
- func NewBlindClientHandshake(binding StreamBinding, random io.Reader) (*BlindClientHandshake, BlindClientHello, error)
- func NewBlindClientStream(binding StreamBinding, agentIdentity ed25519.PublicKey) (*BlindStreamSession, Envelope, error)
- func NewEnrollmentCode() (string, error)
- func SignChallenge(priv ed25519.PrivateKey, version uint16, origin, deviceID string, ...) ([]byte, error)
- func UnmarshalFrames(data []byte) ([][]byte, error)
- func VerifyChallenge(pub ed25519.PublicKey, signature []byte, version uint16, ...) error
- type AgentMetadata
- type BackoffPolicy
- type BlindAgentHandshake
- type BlindAgentResponse
- type BlindClientFinish
- type BlindClientHandshake
- type BlindClientHello
- type BlindKeyRotation
- type BlindStreamSession
- func (s *BlindStreamSession) ApplyRotation(rotation BlindKeyRotation) error
- func (s *BlindStreamSession) HandleOpen(env Envelope) (Envelope, error)
- func (s *BlindStreamSession) NewRotation(random io.Reader) (BlindKeyRotation, error)
- func (s *BlindStreamSession) OpenData(env Envelope) (Envelope, error)
- func (s *BlindStreamSession) Ready() bool
- func (s *BlindStreamSession) SealData(env Envelope) (Envelope, error)
- type BrowserCapability
- type ConnectionState
- type Controller
- func (c *Controller) EnforceRevocations()
- func (c *Controller) RevokeDevice(deviceID, reason string) error
- func (c *Controller) RevokeGrant(grantID, reason string) error
- func (c *Controller) RevokeSubject(subject, reason string) error
- func (c *Controller) ServeHTTP(w http.ResponseWriter, r *http.Request)
- type CounterTracker
- type DeviceCredential
- type DiscoveredSession
- type EnrollmentRequest
- type EnrollmentStore
- type Envelope
- type Header
- type Reconnector
- type SessionDescriptor
- type StreamBinding
- type StreamCipher
- func (s *StreamCipher) ApplyRotation(rotation BlindKeyRotation) (*StreamCipher, error)
- func (s *StreamCipher) Epoch() uint64
- func (s *StreamCipher) NewRotation(random io.Reader) (BlindKeyRotation, *StreamCipher, error)
- func (s *StreamCipher) Open(header Header, ciphertext []byte) ([]byte, error)
- func (s *StreamCipher) OpenEnvelope(env Envelope) (Envelope, error)
- func (s *StreamCipher) Seal(header Header, payload []byte) ([]byte, error)
- func (s *StreamCipher) SealEnvelope(env Envelope) (Envelope, error)
- type TrustMode
- type Tunnel
Constants ¶
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 )
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.
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 ¶
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 EnforceTrustMode ¶
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 ¶
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 MarshalFrames ¶
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 ¶
NewEnrollmentCode returns a URL-safe, single-use bootstrap code. The controller should hash this value with HashEnrollmentCode before storing it.
func SignChallenge ¶
func UnmarshalFrames ¶
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.
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 ¶
func (h *BlindAgentHandshake) Confirm(finish BlindClientFinish) (*StreamCipher, error)
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 ¶
func (h *BlindClientHandshake) Finish(response BlindAgentResponse, identity ed25519.PublicKey) (*StreamCipher, BlindClientFinish, error)
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
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.
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)
type Envelope ¶
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).
type Header ¶
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 ¶
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.
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.
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.