relay

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: 24 Imported by: 0

Documentation

Overview

Package relay is the daemon's leg of the Cloudflare relay: one outbound WebSocket to a Worker, and every browser that reaches this machine multiplexed over it.

The direction is the point. The daemon binds 127.0.0.1 and nothing else, so remote reach cannot come from a port on this machine — it comes from a socket this machine opened. Everything the relay carries is defined by spec/relay-protocol.md and framed by internal/relaywire: binary messages are [4-byte channel][payload], the only text messages are the keepalives, and channel 0 carries the JSON control messages the relay and the daemon say to each other in the clear. Channels 1 and up are one browser's Noise session each, opaque here and served by channel.go.

The adapter owns exactly one thing: keeping that socket up. It dials, reads, dispatches, pings, and — when the socket dies, as a socket held open across the internet by a hibernating Durable Object regularly does — backs off and dials again, forever, until its context ends.

Index

Constants

View Source
const (

	// MaxDirectoryEntries is how many entries one GET may carry, mirroring
	// MAX_ENTRIES in relay/src/directory.ts. Entries past it are dropped
	// unread: the Worker refuses to store a 513th (it answers 507 rather than
	// evicting, because evicting could drop a revocation), so a longer answer
	// is not a bigger fleet, it is a relay that has stopped speaking this
	// protocol.
	//
	// Exported because it is also the number the status surfaces measure a
	// fleet against — `flue relay status` warns as the directory approaches it
	// (cmd/flue, directoryLine), and a second copy of 512 in that file would be
	// a number that could drift from the one the reader actually enforces.
	MaxDirectoryEntries = 512

	// DirectoryWarnAt is where a status surface starts saying the directory is
	// filling up: 90% of the cap.
	//
	// It exists because the cap has no gentle failure. At 512 the relay refuses
	// every new blob — and refuses it before storing and before pushing, so a
	// revocation made past that point reaches nobody at all — and nothing frees
	// an entry short of `flue relay reset`. Without a warning the first signal
	// an operator gets is a 507 in a daemon log *after* a revoke they believed
	// had crossed the fleet, which is precisely the moment it is worth least.
	DirectoryWarnAt = MaxDirectoryEntries * 9 / 10
)

Variables

View Source
var ErrIncompleteConfig = errors.New("relay: incomplete config")

ErrIncompleteConfig is what New answers a Config it cannot dial with: a field missing it cannot invent, or a machine id the relay would never route. Wrapping rather than a sentinel per fault: the caller's decision is the same for all of them — this daemon is not configured for a relay — and the message names which field it was.

Functions

This section is empty.

Types

type Config

type Config struct {
	URL       string // the bare relay address: wss://flue-relay.<sub>.workers.dev
	Secret    string // the DAEMON_SECRET set at deploy time
	Origin    string // https origin the relay serves the UI on
	MachineID string // the machine's slot on the relay: the <id> of /daemon/<id>

	// FleetPub is the fleet public key (spec/fleet-trust.md), derived from
	// the seed relay.json carries. Required like the rest, deliberately: a
	// relay.json without a fleet seed is from a flue that predates the key,
	// and the honest answer is the re-join the operator is one printed line
	// away from — not a transport that quietly serves rule 1 of the
	// acceptance order and silently drops rule 2 (channel.go). Only the
	// public half ever reaches this package.
	FleetPub ed25519.PublicKey

	// MachineCert is this machine's own fleet certificate as relay.json
	// stores it (config.Relay.MachineCert), minted by the command that joined
	// this machine to the relay. The directory leg publishes it so the
	// fleet's browsers can find this machine; nothing else here reads it, and
	// an empty one costs discovery rather than access — see
	// Directory.machineCert, which refuses to publish one that no longer
	// describes this daemon.
	MachineCert []byte
}

Config is everything the adapter needs to reach a deployed relay.

All five fields are required: where to dial, which machine's slot to dial it as, what the dial presents, the origin the relay serves browsers on, and the fleet public key the acceptance rule verifies device certs under. The daemon and the Worker share one long-lived DAEMON_SECRET, set at deploy time by `flue relay setup`; the fleet key was minted beside it and the Worker never sees either half of it.

type Directory added in v0.2.0

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

