Skip to main content

max / alloy

Give vmtest an inbound port the firewall is meant to refuse The firewall check needs the guest reachable from outside, and user-mode networking gives it no inbound path except a hostfwd. A port the host cannot reach at all proves nothing about a zone, so run-vm.sh now forwards :2223 to guest :5555, which nothing in 50-alloy.preset enables. PROBE_PORT moves the host side. The README says how to read the result, because the reading is not obvious: qemu accepts the host side of a forward before it knows what the guest will do, so a dropped packet looks like a timeout rather than a refusal, and a listener that was never started looks identical. Open the port, knock again, close it.
Co-Authored-By
Claude Opus 5 (1M context) <noreply@anthropic.com>
Author: Max Johnson <me@maxj.phd> · 2026-08-25 23:42 UTC
Signed with PGP, not checked
Commit: b46d0e7fff7034b336e3d53e0834b87bf5a91a3c
Parent: 395b8c5
2 files changed, +37 insertions, -1 deletion
@@ -85,6 +85,36 @@
85 85 console goes through. It needs no root, and wiki `alloy-privilege` is the table
86 86 it checks against.
87 87
88 + ## Checking the firewall
89 +
90 + The one thing a container build cannot answer, and it needs the guest to be
91 + reachable from outside. User-mode networking gives the guest no inbound path
92 + except a `hostfwd`, so a port the host simply cannot reach proves nothing:
93 + `run-vm.sh` forwards :2223 to guest :5555 for exactly this, next to ssh on
94 + :2222. Override the host side with `PROBE_PORT=` if 2223 is taken.
95 +
96 + Listen inside the guest, then knock from the host:
97 +
98 + python3 serial.py 'socat TCP-LISTEN:5555,reuseaddr,fork EXEC:/bin/echo &'
99 + python3 -c 'import socket;s=socket.socket();s.settimeout(6);s.connect(("127.0.0.1",2223));print(s.recv(64))'
100 +
101 + **A blocked port reads as a timeout, not as a refusal.** qemu accepts the host
102 + side of a forward before it knows what the guest will do, so the connect always
103 + succeeds and the drop shows up as silence. Read the data, never the connect.
104 +
105 + **Prove the silence is the firewall, or it proves nothing.** A listener that was
106 + not running looks identical. Open the port, knock again, close it:
107 +
108 + python3 serial.py 'firewall-cmd --add-port=5555/tcp' # data arrives
109 + python3 serial.py 'firewall-cmd --remove-port=5555/tcp' # silence returns
110 +
111 + On a server-profile medium the live ISO and the machine it installs differ by
112 + one service on purpose: `alloy-installer-firewall.service` adds mdns at runtime
113 + so a headless install is findable at `<name>.local`, and the installed system
114 + must not have it. `firewall-cmd --list-services` on both is the check;
115 + `--permanent --list-services` on the live medium shows the runtime add is not
116 + written anywhere.
117 +
88 118 ## What it caught
89 119
90 120 The two defects fixed in `crates/alloy/src/install.rs` on 2026-08-09: the
@@ -37,7 +37,13 @@
37 37 -drive if=pflash,format=raw,unit=0,readonly=on,file=/usr/share/OVMF/OVMF_CODE_4M.fd
38 38 -drive "if=pflash,format=raw,unit=1,file=$VARS"
39 39 -drive "file=$DISK,if=virtio,format=qcow2"
40 - -netdev user,id=n0,hostfwd=tcp:127.0.0.1:2222-:22
40 + # Two forwards, and the second one is the firewall probe. User-mode
41 + # networking gives the guest no inbound path except a hostfwd, so a port the
42 + # host cannot reach at all proves nothing about the firewall: :2223 is a real
43 + # inbound path to guest :5555, which nothing in `50-alloy.preset` enables.
44 + # Refused there while ssh answers on :2222 is what the zone claims. See
45 + # build/vmtest/README.md, "Checking the firewall".
46 + -netdev "user,id=n0,hostfwd=tcp:127.0.0.1:2222-:22,hostfwd=tcp:127.0.0.1:${PROBE_PORT:-2223}-:5555"
41 47 -device virtio-net-pci,netdev=n0
42 48 -chardev "socket,id=chrtpm,path=$SCRATCH/tpm/swtpm-sock"
43 49 -tpmdev emulator,id=tpm0,chardev=chrtpm