use microsandbox::sandbox::HttpDenyResponseFormat;
use microsandbox::{NetworkPolicy, Sandbox};
use microsandbox_network::policy::Rule;
use test_utils::msb_test;
const CURL_IMAGE: &str = "mirror.gcr.io/curlimages/curl";
const DENIED_HOST: &str = "example.com";
fn dns_only_policy() -> NetworkPolicy {
let mut policy = NetworkPolicy::none();
policy.rules.push(Rule::allow_dns());
policy
}
async fn spawn(
name: &str,
tls: bool,
enabled: bool,
message: Option<&str>,
format: HttpDenyResponseFormat,
) -> Sandbox {
let message = message.map(str::to_owned);
Sandbox::builder(name)
.image(CURL_IMAGE)
.cpus(1)
.memory(256)
.user("0")
.replace()
.network(move |mut n| {
n = n.policy(dns_only_policy());
if !enabled {
n = n.http(|h| h.deny_response(false));
}
if format == HttpDenyResponseFormat::Text {
n = n.http(|h| h.deny_response_format(format));
}
if tls {
n = n.tls(|t| t.enabled(true));
}
if let Some(message) = message {
n = n.http(|h| match format {
HttpDenyResponseFormat::Text => h.deny_message(message),
HttpDenyResponseFormat::Json => h.network_deny_message(message),
});
}
n
})
.create()
.await
.expect("create sandbox")
}
async fn teardown(sb: Sandbox, name: &str) {
sb.stop().await.expect("stop");
drop(sb);
let _ = Sandbox::remove(name).await;
}
async fn probe(sb: &Sandbox, url: &str) -> (String, String) {
let cmd = format!(
"curl -k -sS --http1.1 -m 30 -o /tmp/body -w '%{{http_code}}' {url} 2>/tmp/err; \
echo; cat /tmp/body; echo; echo '--stderr--'; cat /tmp/err"
);
let out = sb.shell(&cmd).await.expect("curl");
let stdout = out.stdout().unwrap_or_default();
let mut lines = stdout.lines();
let code = lines.next().unwrap_or_default().trim().to_string();
let body = lines
.take_while(|line| *line != "--stderr--")
.collect::<Vec<_>>()
.join("\n");
(code, body)
}
#[msb_test]
async fn denied_requests_fail_with_explicit_opt_out() {
let name = "http-deny-default";
let sb = spawn(
name,
true,
false,
Some("must not enable responses"),
HttpDenyResponseFormat::Json,
)
.await;
for scheme in ["http", "https"] {
let (code, body) = probe(&sb, &format!("{scheme}://{DENIED_HOST}/")).await;
assert_eq!(
code, "000",
"disabled response returned HTTP: {code} ({body})"
);
assert!(
!body.contains("must not enable responses"),
"disabled response returned the custom body: {body}"
);
}
teardown(sb, name).await;
}
#[msb_test]
async fn denied_plain_http_gets_403_with_json_error() {
let name = "http-deny-plain";
let sb = spawn(name, false, true, None, HttpDenyResponseFormat::Json).await;
let (code, body) = probe(&sb, &format!("http://{DENIED_HOST}/")).await;
assert_eq!(
code, "403",
"expected 403 from the gateway, got {code} ({body})"
);
let error: serde_json::Value = serde_json::from_str(&body).unwrap();
assert_eq!(error["code"], "network_policy_denied");
assert_eq!(error["domain"], DENIED_HOST);
assert_eq!(error["message"], "Request blocked by network policy.");
teardown(sb, name).await;
}
#[msb_test]
async fn denied_https_gets_403_inside_intercepted_tls() {
let name = "http-deny-tls";
let sb = spawn(name, true, true, None, HttpDenyResponseFormat::Json).await;
let (code, body) = probe(&sb, &format!("https://{DENIED_HOST}/")).await;
assert_eq!(
code, "403",
"expected 403 from the gateway, got {code} ({body})"
);
let error: serde_json::Value = serde_json::from_str(&body).unwrap();
assert_eq!(error["code"], "network_policy_denied");
assert_eq!(error["domain"], DENIED_HOST);
assert_eq!(error["message"], "Request blocked by network policy.");
teardown(sb, name).await;
}
#[msb_test]
async fn custom_http_deny_message_is_literal() {
let name = "http-deny-custom";
let sb = spawn(
name,
true,
true,
Some("blocked {host}: call the AllowHost tool to request access"),
HttpDenyResponseFormat::Json,
)
.await;
let (code, body) = probe(&sb, &format!("https://{DENIED_HOST}/")).await;
assert_eq!(code, "403", "expected 403, got {code} ({body})");
let error: serde_json::Value = serde_json::from_str(&body).unwrap();
assert_eq!(error["code"], "network_policy_denied");
assert_eq!(error["domain"], DENIED_HOST);
assert_eq!(
error["message"],
"blocked {host}: call the AllowHost tool to request access"
);
teardown(sb, name).await;
}
#[msb_test]
async fn denied_https_without_interception_still_fails_closed() {
let name = "http-deny-tls-off";
let sb = spawn(name, false, true, None, HttpDenyResponseFormat::Json).await;
let (code, body) = probe(&sb, &format!("https://{DENIED_HOST}/")).await;
assert_ne!(code, "403", "no HTTP answer expected without interception");
assert!(
code == "000" || code.is_empty(),
"curl must fail to connect, got {code} ({body})"
);
teardown(sb, name).await;
}
#[msb_test]
async fn legacy_text_denials_preserve_custom_messages() {
let name = "http-deny-legacy";
let sb = spawn(
name,
true,
true,
Some("blocked {host}"),
HttpDenyResponseFormat::Text,
)
.await;
for scheme in ["http", "https"] {
let (code, body) = probe(&sb, &format!("{scheme}://{DENIED_HOST}/")).await;
assert_eq!(code, "403");
assert_eq!(body.trim_end(), "blocked example.com");
}
teardown(sb, name).await;
}