config

package
v0.3.2 Latest Latest
Warning

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

Go to latest
Published: Aug 10, 2026 License: MIT Imports: 13 Imported by: 0

Documentation

Overview

Package config locates and manages flue's on-disk configuration, including the loopback authentication token.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func DeleteCloudflare

func DeleteCloudflare() error

DeleteCloudflare removes the stored credential; absent is success.

func DeleteRelay added in v0.2.1

func DeleteRelay() error

DeleteRelay removes relay.json: this machine's whole membership of a relay, in one file. Absent is success, the same way DeleteCloudflare treats it — leaving a relay this machine is not on is a no-op, not a fault.

It deletes exactly one file and touches nothing else in the directory. The static key, devices.json, revocations.json and cloudflare.json are all separate concerns with separate lifecycles: this is "stop being on that relay", not "forget everything the relay ever touched", and `flue relay leave` says so out loud because the difference is not guessable.

What goes with the file is worth naming here, because nothing else can put it back: the daemon secret (whose other copy is a Worker binding Cloudflare will not read back out) and the fleet key (whose only other copies are the relay.json files of the machines still on that relay). Rejoining means the join line from one of them.

func Dir

func Dir() (string, error)

Dir returns the flue config directory, creating it if needed.

func LoadOrCreateToken

func LoadOrCreateToken() (string, error)

LoadOrCreateToken returns the daemon's loopback token, generating and persisting one at mode 0600 on first use.

func MachineIDTag added in v0.2.0

func MachineIDTag(secret, slug string) string

MachineIDTag is the self-certifying suffix of a machine id: the first 8 lowercase hex characters of HMAC-SHA256(secret, "flue-machine-id/"+slug).

The Worker recomputes exactly this before it lets any id pick a Durable Object (relay/src/index.ts, machineTag), which is what closes the open id namespace: an id whose tag does not verify is answered with the same 404 a malformed id gets, and no object wakes. The two implementations are pinned to each other by testdata/relay/machine-ids.json.

func MintMachineID

func MintMachineID(hostname, secret string, r io.Reader) string

MintMachineID makes the id a machine joins the relay under: `<hostname sanitized, truncated to 24>-<4 lowercase hex>-<8 hex tag>`.

The hostname part is for the human reading a machine list; the random hex is for the relay, where the id is the routing key and a collision means the second machine evicts the first from its hub; the tag is MachineIDTag over everything before it, and is what makes the id verifiable by the Worker without any registry. secret is the relay's DAEMON_SECRET — both minting commands (`flue relay setup`, `flue relay join`) hold it at mint time, and an id minted under any other string is unroutable. r is crypto/rand.Reader everywhere but tests, which inject fixed bytes to pin the format.

func SaveCloudflare

func SaveCloudflare(c Cloudflare) error

SaveCloudflare writes cloudflare.json at 0600, replacing whatever was there — the same fresh-inode rename dance SaveRelay does, for the same concurrent-reader reasons.

func SaveRelay

func SaveRelay(r Relay) error

SaveRelay writes relay.json at 0600, replacing whatever was there.

It goes through the same write-temp-then-rename path as the token, and for the same two reasons: the new secret is never visible at a mode the old file happened to carry, and a crash mid-write cannot leave a truncated file behind where a complete one was. See writeSecretAtomically.

Types

type Cloudflare

type Cloudflare struct {
	// Token is the API token setup verified. Never logged, never echoed into
	// an error, never carried by any endpoint response — reads of this file
	// hand it to the Cloudflare API and nowhere else.
	Token string `json:"token"`
	// AccountID and AccountName are the account the relay was deployed into,
	// recorded so the UI can say where the relay lives and so an update needs
	// no account picker.
	AccountID   string `json:"account_id"`
	AccountName string `json:"account_name,omitempty"`
}

Cloudflare is the stored Cloudflare credential and the account it deploys into: what makes "update the relay" a click instead of a trip to the token page.

Stored by explicit product decision: the token is an account-level Workers credential and flue keeps it the way it keeps the relay secret — one 0600 file in the config directory, owner-readable and nothing else. The user is doing this on their own machine for their own account; the file is theirs to delete, and everything that reads it works (by asking again) when it is gone.

func LoadCloudflare

func LoadCloudflare() (Cloudflare, bool, error)

LoadCloudflare reads cloudflare.json. ok is false when nothing is stored.

type Relay

