Files
owui-tools/tests/vm_negative_guest.sh
T
2026-09-15 15:24:03 -04:00

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"