Directory keeps this machine's view of the fleet directory up to date, and the directory's view of this machine.

It is a sibling of Transport rather than a part of it, and the split is the honest one: they dial different objects for different reasons and neither needs the other to work. A daemon whose directory leg is down still serves every device paired to it; a daemon whose hub leg is down still hears revocations. What they share is the credential, the reconnect discipline and the fleet public key, which is why they share a Config.

func NewDirectory added in v0.2.0

func NewDirectory(cfg Config, sink FleetSink, identity noise.DHKey, devices *crypto.DeviceStore, log *slog.Logger) (*Directory, error)

NewDirectory builds the directory leg for a config relay.New has already accepted. It takes the same three things the transport does — the config, the daemon's identity and the device registry — plus the sink a verified revocation acts on.

The config's checks are relay.New's, not repeated here: this leg dials the same host with the same secret, and a daemon that has no relay transport has no directory either (cmd/flue starts them together).

func (*Directory) Counts added in v0.2.0

func (d *Directory) Counts() DirectoryCounts

Counts is the last read of the directory, for the status surfaces.

func (*Directory) PublishFleetBlob added in v0.2.0

func (d *Directory) PublishFleetBlob(blob []byte)

PublishFleetBlob queues one artifact this daemon just minted. It never blocks and never fails, which is daemon.FleetPublisher's contract: the caller is a revoke, with a client waiting and nowhere to put a network error.

**A device certificate is dropped here, whoever offers one.** It is the only blob kind this leg refuses, and the refusal is a guard rather than a policy the callers rely on: `mine` does not publish them and the pairing ceremony does not either, so nothing should ever reach this line — but the cost of one that did is permanent (nothing in the directory is deleted one entry at a time) and public (`GET /directory` needs no credential, and a device cert names a device key and the label its owner typed). A future call site that forgets meets a log line here instead of a roster of the operator's devices on the open internet. Why they are not in the directory at all is on `mine`.

The parse is a decode of bytes this daemon just signed, on the caller's goroutine: microseconds, and the queue send below is what must not block. Unparseable blobs are passed through rather than dropped — this is not a validator, and the directory's own readers verify everything they ingest.

A full queue drops the blob with a log line rather than waiting. That is safe for exactly one reason, and it is worth naming: everything publishable is written to disk first — the revocation into revocations.json, the machine cert into relay.json — and publishAll re-offers all of it on every connect and every republishEvery. A dropped push costs latency, never the artifact.

func (*Directory) Run added in v0.2.0

func (d *Directory) Run(ctx context.Context) error

Run holds the directory socket and keeps this machine converged with the fleet until ctx is done.

The shape is Transport.Run's, deliberately and not by accident: the same jittered backoff, the same stable-connection rule that stops two daemons fighting over one leg from hammering a Worker with a 100k-request day, and the same "cancellation is not an error" contract. Two legs to one relay that reconnected on two different disciplines would be two things to reason about during the same outage.

func (*Directory) Snapshot added in v0.3.0

func (d *Directory) Snapshot(ctx context.Context) ([]byte, error)

Snapshot reads the directory and returns the relay's answer body unchanged, for a caller that has to hand those bytes to somebody else.

The one caller is the daemon's own loopback UI (daemon.FleetDirectoryPath). A browser on http://127.0.0.1:7717 cannot make this read for itself: the relay is another origin and the Worker sends no `Access-Control-Allow-Origin` on any route, so the fetch is discarded by the browser before a byte of it is readable. This daemon already holds the leg, so it makes the read and passes the body through.

**Verbatim, and nothing here interprets it.** This is deliberately not ingestSnapshot: it does not verify, does not count, does not touch the sink, does not remember an ETag, and returns bytes rather than facts. The reader at the far end verifies every blob under the fleet public key it pinned, exactly as it would against the relay directly — so this function must stay incapable of changing what that reader concludes. Parsing here would create a second opinion about the same document, and the whole design turns on there being only one: the signature.

No conditional read either. The caller needs a body every time, and the ETag this leg caches is a claim about what *this process* has applied, which is a different question from what the browser has seen.

The secret rides along for the reason ingestSnapshot's does: the route takes no credential, but the Worker meters exactly the callers that present none, and a daemon's read has no business spending the per-IP budget that exists to keep browsers working.

