fix(deploy): the tunnel's door binds loopback only (P9a)

Review finding: `--http` parsed whatever address it was given and bound
it. The door is the plaintext MCP service with a static bearer token;
plan 3.9 and the P9a model put a TLS proxy or tunnel in front of a
loopback listener, and no decision authorises exposing the books
unencrypted. `--http 0.0.0.0:7411` would have done exactly that.

`local::tunnel_address` now accepts only a loopback address
(127.0.0.0/8 or ::1; `127.0.0.1:7411` stays the default) and refuses
anything else with a message naming loopback and why. `server` refuses
while parsing, before it takes the lock or binds a socket; `serve`
refuses before it starts a host, so nothing is left half-open. The
bind-failure behaviour for a loopback address is unchanged (said, not
fatal, 3.9), as is the unit's hardening.

The test `the_tunnel_door_binds_loopback_only` refuses 0.0.0.0, a public
sample address and [::] through `server`, checks no socket or host was
left behind, refuses through `serve`, then starts a loopback listener and
reaches `books.start` with the bearer.

No accounting code, report or figure changes.
This commit is contained in:
swaits committed 2026-10-27 09:35:36 -06:00
1 parent 47951e5734
commit c8541ea9fc
7 files changed
+156 -29

No files matched your search

+16 -7
View File
@@ -254,12 +254,20 @@ pub fn serve(
server_flags.push(level);
}
"--http" => {
let addr = match args.peek() {
Some(v) if !v.starts_with('-') => args.next().unwrap_or_default(),
_ => "127.0.0.1:7411".to_string(),
};
// Before a host starts: the door is loopback only (3.9, P9a).
let addr = match ledger_mcp::local::tunnel_address(&addr) {
Ok(addr) => addr,
Err(why) => {
eprintln!("error: {why}");
return Err(exit(2));
}
};
server_flags.push(arg);
if let Some(v) = args.peek()
&& !v.starts_with('-')
{
server_flags.push(args.next().unwrap_or_default());
}
server_flags.push(addr.to_string());
}
"--help" | "-h" => {
eprintln!("usage: {prog} [--root DIR] [--isolation LEVEL] [--http [ADDR]]");
@@ -267,8 +275,9 @@ pub fn serve(
eprintln!(" after nothing else (the app, an agent session) holds them.");
eprintln!(" --isolation same_user | enforced: how books.start says these books");
eprintln!(" are kept from agents (enforced: the owner's own machine, P9a).");
eprintln!(" --http [ADDR]: also serve the MCP door on 127.0.0.1:7411 (or ADDR),");
eprintln!(" for a TLS proxy or a tunnel in front of it.");
eprintln!(" --http [ADDR]: also serve the MCP door on loopback only (default");
eprintln!(" 127.0.0.1:7411), for a TLS proxy or a tunnel in front of it; any");
eprintln!(" other address is refused.");
return Ok(());
}
other => {