//! `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> = 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::>()) ); 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:?}" ); }