#!/usr/bin/env bash # tests/e2e/11_net.sh -- the promise of the project: only the allowlist is reachable. # # Needs outbound internet on the host; skips (rc 0) when there is none. source "$TESTS/lib.sh" PROJ="$WORK/proj"; mkdir -p "$PROJ"; cp "$TESTS/fixtures/egress_probe.sh" "$PROJ/" cd "$PROJ" || exit 1 if ! curl -m8 -sS -o /dev/null http://example.com 2>/dev/null; then echo " (skipped: no outbound internet on this host, so egress cannot be tested)" summary; exit 0 fi GATE="--net allow --allow example.com" # a service on the host's loopback that the sandbox must not be able to reach HOSTPORT="$(python3 -c 'import socket;s=socket.socket();s.bind(("127.0.0.1",0));print(s.getsockname()[1]);s.close()')" python3 -m http.server "$HOSTPORT" --bind 127.0.0.1 --directory "$WORK" >/dev/null 2>&1 & SRV=$! trap 'kill '"$SRV"' 2>/dev/null' EXIT sleep 1 t_name "allow mode: enforcement in one run" sb $GATE --env AISB_PROBE_HOSTSVC="$HOSTPORT" -- bash /project/egress_probe.sh assert_contains "ok_allowed_http=200" "allowlisted http works (through the gate)" assert_contains "ok_allowed_https=200" "allowlisted https works (CONNECT)" assert_contains "ok_gate_denied_domain=403" "a non-allowlisted domain is refused by the gate" assert_contains "ok_gate_denied_https=" "https CONNECT to a non-allowlisted host is refused" assert_contains "ok_direct_egress_blocked=domain" "ignoring the proxy env does not open a direct route" assert_contains "ok_direct_egress_blocked=rawip" "a literal IP is not routable either" assert_contains "ok_raw_socket_blocked" "a raw socket cannot connect out" assert_contains "ok_host_service_blocked" "only the gate's port is reachable on the mapped loopback address" assert_contains "ok_dns_blocked" "DNS is closed: names cannot be resolved behind the proxy's back" t_name "the ruleset cannot be lifted from inside" assert_contains "uid=1000" "the harness runs unprivileged" assert_contains "capeff=0000000000000000" "no capabilities remain for it" assert_contains "ok_rules_hidden" "it cannot read the ruleset" assert_contains "ok_rules_locked" "it cannot delete the ruleset" assert_contains "ok_netns_locked" "it cannot open its own network namespace" assert_contains "ok_still_enforced=403" "and enforcement still holds afterwards" t_name "an IP entry is allowed through the gate as well" sb --net allow --allow example.com --allow 1.1.1.1:80 -- bash -lc ' echo "ip_entry_code=$(curl -m10 -sS -o /dev/null -w %{http_code} http://1.1.1.1 2>&1)" echo "domain_code=$(curl -m10 -sS -o /dev/null -w %{http_code} http://example.org 2>&1)"' assert_absent "ip_entry_code=403" "an allowlisted IP is not refused by the gate" assert_contains "domain_code=403" "while an unlisted domain still is" t_name "--net none is dark" sb --net none -- bash -lc ' curl -m5 -sS -o /dev/null http://example.com 2>/dev/null && echo BAD_open || echo ok_dark ip -4 -o addr show 2>/dev/null | grep -c global | sed "s/^/addrs=/" echo "capeff=$(grep CapEff /proc/self/status | cut -f2)"' assert_contains "ok_dark" "no egress at all" assert_contains "addrs=0" "no global addresses in the namespace" assert_contains "capeff=0000000000000000" "no capabilities either" t_name "--no-gate: IPs resolved up front, everything else dropped" sb --net allow --no-gate --allow example.com -- bash -lc ' echo "allowed=$(curl -m15 -sS -o /dev/null -w %{http_code} http://example.com 2>&1)" curl -m8 -sS -o /dev/null http://example.org 2>/dev/null && echo BAD_not_blocked || echo ok_blocked echo "dns=$(: /dev/null && echo open || echo closed)"' assert_contains "allowed=200" "the resolved address of an allowlisted name works" assert_contains "ok_blocked" "anything else is dropped by netfilter" t_name "the gate does not outlive the session" BEFORE_GATES="$(podman ps -a --filter label=aisb.role=gate -q | wc -l | tr -d ' ')" sb $GATE -- true AFTER_GATES="$(podman ps -a --filter label=aisb.role=gate -q | wc -l | tr -d ' ')" assert_eq "$BEFORE_GATES" "$AFTER_GATES" "no gate container is left behind by a run" LEFT="$(ls -A "$AISB_STATE_DIR/sessions" 2>/dev/null | wc -l | tr -d ' ')" assert_eq "0" "$LEFT" "no session directory is left behind" t_name "a named gate outlives a session on purpose" AISB_GATE_NAME=aisb-test-gate "$AISB" gate up --allow example.com > "$WORK/gate.out" 2>&1 OUT="$(cat "$WORK/gate.out")" assert_rc 0 "gate up succeeds" assert_contains "port=" "and reports the port" PORT="$(printf '%s' "$OUT" | grep '^port=' | cut -d= -f2)" if [[ -n "$PORT" ]]; then code="$(curl -sS -m6 -x "http://127.0.0.1:$PORT" -o /dev/null -w '%{http_code}' http://example.com/ 2>/dev/null || echo 000)" assert_eq "200" "$code" "the named gate proxies an allowlisted host" code="$(curl -sS -m6 -x "http://127.0.0.1:$PORT" -o /dev/null -w '%{http_code}' http://example.org/ 2>/dev/null || echo 000)" assert_eq "403" "$code" "and refuses a non-allowlisted host" fi AISB_GATE_NAME=aisb-test-gate "$AISB" gate down > /dev/null 2>&1 assert_eq "0" "$(podman ps -a --filter name=aisb-test-gate -q | wc -l | tr -d ' ')" "gate down removes it" t_name "a sandbox can use a named gate instead of starting its own" AISB_GATE_NAME=aisb-test-gate2 "$AISB" gate up --allow example.com > "$WORK/gate2.out" 2>&1 PORT2="$(grep '^port=' "$WORK/gate2.out" | cut -d= -f2)" sb --net allow --proxy "100.64.0.1:${PORT2}" --allow example.com -- bash -lc ' echo "via_proxy=$(curl -m15 -sS -o /dev/null -w %{http_code} http://example.com 2>&1)"' assert_contains "via_proxy=200" "--proxy routes through the named gate" AISB_GATE_NAME=aisb-test-gate2 "$AISB" gate down > /dev/null 2>&1 t_name "a bad gate config fails closed rather than running wide open" # point the sandbox at a port where nothing answers: the run must not silently # proceed with a broken egress path. sb --net allow --proxy 100.64.0.1:1 -- bash -lc ' code=$(curl -m8 -sS -o /dev/null -w %{http_code} http://example.com 2>/dev/null); echo "code=$code" exit 0' assert_contains "code=000" "with a dead proxy, egress simply fails (no bypass)" kill "$SRV" 2>/dev/null