#!/usr/bin/env bash # tests/fixtures/egress_probe.sh -- runs *inside* the sandbox and reports, one # marker per line, what is and is not reachable. Kept as a file so the outer test # does not have to nest five layers of quoting. # # Markers start with ok_/BAD_ so the caller can grep for either. set -uo pipefail ALLOW="${AISB_PROBE_ALLOW:-example.com}" OTHER="${AISB_PROBE_OTHER:-example.org}" RAWIP="${AISB_PROBE_RAWIP:-1.1.1.1}" GATE="${AISB_PROBE_GATE:-100.64.0.1}" HOSTSVC="${AISB_PROBE_HOSTSVC:-18099}" # 1. allowlisted destination, through the proxy code="$(curl -m15 -sS -o /dev/null -w '%{http_code}' "http://$ALLOW" 2>/dev/null)" [[ "$code" == 200 ]] && echo "ok_allowed_http=$code" || echo "BAD_allowed_http=$code" # 2. allowlisted destination over https (CONNECT) code="$(curl -m15 -sS -o /dev/null -w '%{http_code}' "https://$ALLOW" 2>/dev/null)" [[ "$code" == 200 ]] && echo "ok_allowed_https=$code" || echo "BAD_allowed_https=$code" # 3. non-allowlisted domain: the gate must answer 403 code="$(curl -m10 -sS -o /dev/null -w '%{http_code}' "http://$OTHER" 2>/dev/null)" [[ "$code" == 403 ]] && echo "ok_gate_denied_domain=$code" || echo "BAD_gate_denied_domain=$code" # a denied CONNECT is refused by the proxy: curl reports 000/502, never 200 code="$(curl -m10 -sS -o /dev/null -w '%{http_code}' "https://$OTHER" 2>/dev/null)" [[ "$code" != 200 ]] && echo "ok_gate_denied_https=$code" || echo "BAD_gate_denied_https=$code" # 4. same, but pretending the caller ignores the proxy env: the netfilter ruleset # is what stops it. Anything other than a connection failure is a leak. nop () { env -u http_proxy -u https_proxy -u all_proxy -u HTTP_PROXY -u HTTPS_PROXY -u ALL_PROXY "$@"; } if nop curl -m8 -sS -o /dev/null "http://$OTHER" 2>/dev/null; then echo "BAD_direct_egress=domain" else echo "ok_direct_egress_blocked=domain" fi if nop curl -m8 -sS -o /dev/null "http://$RAWIP" 2>/dev/null; then echo "BAD_direct_egress=rawip" else echo "ok_direct_egress_blocked=rawip" fi # 5. a raw socket (no proxy semantics at all) must not connect either python3 - "$RAWIP" "$GATE" <<'PY' import socket, sys rawip, gate = sys.argv[1], sys.argv[2] for label, host, port in (("rawip", rawip, 80), ("cdnish", "93.184.216.34", 80), ("dns", "8.8.8.8", 53)): s = socket.socket(); s.settimeout(6) try: s.connect((host, port)); print(f"BAD_raw_socket={label}") except Exception as e: print(f"ok_raw_socket_blocked={label}:{type(e).__name__}") finally: s.close() PY # 6. an unrelated service on the host's loopback must be unreachable through the # mapped address: only the gate's own port is routable there. Tested without # the proxy, because a proxied request dies at the squid and would exit 0 on a # 403 -- which proves the proxy works, not that the route is closed. if nop curl -m6 -sS -o /dev/null "http://$GATE:$HOSTSVC/" 2>/dev/null; then echo "BAD_host_service_reachable=$HOSTSVC" else echo "ok_host_service_blocked=$HOSTSVC" fi # 7. DNS: gate mode keeps it shut, so nothing can resolve names behind the proxy if getent hosts "$OTHER" > /dev/null 2>&1; then echo "BAD_dns_resolved=$OTHER" else echo "ok_dns_blocked=$OTHER" fi # 8. who are we, and can we touch the ruleset? echo "uid=$(id -u)" echo "capeff=$(grep CapEff /proc/self/status | cut -f2)" if nft list table inet aisb > /dev/null 2>&1; then echo "BAD_rules_readable"; else echo "ok_rules_hidden"; fi if nft delete table inet aisb > /dev/null 2>&1; then echo "BAD_rules_deleted"; else echo "ok_rules_locked"; fi if unshare -n true > /dev/null 2>&1; then echo "BAD_new_netns"; else echo "ok_netns_locked"; fi # 9. and after all that, enforcement is still in place code="$(curl -m10 -sS -o /dev/null -w '%{http_code}' "http://$OTHER" 2>/dev/null)" [[ "$code" == 403 ]] && echo "ok_still_enforced=$code" || echo "BAD_still_enforced=$code"