type DirectoryCounts added in v0.2.0

type DirectoryCounts struct {
	// Connected is whether this daemon is holding the directory socket right
	// now. The counts below are the last snapshot read *on that socket*, so
	// they are meaningless without it and are cleared with it.
	Connected   bool
	Entries     int
	Verified    int
	Machines    int
	Devices     int
	Revocations int
}

DirectoryCounts is what the last read of the directory found — for the status surfaces, and for nothing else. Verified is the only number with any authority in it; Entries is what the relay claimed to be holding.

type FleetSink added in v0.2.0

type FleetSink interface {
	// ApplyFleetRevocation records a revocation whose signature the caller has
	// already checked under the fleet public key, drops any local registry row
	// for the key, and closes that device's live channels. It must be
	// idempotent: the same revocation arrives on every reconnect.
	ApplyFleetRevocation(deviceKey, blob []byte) error
}

FleetSink is what a *verified* directory artifact does to this machine.

One method, because one of the three kinds of blob has a local consequence. A revocation is the fleet-wide kill switch and has to reach the device store and the live channels; a machine cert is for browsers, which read the directory themselves; and a device cert deliberately does nothing here — see ingest, which explains at length why a daemon that pre-populated its registry from this channel would be widening rule 1 of the acceptance order to keys nobody has proved they hold.

type Server

type Server interface {
	ServeConn(ctx context.Context, mc daemon.MessageConn, meta daemon.ConnMeta)
	// PairDevice runs the pairing ceremony on a body whose provenance this
	// package has already checked — the announced origin being the relay this
	// daemon dialled. It answers the status and JSON body the Worker writes
	// back to the browser that posted it.
	PairDevice(body []byte, peer string) daemon.PairOutcome
	// SetRelayStatus reports what this transport is doing, in the daemon's own
	// vocabulary: daemon.RelayConnecting while there is no socket, and
	// daemon.RelayConnected with the configured origin while there is one. The
	// daemon builds pairing URLs and answers welcomes out of it, so it tracks
	// the socket rather than the configuration — a relay that is configured and
	// down is one nothing can be reached through.
	SetRelayStatus(status, origin string)
}

Server is the surface the adapter drives — implemented by *daemon.Server.

It is declared here rather than in daemon because it is this package that needs it: the daemon does not know a relay exists. Two calls, because the relay carries two kinds of thing — a browser's connection, and the one part of the ceremony that is an HTTP request rather than a WebSocket message.

type Transport

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

Transport dials the relay and serves channels until ctx ends. It reconnects with jittered exponential backoff and only returns when its context is done.

func New

func New(cfg Config, srv Server, identity noise.DHKey, devices *crypto.DeviceStore, log *slog.Logger) (*Transport, error)

New builds a transport, or says why the configuration cannot make one. A nil logger discards, so a caller that has not wired one up gets a working adapter rather than a panic on the first log line.

All three fields are required, and Origin is required for a reason worth stating: it is not decoration, it is the value every announced open and every forwarded pair is checked against (channel.go). An empty Origin makes both checks vacuous — a relay announcing `origin:""` matches it — and it is also what ConnMeta.Origin carries into the daemon, which is what pairing URLs are built from. A misconfigured field must fail here, at wiring time, rather than quietly disarming the check it exists for.

URL and the secret are required for the ordinary reason: without them there is nothing to dial and nothing to authenticate with, so Run would do nothing but fail and back off forever. The machine id is the same kind of required: it is the path this transport dials, and without one every dial would meet the Worker's "no such machine" 404 while the config looked complete. Held to the relay's own grammar too, not merely non-empty, because an id the Worker will not route earns exactly that 404 with a value in the field.

func (*Transport) Run

func (t *Transport) Run(ctx context.Context) error

Run dials the relay and keeps it dialled until ctx is done.

It returns nil on cancellation: being asked to stop, and stopping, is not an error the caller has to recognise and filter out — the same contract daemon.ListenAndServe keeps. Every other outcome is a reason to try again, so nothing else ever returns from here. A relay that is down, misconfigured, or answering 401 while its operator rotates a secret is a temporary state, and a daemon that gave up on it would never come back without a restart.

Jump to

Keyboard shortcuts

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