#!/usr/bin/env bash # All firewall mutations run only after unshare creates BOTH a network and mount # namespace. Never installs/runs the agent against the runner's host firewall. set -Eeuo pipefail [[ $(uname -s) == Linux ]] || { echo 'Linux namespace runner required' >&2; exit 1; } for command in unshare ip nft ipset iptables ip6tables python3 curl mount flock timeout; do command -v "$command" >/dev/null; done temporary=$(mktemp -d) trap 'rm -rf -- "$temporary"' EXIT cp apps/api/src/agent-scripts/evofw-firewall.sh "$temporary/agent.sh" cp scripts/ci/linux-agent-fixture.py "$temporary/server.py" parent_net=$(readlink /proc/self/ns/net) parent_mount=$(readlink /proc/self/ns/mnt) for backend in nft ipset; do rm -f "$temporary/port" "$temporary/port6" "$temporary/invalid" unshare --net --mount --pid --fork --mount-proc env EVOFW_TEST_PARENT_NET="$parent_net" EVOFW_TEST_PARENT_MOUNT="$parent_mount" \ bash -s -- "$temporary" "$backend" <<'CHILD' set -Eeuo pipefail root=$1 backend=$2 unset http_proxy HTTP_PROXY https_proxy HTTPS_PROXY ALL_PROXY all_proxy export NO_PROXY='*' [[ $(readlink /proc/self/ns/net) != "$EVOFW_TEST_PARENT_NET" ]] [[ $(readlink /proc/self/ns/mnt) != "$EVOFW_TEST_PARENT_MOUNT" ]] mount --make-rprivate / # Agent hardcoded config/state/log paths are private mounts, never host writes. for directory in /etc /var/lib /var/log /run; do mount -t tmpfs tmpfs "$directory"; done if [[ -n "${EVOFW_TEST_ETC_DATA:-}" ]]; then [[ "$EVOFW_TEST_ETC_DATA" == /work/helper-etc ]] cp "$EVOFW_TEST_ETC_DATA/protocols" "$EVOFW_TEST_ETC_DATA/services" /etc/ fi mkdir -p /etc/evofw /var/lib/evofw ip link set lo up ip netns add client ip link add server0 type veth peer name client0 ip link set client0 netns client ip addr add 198.18.0.1/24 dev server0 ip -6 addr add 2001:db8:1::1/64 dev server0 nodad ip link set server0 up ip -n client addr add 198.18.0.2/24 dev client0 ip -n client -6 addr add 2001:db8:1::2/64 dev client0 nodad ip -n client link set client0 up ip -n client link set lo up python3 "$root/server.py" "$root" & server_pid=$! trap 'kill "$server_pid" 2>/dev/null || true; ip netns del client 2>/dev/null || true' EXIT for attempt in {1..100}; do [[ ! -f "$root/port" ]] || break; sleep .05; done port=$(cat "$root/port") port6=$(cat "$root/port6") cat > /etc/evofw/agent.conf </dev/null ip netns exec client curl --noproxy '*' --max-time 2 -gfsS "http://[2001:db8:1::1]:$port6/echo" >/dev/null bash "$root/agent.sh" python3 -c 'import json; assert json.load(open("/var/lib/evofw/telemetry-state.json"))["pending"] == [], "fixture ACK did not drain durable telemetry"' [[ $(cat /var/lib/evofw/last_hash) == "sha256:$(printf '1%.0s' {1..64})" ]] nft list table inet unrelated >/dev/null if ip netns exec client curl --noproxy '*' --max-time 2 -fsS "http://198.18.0.1:$port/echo" >/dev/null; then echo 'deny did not block packet' >&2; exit 1; fi if ip netns exec client curl --noproxy '*' --max-time 2 -gfsS "http://[2001:db8:1::1]:$port6/echo" >/dev/null; then echo 'IPv6 deny did not block packet' >&2; exit 1; fi # Same hash must reconcile missing owned state, retaining unrelated rules. if [[ "$backend" == nft ]]; then nft delete table inet evofw; else ipset flush evofw_deny_v4; fi bash "$root/agent.sh" if [[ "$backend" == nft ]]; then nft list table inet evofw >/dev/null; else ipset test evofw_deny_v4 198.18.0.2; fi nft list table inet unrelated >/dev/null touch "$root/invalid" if bash "$root/agent.sh"; then echo 'invalid candidate unexpectedly applied' >&2; exit 1; fi [[ $(cat /var/lib/evofw/last_hash) == "sha256:$(printf '1%.0s' {1..64})" ]] if [[ "$backend" == nft ]]; then nft list set inet evofw deny_v4 | grep -q '198.18.0.2' nft list set inet evofw deny_v6 | grep -q '2001:db8:1::2' else ipset test evofw_deny_v4 198.18.0.2 ipset test evofw_deny_v6 2001:db8:1::2 fi nft list table inet unrelated >/dev/null echo "Linux namespace smoke passed ($backend): IPv4+IPv6 apply, real packet deny, drift repair, invalid-candidate last-good, unrelated table preservation" CHILD done