relaywire

package
v0.9.4 Latest Latest
Warning

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

Go to latest
Published: Aug 20, 2026 License: MIT Imports: 4 Imported by: 0

Documentation

Overview

Package relaywire defines the framing that crosses the daemon↔relay socket.

It is the one place the relay's byte layout is written down for Go; the Cloudflare Worker and the web client implement the same layout and are held to it by the shared fixtures in testdata/relay/frames.json. Nothing here knows about WebSockets, Noise, or the wire protocol carried inside — it is framing only, so all three implementations can be compared byte for byte.

Two layers stack:

[4-byte big-endian channel][payload]   the daemon↔relay socket
[1-byte kind][wire bytes]              inside a decrypted channel payload

See spec/relay-protocol.md for the protocol these frames carry.

Index

Constants

View Source
const (
	Ping = "flue-ping"
	Pong = "flue-pong"
)

Ping and Pong are the keepalive text frames. They are never channel-framed: either leg may send Ping, the edge answers Pong through the Durable Object's auto-response, and a receiver drops Pong silently.

View Source
const ControlChannel uint32 = 0

ControlChannel is the channel id reserved for the JSON control messages in control.go. Every other channel carries one browser's Noise session.

Variables

View Source
var ErrEmptyPayload = errors.New("relaywire: plain payload has no kind byte")

ErrEmptyPayload is returned by DecodePlain for a payload with no kind byte.

View Source
var ErrShortFrame = errors.New("relaywire: frame shorter than the channel header")

ErrShortFrame is returned by Decode for a frame with no room for the header.

Functions

func DecodeControl

func DecodeControl(b []byte) (any, error)

DecodeControl parses a control message into its concrete type, returning one of *Open, *Closed, *Close, *Pair or *PairResult. An unknown discriminator is an error rather than a silent drop: on this channel every message is one the two ends agreed on, so an unrecognised one means the peers disagree about the protocol.

func DecodePlain

func DecodePlain(b []byte) (text bool, data []byte, err error)

DecodePlain splits a decrypted channel payload into its kind and its bytes. The returned data aliases b, with its capacity clamped as Decode's is. An empty payload, or a kind byte other than 0 or 1, is a protocol error: the peer is not speaking this protocol, and guessing would hand the layer above a frame of the wrong sort.

func Encode

func Encode(f Frame) []byte

Encode lays a frame out as [4-byte big-endian channel][payload]. The result is freshly allocated, so a caller may keep mutating the payload it passed.

func EncodeControl

func EncodeControl(msg any) ([]byte, error)

EncodeControl marshals a control message, setting "type" from the concrete Go type. Both values and pointers are accepted, so a message DecodeControl returned can be handed straight back, and whatever the caller left in the Type field is overwritten: the concrete type is the authority.

Unlike internal/wire's encoder this marshals the struct directly rather than round-tripping through a map, which keeps the field order the declarations give — "type" first — so the bytes match what the TypeScript side writes by hand and the shared fixtures can pin them.

A nil pointer is an error rather than a panic: this runs on the daemon's relay writer, where a nil message is a bug to report, not one to crash on.

func EncodePlain

func EncodePlain(text bool, data []byte) []byte

EncodePlain prefixes the kind byte that survives the trip through Noise.

The wire protocol distinguishes text frames (JSON control) from binary ones (terminal data), a distinction the WebSocket gives us locally and encryption erases: through the relay every frame is one binary WebSocket message of ciphertext. This byte carries it, so the layer above the relay reads the same (text, data) pair it reads locally.

Types

type Close

type Close struct {
	Type    string `json:"type"` // "close"
	Channel uint32 `json:"channel"`
}

Close asks the relay to close that browser's socket.

daemon -> relay.

type Closed

type Closed struct {
	Type    string `json:"type"` // "closed"
	Channel uint32 `json:"channel"`
}

Closed tells the daemon that browser went away.

relay -> daemon.

type Frame

type Frame struct {
	Channel uint32
	Payload []byte
}

Frame is one message on the daemon↔relay socket: a channel id and the bytes carried on it. On channel 0 the payload is a control message; on any other channel it is a Noise handshake message or transport ciphertext, which this package never inspects.

func Decode

func Decode(b []byte) (Frame, error)

Decode parses a framed message. The returned payload aliases b, so a caller that retains it past the read buffer's lifetime must copy it. Its capacity is clamped to its length, so appending to it allocates rather than writing into the spare capacity of the caller's read buffer.

A frame that is exactly a header is well formed and carries no payload; only a frame too short to hold the header is an error.

type Open

type Open struct {
	Type    string `json:"type"` // "open"
	Channel uint32 `json:"channel"`
	Origin  string `json:"origin"`
}

Open tells the daemon a browser connected and was assigned Channel. Origin is the Worker's own origin, which the daemon checks against the relay it dialled.

relay -> daemon.

type Pair

type Pair struct {
	Type   string          `json:"type"` // "pair"
	ID     uint64          `json:"id"`
	Origin string          `json:"origin"`
	Body   json.RawMessage `json:"body"`
}

Pair carries an HTTP POST /api/pair the Worker received. Body is the client's JSON as it arrived — the daemon's pairing handler parses these bytes itself, so the relay must not reshape them. Decoding preserves them exactly; re-encoding a Pair in Go escapes <, > and & to their \u00xx forms, which every JSON parser reads back identically but which is not byte-for-byte what a Worker's JSON.stringify produced. Only Go re-encodes a Pair, and only in tests: on the wire this message is written once, by the Worker.

ID correlates the answer, and is the relay's own: it means nothing across a reconnect.

relay -> daemon.

type PairResult

type PairResult struct {
	Type   string          `json:"type"` // "pairResult"
	ID     uint64          `json:"id"`
	Status int             `json:"status"`
	Body   json.RawMessage `json:"body"`
}

PairResult answers a Pair with the HTTP status and response body the Worker should write back to the browser.

Body must be a JSON value: the relay writes it as an application/json response, and EncodeControl refuses anything else rather than shipping a malformed frame. A refusal therefore travels as JSON — {"error":"pairing refused"} — not as the bare text the daemon's own HTTP handler writes.

daemon -> relay.

Jump to

Keyboard shortcuts

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