type Relay struct {
	// URL is the bare wss:// address of the relay — host only, no path. The
	// transport appends /daemon/<machine id> itself, so the file never holds a
	// path that could disagree with the id beside it.
	URL    string `json:"url"`
	Origin string `json:"origin"`

	// Secret is the relay's shared DAEMON_SECRET: the deploy set it on the
	// user's own Worker, and the daemon presents it on every dial. Never
	// logged, and the reason this file is 0600. It does ride argv exactly
	// once — `flue relay join --secret`, the hand-off line setup prints —
	// and that is deliberate: argv and the shell history that keeps it are
	// the accepted cost of moving the secret to the next machine
	// (docs/RELAY.md says so where the line is printed).
	Secret string `json:"secret,omitempty"`

	// FleetSeed is the fleet key's Ed25519 seed, unpadded base64url —
	// exactly the value `flue relay join --fleet` was given, minted by
	// `flue relay setup` alongside the secret (spec/fleet-trust.md and
	// internal/fleet own its meaning; this package only stores the string).
	// It is the one credential in this file the Worker never holds: the
	// secret's other copy lives on Cloudflare, the fleet key's copies live
	// only in the fleet's relay.json files and the join line that carried
	// it between them. Never logged, never sent anywhere, and the second
	// reason this file is 0600 — a leaked seed signs device certs every
	// machine in the fleet honours.
	//
	// It rides argv exactly once, beside the secret and for the same
	// reason: `flue relay join --fleet` is the deliberate hand-off. The
	// cost is the same shell history, and the consequence is larger, which
	// is why docs/RELAY.md states it where it teaches the line rather than
	// leaving it to be inferred from the secret's warning.
	FleetSeed string `json:"fleet_seed,omitempty"`

	// MachineCert is this machine's fleet machine certificate — the signed
	// blob internal/fleet produces over {id, name, this daemon's Noise
	// static public key, iat} — minted by whichever command wrote this file
	// (`flue relay setup`, `flue relay join`, the Remote screen's deploy) and
	// published to the relay's fleet directory by the daemon when it
	// connects. It is a public artifact: it is signed, not secret, and every
	// browser in the fleet reads it out of the directory to learn which Noise
	// key to pin for this machine (spec/fleet-trust.md).
	//
	// It is stored rather than minted at startup because the directory is
	// content-addressed: a cert re-minted on every boot would carry a fresh
	// `iat`, hash to a fresh key, and spend one of the directory's 512
	// entries per daemon restart. Stored, it is one entry forever.
	//
	// Empty means "this machine publishes no machine cert" — a relay.json
	// written before this field, or one whose fleet key could not sign. The
	// daemon says so once and carries on: a machine missing from the
	// directory is one browsers cannot auto-discover, not one that stops
	// working for the devices paired to it directly.
	MachineCert []byte `json:"machine_cert,omitempty"`

	// MachineID is the slot this machine holds on the relay: the <id> in the
	// /daemon/<id> leg the daemon dials and the /client/<id> URLs browsers
	// open. Minted by `flue relay setup` and `flue relay join` (MintMachineID),
	// never typed by hand — which is why it is a slug and the name below is
	// not.
	MachineID string `json:"machine_id"`
	// MachineName is the human label for this machine — free text, shown in
	// machine lists; it rides the pairing link's query (`n=`, so the pairing
	// browser can label the machine), never any path the relay routes on.
	MachineName string `json:"machine_name"`

	// Worker is the Cloudflare script name the relay was deployed under —
	// `flue relay setup --worker`, or the default. `flue relay update` reads
	// it back so a redeploy names the same script without re-deriving it from
	// the URL. Empty in files written by `flue relay join` (join deploys
	// nothing and does not know) and in files from before the field existed;
	// readers must treat empty as "derive or ask", never as a name.
	Worker string `json:"worker,omitempty"`
}

Relay is how this daemon reaches a deployed relay: where to dial, what authenticates the dial, and the origin the relay serves browsers on.

It is written by `flue relay setup` and read by `flue serve` at startup. This package deliberately does not decide whether a file is *complete* — see transport/relay.New, which is where that lives — because a file that exists says the user meant to have a relay, and telling them which field is missing is a better answer than pretending they never configured one.

func LoadRelay

func LoadRelay() (Relay, bool, error)

LoadRelay reads relay.json. ok is false when there is no relay configured at all, which is the ordinary state and not an error.

An unreadable or unparseable file *is* an error, and the distinction matters: "there is no relay" and "there is a relay and I cannot read it" lead to the same daemon — one serving loopback only — but only the second is something the operator has to be told about, and reporting it as absence would hide a typo in a file they just edited.

The file's mode is not policed the way the token's is. A loose mode on the token is evidence the secret leaked *and* a reason to mint a new one, which costs one browser session; the relay secret's other copy lives in a deployed Worker, so this process cannot regenerate it and refusing to read it would only take remote access away without making anything safer. SaveRelay writes 0600, and rotating a secret that has been exposed is the operator's move.

Jump to

Keyboard shortcuts

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