A draft spec for the owner's review and 13 offline tasks with their given tests: shared tool arguments and host rules in proto, the sealed fetch target (M3a finding 14), the toolkit tools and SOCKS5 egress proxy, and brokerd's [runner], podman argument lists, runtime and proxy lifecycle. Each task's tests were run against a reference at that task's end state (560 to 638 tests, clippy clean); the reference is not in the repository. Adds the runner-unavailable runbook entry and tip T23 (ETXTBSY in script tests). Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
214 lines
7.2 KiB
Rust
214 lines
7.2 KiB
Rust
//! `http_fetch` through the Podman runtime: the proxy is started first, its socket awaited, the tool
|
|
//! run with the directory mounted, and the proxy and directory removed afterwards on every path
|
|
//! (M3b spec, section 6, "http_fetch"). Against a fake `podman`. Do not edit.
|
|
|
|
#[path = "support/build.rs"]
|
|
mod build;
|
|
#[path = "support/fake_podman.rs"]
|
|
mod fake_podman;
|
|
|
|
use std::os::unix::fs::PermissionsExt;
|
|
use std::time::{Duration, Instant};
|
|
|
|
use brokerd::container::{CANNOT_START, Podman, RUNBOOK, TIMED_OUT};
|
|
use brokerd::policy::{Outcome, SessionState, decide};
|
|
use brokerd::runner::run;
|
|
use build::{fetch, grant, now, set};
|
|
use fake_podman::{Fake, Lines, path, serial};
|
|
use proto::{Mode, ToolResponse};
|
|
|
|
/// For `run -d` (the proxy): make the socket file in the mounted directory, as the proxy does.
|
|
const PROXY_OK: &str = r#"for a in "$@"; do case "$a" in --volume=*:/run/egress:rw) v=${a#--volume=}; v=${v%:/run/egress:rw};; esac; done"#;
|
|
|
|
fn body(proxy: &str, tool: &str) -> String {
|
|
format!("{PROXY_OK}\nif [ \"$2\" = -d ]; then\n{proxy}\nfi\n{tool}")
|
|
}
|
|
|
|
fn call(podman: &Podman) -> ToolResponse {
|
|
let grants = set(vec![
|
|
grant("web", "http_fetch", Mode::Auto).hosts(&["example.com", "*.example.org"]),
|
|
]);
|
|
let decision = match decide(
|
|
fetch("https://example.com/a"),
|
|
&grants,
|
|
SessionState::default(),
|
|
now(),
|
|
) {
|
|
Outcome::Allowed(d) => d,
|
|
other => panic!("not allowed: {other:?}"),
|
|
};
|
|
run(decision, podman)
|
|
}
|
|
|
|
fn failed(message: &str) -> ToolResponse {
|
|
ToolResponse::Failed {
|
|
message: message.to_string(),
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn the_proxy_starts_first_the_tool_gets_its_socket_and_both_are_cleaned_up() {
|
|
let _s = serial();
|
|
let fake = Fake::new(
|
|
"eg-ok",
|
|
&body(
|
|
r#": > "$v/egress.sock"; exit 0"#,
|
|
r#"cat > "$D/stdin"; printf 'body\n[http 200]'; exit 0"#,
|
|
),
|
|
);
|
|
let log = Lines::default();
|
|
let egress = fake.dir.join("egress");
|
|
let got = call(&Podman::new(fake.runner(""), egress.clone(), log.sink()));
|
|
assert!(
|
|
matches!(&got, ToolResponse::Result { content, .. } if content == "body\n[http 200]"),
|
|
"{got:?} {}",
|
|
log.all()
|
|
);
|
|
let calls = fake.calls();
|
|
assert_eq!(calls.len(), 3, "{calls:?}");
|
|
let dir = egress.join("boxmaker-s1-1-0");
|
|
// 1. The proxy, detached, with the call's hosts.
|
|
assert_eq!(&calls[0][..2], ["run", "-d"]);
|
|
assert!(calls[0].contains(&"--name=boxmaker-s1-1-0-egress".to_string()));
|
|
assert!(calls[0].contains(&format!("--volume={}:/run/egress:rw", path(&dir))));
|
|
assert_eq!(calls[0].last().unwrap(), "example.com,*.example.org");
|
|
// 2. The tool, with the same directory and no network.
|
|
assert_eq!(&calls[1][..3], ["run", "--rm", "-i"]);
|
|
assert!(calls[1].contains(&format!("--volume={}:/run/egress:rw", path(&dir))));
|
|
assert!(calls[1].contains(&"--network=none".to_string()));
|
|
assert_eq!(fake.stdin(), r#"{"url":"https://example.com/a"}"#);
|
|
// 3. The proxy removed, and its directory with it.
|
|
assert_eq!(calls[2], ["rm", "-f", "boxmaker-s1-1-0-egress"]);
|
|
assert!(!dir.exists(), "the call's directory is removed");
|
|
let mode = std::fs::metadata(&egress).unwrap().permissions().mode() & 0o777;
|
|
assert_eq!(mode, 0o700);
|
|
}
|
|
|
|
#[test]
|
|
fn a_proxy_that_podman_cannot_start_means_no_tool_and_nothing_left() {
|
|
let _s = serial();
|
|
let fake = Fake::new(
|
|
"eg-fail",
|
|
&body(
|
|
"echo 'Error: network pasta not found' >&2; exit 125",
|
|
"cat > /dev/null; exit 0",
|
|
),
|
|
);
|
|
let log = Lines::default();
|
|
let egress = fake.dir.join("egress");
|
|
let got = call(&Podman::new(fake.runner(""), egress.clone(), log.sink()));
|
|
assert_eq!(got, failed(CANNOT_START));
|
|
let calls = fake.calls();
|
|
assert!(
|
|
calls
|
|
.iter()
|
|
.all(|c| c.get(2).map(String::as_str) != Some("-i")),
|
|
"no tool ran: {calls:?}"
|
|
);
|
|
assert_eq!(
|
|
calls.last().unwrap(),
|
|
&["rm", "-f", "boxmaker-s1-1-0-egress"]
|
|
);
|
|
assert!(!egress.join("boxmaker-s1-1-0").exists());
|
|
assert!(
|
|
log.all().contains("network pasta not found"),
|
|
"{}",
|
|
log.all()
|
|
);
|
|
assert!(log.all().contains(RUNBOOK));
|
|
}
|
|
|
|
#[test]
|
|
fn a_proxy_that_makes_no_socket_in_time_means_no_tool_and_nothing_left() {
|
|
let _s = serial();
|
|
let fake = Fake::new("eg-nosock", &body("exit 0", "cat > /dev/null; exit 0"));
|
|
let log = Lines::default();
|
|
let egress = fake.dir.join("egress");
|
|
let podman = Podman::new(fake.runner(""), egress.clone(), log.sink())
|
|
.with_egress_wait(Duration::from_millis(200));
|
|
let started = Instant::now();
|
|
assert_eq!(call(&podman), failed(CANNOT_START));
|
|
assert!(started.elapsed() < Duration::from_secs(3));
|
|
let calls = fake.calls();
|
|
assert_eq!(calls.len(), 2, "{calls:?}");
|
|
assert_eq!(calls[1], ["rm", "-f", "boxmaker-s1-1-0-egress"]);
|
|
assert!(!egress.join("boxmaker-s1-1-0").exists());
|
|
assert!(log.all().contains(RUNBOOK));
|
|
}
|
|
|
|
#[test]
|
|
fn a_tool_that_runs_too_long_still_leaves_nothing_behind() {
|
|
let _s = serial();
|
|
let fake = Fake::new(
|
|
"eg-slow",
|
|
&body(
|
|
r#": > "$v/egress.sock"; exit 0"#,
|
|
"cat > /dev/null; exec sleep 30",
|
|
),
|
|
);
|
|
let log = Lines::default();
|
|
let egress = fake.dir.join("egress");
|
|
let got = call(&Podman::new(
|
|
fake.runner("http_fetch_ms = 300"),
|
|
egress.clone(),
|
|
log.sink(),
|
|
));
|
|
assert_eq!(got, failed(TIMED_OUT));
|
|
let calls = fake.calls();
|
|
let tail: Vec<Vec<String>> = calls[2..].to_vec();
|
|
assert_eq!(
|
|
tail,
|
|
[
|
|
vec!["kill", "boxmaker-s1-1-0"],
|
|
vec!["rm", "-f", "boxmaker-s1-1-0"],
|
|
vec!["rm", "-f", "boxmaker-s1-1-0-egress"],
|
|
]
|
|
.map(|c| c.into_iter().map(String::from).collect::<Vec<_>>())
|
|
);
|
|
assert!(!egress.join("boxmaker-s1-1-0").exists());
|
|
}
|
|
|
|
#[test]
|
|
fn a_tool_that_podman_cannot_start_still_leaves_nothing_behind() {
|
|
let _s = serial();
|
|
let fake = Fake::new(
|
|
"eg-toolfail",
|
|
&body(
|
|
r#": > "$v/egress.sock"; exit 0"#,
|
|
"cat > /dev/null; exit 125",
|
|
),
|
|
);
|
|
let log = Lines::default();
|
|
let egress = fake.dir.join("egress");
|
|
assert_eq!(
|
|
call(&Podman::new(fake.runner(""), egress.clone(), log.sink())),
|
|
failed(CANNOT_START)
|
|
);
|
|
assert_eq!(
|
|
fake.calls().last().unwrap(),
|
|
&["rm", "-f", "boxmaker-s1-1-0-egress"]
|
|
);
|
|
assert!(!egress.join("boxmaker-s1-1-0").exists());
|
|
}
|
|
|
|
#[test]
|
|
fn a_directory_left_by_a_crash_is_replaced() {
|
|
let _s = serial();
|
|
let fake = Fake::new(
|
|
"eg-stale",
|
|
&body(
|
|
r#"[ -e "$v/old.sock" ] && exit 9; : > "$v/egress.sock"; exit 0"#,
|
|
"cat > /dev/null; printf ok; exit 0",
|
|
),
|
|
);
|
|
let log = Lines::default();
|
|
let egress = fake.dir.join("egress");
|
|
std::fs::create_dir_all(egress.join("boxmaker-s1-1-0")).unwrap();
|
|
std::fs::write(egress.join("boxmaker-s1-1-0/old.sock"), "").unwrap();
|
|
let got = call(&Podman::new(fake.runner(""), egress.clone(), log.sink()));
|
|
assert!(
|
|
matches!(&got, ToolResponse::Result { content, .. } if content == "ok"),
|
|
"{got:?}"
|
|
);
|
|
}
|