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:
+30
-1
@@ -9,7 +9,9 @@ mod go2rtc;
|
||||
use tracing::info;
|
||||
use tracing_subscriber::EnvFilter;
|
||||
|
||||
use continuum_proxy::bootstrap::Bootstrap;
|
||||
use continuum_proxy::config::Config;
|
||||
use continuum_proxy::config_import;
|
||||
use continuum_proxy::printer::{GenericPrinter, PrinterHandle};
|
||||
use continuum_proxy::{camera, completion, fleet, uplink};
|
||||
|
||||
@@ -20,7 +22,25 @@ async fn main() -> anyhow::Result<()> {
|
||||
.init();
|
||||
|
||||
dotenvy::dotenv().ok();
|
||||
let config = Config::from_env()?;
|
||||
|
||||
// Order matters: import first, so a stick present at boot is already
|
||||
// the installed config by the time it is read. A rejected stick is
|
||||
// logged inside and does not stop startup - the box runs on what it has.
|
||||
let bootstrap = Bootstrap::discover()?;
|
||||
for outcome in config_import::import_from_pendrives(&bootstrap) {
|
||||
config_import::log_outcome(&outcome);
|
||||
}
|
||||
let file = bootstrap.load_config_file()?;
|
||||
let config = Config::from_file_and_env(file.as_ref())?;
|
||||
info!(mode = ?bootstrap.mode, config_file = ?file.as_ref().map(|_| &bootstrap.config_path), "configuration loaded");
|
||||
|
||||
// BOUNDARY: network settings are parsed and validated, and stop here.
|
||||
// Applying them (static IP, DNS, wifi) is the appliance image's job, not
|
||||
// this process's - see the module docs of `config`. Logged so that a box
|
||||
// given a network block does not look like it acted on it.
|
||||
if let Some(network) = file.as_ref().and_then(|f| f.network.as_ref()) {
|
||||
tracing::warn!(?network, "network settings are present but this build does NOT apply them");
|
||||
}
|
||||
|
||||
tracing::info!(gateway_id = %config.gateway_id, farm_id = %config.farm_id, "starting continuum-proxy");
|
||||
|
||||
@@ -86,6 +106,15 @@ async fn main() -> anyhow::Result<()> {
|
||||
restart_go2rtc.clone(),
|
||||
) => {}
|
||||
_ = completion::run(fleet.clone()) => {}
|
||||
// Returns only after installing a new config from a stick inserted
|
||||
// while running. Settings are read once at startup, so the process
|
||||
// ends here and relies on its supervisor (systemd `Restart=always`
|
||||
// on a box) to start it again on the new config. Not exercised end
|
||||
// to end - there is no supervisor or real stick in the dev
|
||||
// environment.
|
||||
_ = config_import::watch(bootstrap.clone()) => {
|
||||
info!("exiting so the supervisor restarts the proxy on the newly installed config");
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
|
||||
Reference in New Issue
Block a user