82 lines
3.9 KiB
Bash
Executable File
82 lines
3.9 KiB
Bash
Executable File
#!/bin/bash
|
|
# =====================================================================
|
|
# Runs INSIDE the sandbox VM. Proves the guest cannot reach this host or
|
|
# the LAN, and - just as important - that it CAN reach the internet.
|
|
#
|
|
# WHY THE POSITIVE CONTROLS ARE NOT OPTIONAL
|
|
#
|
|
# A suite made only of "this must fail" passes perfectly on a machine
|
|
# with no network at all, on a machine that is switched off, and on a
|
|
# machine where the test harness is broken. Every must-fail target here
|
|
# is paired with a must-succeed one, so a pass means "the filter works"
|
|
# rather than "nothing works".
|
|
#
|
|
# ping IS EXPECTED TO FAIL, AND THAT IS NOT THE FILTER
|
|
#
|
|
# net.ipv4.ping_group_range on the host is "1 0", so no group may open an
|
|
# unprivileged ICMP socket and libslirp cannot forward one. ping fails
|
|
# here permanently, on a machine whose HTTPS works perfectly. It is
|
|
# asserted as a known-fail rather than left to be rediscovered as a
|
|
# mystery by everyone who ever debugs this.
|
|
# =====================================================================
|
|
fails=0
|
|
pass() { printf ' ok %s\n' "$*"; }
|
|
fail() { printf ' FAIL %s\n' "$*"; fails=$((fails+1)); }
|
|
|
|
# A TCP connect with a short timeout, using bash's own /dev/tcp so the
|
|
# result does not depend on nc, curl or python being installed.
|
|
reach() {
|
|
timeout 4 bash -c "exec 3<>/dev/tcp/$1/$2" 2>/dev/null
|
|
}
|
|
|
|
echo "== must be UNREACHABLE: the host, the LAN, and the stack =="
|
|
# 10.0.2.2 and 10.0.2.3 are SLIRP's aliases for the host's loopback and
|
|
# the host's DNS forwarder. They are the closest thing to a back door
|
|
# this design has, and the deny on 127.0.0.0/8 is what closes them.
|
|
for target in \
|
|
"10.0.2.2:22|SLIRP alias for the host's loopback (sshd)" \
|
|
"10.0.2.2:3000|SLIRP alias for the host's loopback (Open WebUI)" \
|
|
"192.168.1.10:22|this host on the LAN (sshd)" \
|
|
"192.168.1.10:3000|this host on the LAN (Open WebUI)" \
|
|
"192.168.1.10:7200|this host on the LAN (Gitea)" \
|
|
"192.168.1.10:9443|this host on the LAN (Portainer)" \
|
|
"192.168.1.1:80|the LAN gateway" \
|
|
"172.17.0.1:11434|Ollama on the docker bridge" \
|
|
"172.22.0.10:8000|the owuitools gateway" \
|
|
"169.254.169.254:80|cloud metadata" ; do
|
|
addr="${target%%|*}"; what="${target#*|}"
|
|
host="${addr%:*}"; port="${addr##*:}"
|
|
if reach "$host" "$port"; then fail "REACHED $addr - $what"; else pass "blocked $addr ($what)"; fi
|
|
done
|
|
|
|
echo
|
|
echo "== must be REACHABLE: the internet (or this suite proves nothing) =="
|
|
for target in "1.1.1.1:443|Cloudflare DNS over TLS" "8.8.8.8:53|Google DNS" "1.1.1.1:80|a plain HTTP port"; do
|
|
addr="${target%%|*}"; what="${target#*|}"
|
|
host="${addr%:*}"; port="${addr##*:}"
|
|
if reach "$host" "$port"; then pass "reachable $addr ($what)"; else fail "UNREACHABLE $addr - $what: the positive control failed, so the negatives above mean nothing"; fi
|
|
done
|
|
|
|
echo
|
|
echo "== name resolution and a real fetch =="
|
|
if getent hosts archive.ubuntu.com >/dev/null 2>&1; then pass "DNS resolves (static 1.1.1.1, not SLIRP's forwarder)"; else fail "DNS does not resolve"; fi
|
|
if timeout 20 curl -fsS -o /dev/null https://example.com/ 2>/dev/null; then pass "HTTPS to a public host works"; else fail "HTTPS to a public host failed"; fi
|
|
|
|
echo
|
|
echo "== known-fail, documented: ICMP =="
|
|
if timeout 5 ping -c1 -W2 8.8.8.8 >/dev/null 2>&1; then
|
|
fail "ping WORKED - ping_group_range must have changed; update the docs and check_internet's note"
|
|
else
|
|
pass "ping fails as expected (host ping_group_range is '1 0'; use curl instead)"
|
|
fi
|
|
|
|
echo
|
|
echo "== no IPv6, and no neighbours on the LAN =="
|
|
if ip -6 addr show scope global 2>/dev/null | grep -q inet6; then fail "the guest has a global IPv6 address"; else pass "no global IPv6"; fi
|
|
neighbours=$(ip neigh show 2>/dev/null | grep -cvE '^\s*$|10\.0\.2\.' || true)
|
|
if [ "${neighbours:-0}" -gt 0 ]; then fail "the guest can see $neighbours neighbour(s) outside its own SLIRP subnet"; else pass "no neighbours outside 10.0.2.0/24"; fi
|
|
|
|
echo
|
|
if [ "$fails" -gt 0 ]; then echo "FAILED $fails"; exit 1; fi
|
|
echo "the guest is isolated as designed"
|