server/downstream/src/front.rs
//! **The front's report to main** (lane HOL-FLOW-FRONT, Henrik's Q1 of HOL-FLOW-SMART).
//!
//! Main draws the estate's flow, and the front is one of its machines; but main does not know
//! the front's doors, the names it serves or where each name is routed. So the front TELLS it,
//! the way a downstream tells its upstream its RON summary: a small report, with a proof of
//! its own, that an older main ignores safely (its HTTP door answers `404` for a path it does
//! not know, and the front says so and carries on).
//!
//! ```text
//! front ── POST /-/front/report { report, proof } ──► main: check() → <data>/holger-front/report.ron
//! flow: the front unfolds to its doors and routes
//! ```
//!
//! | piece | what it is |
//! |---|---|
//! | [`FrontReport`] | the front's doors ([`FrontDoor`]), the names it serves and where each goes ([`FrontRoute`]), its console's address; names and ports only, never a key, a token or a certificate |
//! | [`text`] | the bytes the proof is over: one line per field, the same order on both ends |
//! | [`key_of`] | the proof's key, derived from the x25519 secret the front's console key and the box's console key share (the pair the console's bearer comes from), under its OWN context ([`KEY_CONTEXT`]): the key proves this front to this box, and is no bearer |
//! | [`proof`], [`check`] | a keyed blake3 over [`text`]; `check` also refuses a report older or newer than [`FRESH_SECS`], and one too large to be a front's |
//! | [`console_url`] | a console address a page may link to: `https://`, or `http://` on a loopback host only, nothing that is not plainly a URL |
//!
//! Pure: no I/O, no network.
use serde::{Deserialize, Serialize};
/// Main's HTTP door that takes the report (`POST`, JSON [`Sent`]).
pub const PATH: &str = "/-/front/report";
/// The derivation's context (blake3 `derive_key`): never the console bearer's.
pub const KEY_CONTEXT: &str =
"holger 2026-11-01 the front's report to a box, from the front's and the box's x25519 console keys";
/// The first line of [`text`].
pub const SCHEMA: &str = "holger.front-report/v1";
/// A report signed further than this from the box's clock is refused (seconds).
pub const FRESH_SECS: i64 = 600;
/// How many doors and routes a front reports at most, and how long one field may be.
pub const MAX_DOORS: usize = 32;
pub const MAX_ROUTES: usize = 128;
pub const MAX_FIELD: usize = 256;
/// Where a route goes: main's repository doors (HTTP and the product's gRPC).
pub const TO_MAIN: &str = "main";
/// Where a route goes: the console (holger-ui), whose pass-through reaches main's two doors.
pub const TO_CONSOLE: &str = "console";
/// Where a route goes: the ONE monetize (its control host or a vendor's callbacks).
pub const TO_MONETIZE: &str = "monetize";
/// **What the front serves, as it reports it to main.**
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct FrontReport {
/// The front's pairing name (`front`, `vm1`, …).
pub front: String,
/// When the front made it (Unix seconds, the front's clock: the estate's).
pub at_unix: i64,
/// The front's own doors (`https` tcp/443, `http` tcp/80, the downstreams' udp/51830).
pub doors: Vec<FrontDoor>,
/// Every name the front serves and where it is routed.
pub routes: Vec<FrontRoute>,
/// The console's address (`https://www.<site>`); empty when the front serves none.
pub console_url: String,
}
/// **One of the front's doors.**
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct FrontDoor {
/// `https`, `http`, `downstreams`.
pub name: String,
/// `tcp/443`, `udp/51830`.
pub listen: String,
/// One line: what it is.
pub what: String,
}
/// **One name the front serves, and where it goes.**
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct FrontRoute {
/// The served name (`repo.example.com`), or the door's own address for a door no name
/// reaches (the downstreams' sync door).
pub host: String,
/// The door it comes in by ([`FrontDoor::name`]).
pub door: String,
/// [`TO_MAIN`], [`TO_CONSOLE`] or [`TO_MONETIZE`] (a later front may name more; main draws
/// a route to a word it does not know as that word, inside the front).
pub to: String,
/// One line: what reaches it there (`main http :8443, gRPC :50051 (holger.*)`).
pub what: String,
}
/// **What the front sends** (the JSON body of [`PATH`]).
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct Sent {
pub report: FrontReport,
/// [`proof`] of `report`, hex.
pub proof: String,
}
fn one_line(v: &str) -> String {
v.replace(['\n', '\t', '\r'], " ")
}
/// Pure: **the bytes the proof is over**: [`SCHEMA`], the front and the time, then one
/// `door` line per door, one `route` line per route and the `console` line, in order.
pub fn text(r: &FrontReport) -> String {
let mut t = format!(
"{SCHEMA}\nfront\t{}\nat\t{}\n",
one_line(&r.front),
r.at_unix
);
for d in &r.doors {
t.push_str(&format!(
"door\t{}\t{}\t{}\n",
one_line(&d.name),
one_line(&d.listen),
one_line(&d.what)
));
}
for x in &r.routes {
t.push_str(&format!(
"route\t{}\t{}\t{}\t{}\n",
one_line(&x.host),
one_line(&x.door),
one_line(&x.to),
one_line(&x.what)
));
}
t.push_str(&format!("console\t{}\n", one_line(&r.console_url)));
t
}
/// Pure: **the proof's key**, from the x25519 secret the front's and the box's console keys
/// share, under [`KEY_CONTEXT`].
pub fn key_of(shared: &[u8; 32]) -> [u8; 32] {
blake3::derive_key(KEY_CONTEXT, shared)
}
/// Pure: **the report's proof** under `key`, hex.
pub fn proof(key: &[u8; 32], r: &FrontReport) -> String {
blake3::keyed_hash(key, text(r).as_bytes())
.to_hex()
.to_string()
}
/// Pure: **is this the front's report, now?** The proof under `key` (compared in constant
/// time), signed within [`FRESH_SECS`] of `now_unix`, and no larger than a front's. The
/// refusal says which, in one line.
pub fn check(
key: &[u8; 32],
r: &FrontReport,
proof_hex: &str,
now_unix: i64,
) -> Result<(), String> {
if r.doors.len() > MAX_DOORS || r.routes.len() > MAX_ROUTES {
return Err(format!(
"the report names {} doors and {} routes; a front has at most {MAX_DOORS} and {MAX_ROUTES}",
r.doors.len(),
r.routes.len()
));
}
let long = std::iter::once(r.front.as_str())
.chain(std::iter::once(r.console_url.as_str()))
.chain(
r.doors
.iter()
.flat_map(|d| [d.name.as_str(), d.listen.as_str(), d.what.as_str()]),
)
.chain(r.routes.iter().flat_map(|x| {
[
x.host.as_str(),
x.door.as_str(),
x.to.as_str(),
x.what.as_str(),
]
}))
.any(|f| f.len() > MAX_FIELD);
if long {
return Err(format!(
"a field of the report is longer than {MAX_FIELD} bytes"
));
}
let given = blake3::Hash::from_hex(proof_hex.trim())
.map_err(|_| "its proof is not 64 hex characters".to_string())?;
// blake3::Hash compares in constant time.
if given != blake3::keyed_hash(key, text(r).as_bytes()) {
return Err("its proof does not hold under this box's key for its front".into());
}
if (now_unix - r.at_unix).abs() > FRESH_SECS {
return Err(format!(
"it was made at {} and this box's clock says {now_unix}: more than {FRESH_SECS} s apart",
r.at_unix
));
}
Ok(())
}
/// Pure: **a console address a page may link to**, without a trailing `/`: `https://` and a
/// plain host (letters, digits, `.`, `-`, an optional `:port`) and an optional plain path, or
/// `http://` to a loopback host (`localhost`, `127.0.0.1`, `[::1]`: a console on the same
/// machine). Anything else (`javascript:`, quotes, spaces, a user part) is `None`.
pub fn console_url(raw: &str) -> Option<String> {
let raw = raw.trim().trim_end_matches('/');
let (https, rest) = if let Some(r) = raw.strip_prefix("https://") {
(true, r)
} else {
(false, raw.strip_prefix("http://")?)
};
let (authority, path) = match rest.find('/') {
Some(i) => (&rest[..i], &rest[i..]),
None => (rest, ""),
};
let (host, port) = if let Some(h) = authority.strip_prefix("[::1]") {
("[::1]", h)
} else {
match authority.rfind(':') {
Some(i) => (&authority[..i], &authority[i..]),
None => (authority, ""),
}
};
let host_ok = host == "[::1]"
|| (!host.is_empty()
&& host.len() <= 253
&& host
.bytes()
.all(|b| b.is_ascii_alphanumeric() || b == b'.' || b == b'-'));
let port_ok = port.is_empty()
|| (port.len() > 1 && port.len() <= 6 && port[1..].bytes().all(|b| b.is_ascii_digit()));
let path_ok = path
.bytes()
.all(|b| b.is_ascii_alphanumeric() || b"/-._~".contains(&b));
let loopback = matches!(host, "localhost" | "127.0.0.1" | "[::1]");
(host_ok && port_ok && path_ok && (https || loopback)).then(|| raw.to_string())
}
#[cfg(test)]
mod tests {
use super::*;
fn report(at: i64) -> FrontReport {
FrontReport {
front: "front".into(),
at_unix: at,
doors: vec![FrontDoor {
name: "https".into(),
listen: "tcp/443".into(),
what: "the web face".into(),
}],
routes: vec![FrontRoute {
host: "repo.example.com".into(),
door: "https".into(),
to: TO_MAIN.into(),
what: "main http :8443".into(),
}],
console_url: "https://www.example.com".into(),
}
}
/// ★ The front's report is proven by the key its console key and the box's share, near
/// now; a changed report, another key, an old report or a proof that is no proof is
/// refused by name.
#[test]
fn a_front_report_holds_under_its_key_near_now_and_nothing_else_does() {
let key = key_of(&[7; 32]);
let r = report(1_000_000);
let p = proof(&key, &r);
assert_eq!(check(&key, &r, &p, 1_000_100), Ok(()));
let mut changed = r.clone();
changed.routes[0].to = TO_CONSOLE.into();
assert!(check(&key, &changed, &p, 1_000_100)
.unwrap_err()
.contains("does not hold"));
assert!(check(&key_of(&[8; 32]), &r, &p, 1_000_100)
.unwrap_err()
.contains("does not hold"));
assert!(check(&key, &r, &p, 1_000_000 + FRESH_SECS + 1)
.unwrap_err()
.contains("apart"));
assert!(check(&key, &r, "not hex", 1_000_100)
.unwrap_err()
.contains("hex"));
let mut big = r.clone();
big.doors = vec![FrontDoor::default(); MAX_DOORS + 1];
let bp = proof(&key, &big);
assert!(check(&key, &big, &bp, 1_000_100)
.unwrap_err()
.contains("at most"));
assert_ne!(
key,
*blake3::hash(&[7; 32]).as_bytes(),
"its own context, never a plain hash"
);
}
#[test]
fn the_text_is_one_line_per_field_in_order_and_a_newline_cannot_forge_a_line() {
let mut r = report(5);
r.routes[0].what = "a\nroute\tforged".into();
assert_eq!(
text(&r),
"holger.front-report/v1\nfront\tfront\nat\t5\ndoor\thttps\ttcp/443\tthe web face\nroute\trepo.example.com\thttps\tmain\ta route forged\nconsole\thttps://www.example.com\n"
);
}
#[test]
fn a_console_url_is_https_or_a_loopback_and_nothing_else() {
assert_eq!(
console_url("https://www.example.com/"),
Some("https://www.example.com".into())
);
assert_eq!(
console_url("https://lab-1.example.com:8443/console"),
Some("https://lab-1.example.com:8443/console".into())
);
assert_eq!(
console_url("http://localhost:4000"),
Some("http://localhost:4000".into())
);
assert_eq!(
console_url("http://127.0.0.1:9"),
Some("http://127.0.0.1:9".into())
);
for bad in [
"",
"http://lab-1.example.com",
"javascript:alert(1)",
"https://x.example.com/\"onmouseover=1",
"https://user@x.example.com",
"https://x.example.com/a b",
"https://x.example.com:port",
"ftp://x.example.com",
"https://",
] {
assert_eq!(console_url(bad), None, "{bad}");
}
}
}