#!/bin/bash # ===================================================================== # Structural assertions about the VM sandbox's host side. # # THE POINT OF THIS FILE # # The guest cannot reach this host or the LAN, and that claim rests on # two controls which are deliberately independent of each other: # # 1. IPAddressDeny= in owuivm.service - a BPF filter on the unit's # cgroup. Survives `nft flush ruleset`, a ufw reload and an # nftables package upgrade. # 2. the `inet owuivm_guard` nft table - the host's usual convention, # and the thing an operator will actually think to look at. # # Checking that both are loaded proves nothing useful, because in normal # operation either one alone would hide a broken other. So this script # turns each OFF in turn and re-runs the same probe. Both runs must pass. # # This is not theoretical. The first version of owuivm.service carried # `IPAddressAllow=0.0.0.0/0` alongside the deny list, on the assumption # that longest-prefix would let the denies win. It does not - the allow # list is checked first and grants unconditionally - so layer 1 was # doing nothing at all, and every check passed anyway because layer 2 # was covering for it. This script is what found that. # # sudo ./tests/vm_host_asserts.sh # ===================================================================== set -uo pipefail UNIT=/etc/systemd/system/owuivm.service PROBE=/usr/local/libexec/owuivm-probe.py DENY='127.0.0.0/8 10.0.0.0/8 172.16.0.0/12 192.168.0.0/16 169.254.0.0/16 100.64.0.0/10 192.0.0.0/24 224.0.0.0/4 240.0.0.0/4' # Must be unreachable from the sandbox uid. These are this host's own LAN # address, the LAN gateway, Ollama on the docker bridge, a host service on # loopback, and the owuitools gateway. MUST_FAIL="192.168.1.10:7200 192.168.1.1:80 172.17.0.1:11434 127.0.0.1:3000 172.22.0.10:8000" # Must be reachable, or the suite proves only that networking is broken. MUST_PASS="1.1.1.1:443 8.8.8.8:53" fails=0 ok() { printf ' ok %s\n' "$*"; } bad() { printf ' FAIL %s\n' "$*"; fails=$((fails+1)); } [ "$(id -u)" -eq 0 ] || { echo "run as root" >&2; exit 1; } # However this script exits, the guard goes back - and so does any base # image this suite unfroze. An image left writable is not dangerous (vmd # refuses to open a session on one) but it stops the agent working, and a # test suite must not be able to do that to the machine it ran on. restore() { systemctl restart owuivm-nft-guard.service >/dev/null 2>&1 for img in /var/lib/owuivm/base/*.qcow2; do [ -e "$img" ] || continue systemctl is-active --quiet "owuivm-maint@$(basename "$img" .qcow2).service" && continue chmod 0444 "$img" 2>/dev/null done } trap restore EXIT INT TERM install -o root -g root -m 0755 "$(dirname "$(readlink -f "$0")")/vm_probe.py" "$PROBE" probe() { # probe ; prints "target verdict" systemd-run --quiet --wait --pipe --collect --uid=owuivm "$@" \ /usr/bin/python3 "$PROBE" $MUST_FAIL $MUST_PASS 2>/dev/null } check_probe() { # check_probe