Load proxy settings from a file, and import them from a pendrive

Everything came from CONTINUUM_* environment variables, which suits neither
shape the product ships in. A Proxy Box is an appliance whose settings
arrive on a USB stick; a proxy on someone's PC is a config file they edit.

Precedence is stated once, in config.rs: the file is the base, env overrides
it, defaults fill the rest. `Config::from_env` keeps its behaviour and the
struct is unchanged, so existing callers and tests are untouched. Layering
is `Config::layered(file, env_lookup)` with the environment injected as a
closure, so tests do not have to mutate process env to cover it.

The bootstrap is read from $CONTINUUM_BOOTSTRAP, then
/etc/continuum/bootstrap.toml, then nowhere - and nowhere means PC mode with
./continuum.toml, so an env-only deployment behaves exactly as before. The
working directory is deliberately not searched, and the bootstrap itself
must never come from a stick: it is what says whether sticks are trusted at
all.

Bundle format is continuum-config.zip holding config.toml and
config.toml.sha256. The hash is checked before the bytes are parsed - the
TOML parser should not be the first thing to read possibly-corrupt input -
and **the comments say plainly what that buys: corruption and change, not
forgery.** Anyone who can write the stick writes both files. It is an
integrity check under a recorded trust assumption, that physical access to
the box is trusted; a signature with the key in the box image is named as
the follow-up that would give authenticity. sha256 via openssl, already a
direct dependency, so no new crate and `cargo deny check licenses` still
passes.

The hazards of ingesting a file off removable media, all tested:

Zip-slip is refused on absolute paths, `..` components, backslash
separators, drive prefixes, NUL and non-UTF-8 names, and one bad entry
rejects the whole bundle rather than being skipped. The stronger guarantee
is structural though: entries are only ever read **by index, matched against
two literal names**, and no write path is ever derived from an archive name,
so a hole in the name check still writes nothing. A test confirms a bundle
carrying `../evil.toml` alongside a valid config writes nothing outside the
target and leaves the installed config alone.

Zip bombs are capped at 16 entries, 256 KiB per entry and a 1 MiB file on
the stick. Declared sizes are refused in a first pass that decompresses
nothing, and a bounded reader then caps actual output so a lying header
gains nothing.

Installing is atomic: temp file in the destination directory - not /tmp,
since rename is only atomic within a filesystem - created 0600 from the
first byte because the config holds the gateway token, fsync, rename, then a
best-effort directory fsync. A write that fails halfway leaves the previous
config intact and no temp file behind.

A bad stick never costs the box its config: a bad hash, bad TOML, an unsafe
entry or missing settings all return Rejected and are logged as keeping the
existing config. A correctly-hashed config is also validated **with no
environment at all**, so a stick cannot install something that parses now
and fails to start on the next boot - which would be a bricked box caused by
exactly the stick we were being careful about.

PC mode ignores pendrives twice over, by mode and by an emptied directory
list, because on a general-purpose machine importing settings from a USB
stick would be a way to change them behind the user's back.

A [network] block is parsed, validated and exposed, and stops there. main.rs
logs a warning that this build does not apply it, so a box given network
settings cannot look like it acted on them. Applying them needs root, is
specific to the appliance image, and a wrong write strands the box - it wants
its own decision, not a side effect of reading a file.

continuum.toml is gitignored, since it holds the gateway token.

Not done, and not claimed: no signature, so no authenticity. There is no real
pendrive here, so the removable-media path was tested with ordinary
directories standing in for mount points - real udisks/udev mounts, FAT
behaviour, a genuinely yanked stick and how promptly the 5s poll notices a
mount are all unproven. The watcher's install-then-exit flow, which relies
on a supervisor restarting the process, has no test and no supervisor exists
in this environment; a stick left permanently inserted reports Unchanged
rather than looping, because the hash matches. Env still overrides on a box,
so a box is only pendrive-only to the extent its environment is trusted -
bootstrap.rs says so.
This commit is contained in:
2026-10-19 07:49:46 +00:00
parent da28597eeb
commit 64d46dbc49
8 changed files with 1469 additions and 15 deletions
+43
View File
@@ -0,0 +1,43 @@
# The proxy's OWN settings - not the printers (that is printers.toml, which
# holds per-site secrets and is never shipped on a pendrive). See
# src/config.rs for how this file layers with CONTINUUM_* environment
# variables: this file is the base, an env var overrides the same key.
#
# PC / server: copy to continuum.toml next to the proxy and edit it. Nothing
# else is needed; removable media is never read.
# Proxy Box: put this file in a ZIP named continuum-config.zip together
# with its SHA-256, and insert the stick:
#
# cp continuum.example.toml config.toml # edit it
# sha256sum config.toml > config.toml.sha256
# zip continuum-config.zip config.toml config.toml.sha256
#
# The hash catches a damaged or hand-edited stick. It does NOT
# prove who made it - anyone who can write the stick can write
# both files. See src/config_import.rs.
#
# Unknown keys are rejected on purpose, so a typo cannot silently fall back to
# a default. On a Proxy Box the file must be complete by itself.
gateway_id = "REPLACE_ME"
farm_id = "REPLACE_ME"
gateway_token = "REPLACE_ME"
# uplink_url = "wss://api.continuum.local/ws/edge/v1"
# go2rtc_bin = "go2rtc"
# go2rtc_config = "./go2rtc.yaml"
# go2rtc_api_listen = ":1984"
# stream_base_url = "https://proxy.example/"
# printers_path = "/var/lib/continuum/printers.toml" # absolute on a box
# Proxy Box only. PARSED AND VALIDATED, NOT APPLIED - this build never
# touches the OS network configuration.
# [network]
# mode = "static" # or "dhcp"
# ip = "192.168.1.60"
# prefix_len = 24
# gateway = "192.168.1.1"
# dns = ["1.1.1.1"]
# [network.wifi]
# ssid = "farm"
# psk = "REPLACE_ME"