#!/usr/bin/env bash # tests/spike_network.sh -- empirical verification of the network enforcement primitives. # # Run on a machine with rootless podman. Output is captured into docs/networking.md. # Nothing here is a unit test; it is evidence that the enforcement model holds on the # host it was run on. Safe: creates and removes its own podman network + containers. # # ./tests/spike_network.sh | tee /tmp/spike.txt set -uo pipefail NET="${AISB_SPIKE_NET:-aisb-spike}" IMG="${AISB_SPIKE_IMG:-docker.io/library/ubuntu:24.04}" CURL_IMG="${AISB_SPIKE_CURL_IMG:-docker.io/curlimages/curl:latest}" UID_N="$(id -u)" say() { printf '\n=== %s ===\n' "$*"; } cleanup() { podman rm -f aisb-spike-a aisb-spike-b aisb-spike-gate >/dev/null 2>&1 || true podman network rm "$NET" >/dev/null 2>&1 || true } cleanup # "Connection refused" => L3 reachable (nothing listening). "Could not connect"/timeout => not reachable. probe() { # probe -> prints REFUSED|UNREACHABLE local out out="$(timeout 25 podman run --rm --network="$1" "$CURL_IMG" -m3 -o /dev/null "http://$2:1/" 2>&1 || true)" case "$out" in *"Connection refused"*) echo "REFUSED (reachable)" ;; *) echo "UNREACHABLE :: $(printf '%s' "$out" | tr '\n' ' ' | cut -c1-90)" ;; esac } say "0. host" printf 'podman : %s\n' "$(podman --version)" printf 'rootless: %s\n' "$(podman info --format '{{.Host.Security.Rootless}}')" printf 'uid : %s subuid: %s\n' "$UID_N" "$(grep -c "^$(id -un):" /etc/subuid 2>/dev/null || echo 0)" printf 'helpers : pasta=%s slirp4netns=%s\n' "$(command -v pasta || echo no)" "$(command -v slirp4netns || echo no)" printf 'state : %s\n' "$(ls "/run/user/$UID_N/netns" 2>/dev/null | tr '\n' ' ')" printf 'bridges : %s\n' "$(ip -br -o link show type bridge 2>/dev/null | awk '{print $1}' | tr '\n' ' ')" say "1. internal network: egress + DNS must fail" podman network create --internal "$NET" >/dev/null || { echo "SKIP: cannot create network"; exit 0; } printf 'internal subnet: %s\n' "$(podman network inspect "$NET" --format '{{range .plugins}}{{if .subnet}}{{.subnet}} gw={{.gateway}}{{end}}{{end}}')" timeout 60 podman run --rm --network="$NET" "$IMG" sh -c ' cat /etc/resolv.conf | sed "s/^/ resolv: /" getent hosts example.com >/dev/null 2>&1 && echo " DNS: RESOLVED (!!! BAD)" || echo " DNS: blocked" timeout 4 sh -c "echo > /dev/tcp/93.184.216.34/80" 2>/dev/null && echo " RAW-IP EGRESS: WORKED (!!! BAD)" || echo " RAW-IP EGRESS: blocked" ' 2>&1 | sed 's/^/ /' say "2. default (podman) network: egress must work" timeout 60 podman run --rm "$CURL_IMG" -sS -m6 -o /dev/null -w ' http://example.com -> HTTP %{http_code}\n' http://example.com 2>&1 | sed 's/^/ /' say "3. container placement: where do the veths live?" timeout 60 podman run -d --name aisb-spike-a --network="$NET" "$IMG" sleep 120 >/dev/null timeout 60 podman run -d --name aisb-spike-b --network="$NET" "$IMG" sleep 120 >/dev/null IP_A=$(podman inspect -f '{{range .NetworkSettings.Networks}}{{.IPAddress}} {{end}}' aisb-spike-a | awk '{print $1}') printf ' container A internal ip: %s\n' "$IP_A" printf ' A -> B : %s\n' "$(probe "$NET" "$(podman inspect -f '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' aisb-spike-b)")" say "4. gate candidate A: --network=host can it reach the internal subnet?" timeout 60 podman run --rm --network=host "$CURL_IMG" -m3 -o /dev/null "http://$IP_A:1/" 2>&1 | sed 's/^/ host-net ctr -> internal ip: /' printf ' host-net ctr has egress : %s\n' "$(timeout 60 podman run --rm --network=host "$CURL_IMG" -sS -m6 -o /dev/null -w '%{http_code}' http://example.com 2>&1 | tail -1)" say "5. gate candidate B: multi-attach (internal + internet) egress + DNS" timeout 60 podman run --rm --network="$NET" --network=podman "$CURL_IMG" -sS -m6 -o /dev/null -w ' HTTP %{http_code}\n' http://example.com 2>&1 | sed 's/^/ /' say "6. pasta option passthrough (does podman forward args to pasta?)" timeout 60 podman run --rm --network=pasta:--map-gw "$CURL_IMG" -sS -m6 -o /dev/null -w ' pasta:--map-gw HTTP %{http_code}\n' http://example.com 2>&1 | tail -2 | sed 's/^/ /' say "7. resource caps observable from inside" timeout 60 podman run --rm --pids-limit 64 --memory 256m --cpus 0.5 "$IMG" sh -c \ 'printf " pids.max=%s mem.max=%s\n" "$(cat /sys/fs/cgroup/pids.max 2>/dev/null)" "$(cat /sys/fs/cgroup/memory.max 2>/dev/null)"; grep -c ^processor /proc/cpuinfo | sed "s/^/ cpuinfo nproc=/"' 2>&1 | sed 's/^//' say "8. userns keep-id: file ownership written to a host bind" D="$(mktemp -d)"; trap 'rm -rf "$D"' EXIT timeout 60 podman run --rm --userns=keep-id:uid=1000,gid=1000 -u 1000:1000 -v "$D:/project" -w /project "$IMG" \ sh -c 'id -u; id -g; touch /project/owned && echo ok' 2>&1 | sed 's/^/ ctr: /' printf ' on host: %s\n' "$(stat -c '%u:%g %n' "$D/owned" 2>/dev/null || echo "NOT CREATED")" cleanup