Files
ced4568 eae8e48d43 feat(routing): Lab Host Router closes the Gateway-to-Lab cross-VM gap
Adds platform/lab-router/ — a narrow service that runs on the Lab Host,
authenticates the Gateway via mTLS, and resolves an opaque routing token to
a local container purely from Docker label state (no Postgres access, no
callback to the Provisioner). Replaces the Gateway's env.internal_ip
assumption (only valid when Gateway and Lab share a Docker daemon) with a
data-driven direct/router decision per lab_host
(lab_hosts.routing_mode/router_endpoint, migration 004), resolved in one
place (platform/gateway/src/lab-endpoint.js's labTarget()) that every
proxy/fetch call site in server.js now goes through.

Extends the existing Docker TLS CA (infra/scripts/docker-tls/generate-docker-tls.sh)
with a second mTLS leaf pair (router-server/gateway-client) for this new
data-plane connection, deployed by the docker-tls Ansible role into a
separate directory from the control-plane pair. Firewall
(rn-lab-host-01.rules) replaces the old speculative wide port-range
allowance with one narrow rule: platform_ip -> the Router's port only.

dev-local stays routing_mode: direct — zero behavior change for local dev,
confirmed by the full existing regression suite passing unmodified.
2026-08-24 20:14:39 -05:00

64 lines
2.4 KiB
Bash
Executable File

#!/usr/bin/env bash
# Local test (Phase 4 §29): renders each .rules file with dummy IPs and validates it.
#
# `iptables-restore --test` needs root (it talks to netfilter) — if this shell has passwordless
# sudo, we use it for a real semantic check. Otherwise we fall back to a structural lint
# (balanced *table/COMMIT blocks, every rule starts with a real flag, no unrendered {{ }}
# placeholders left over) — good enough to catch a typo'd rule or a missing COMMIT, NOT a
# substitute for the real `iptables-restore --test` a human/Ansible with actual root runs
# before this ever reaches a live host. HARDWARE-DEPENDENT: proving the real IPs/CIDRs are
# correct needs the actual production network, not this script.
set -euo pipefail
cd "$(dirname "$0")"
TMPDIR="$(mktemp -d)"
trap 'rm -rf "$TMPDIR"' EXIT
render() {
sed \
-e 's/{{ management_cidr }}/203.0.113.0\/24/g' \
-e 's/{{ platform_ip }}/10.10.10.10/g' \
-e 's/{{ lab_host_ip }}/10.10.10.20/g' \
-e 's/{{ lab_subnet_range }}/10.42.0.0\/16/g' \
-e 's/{{ proxmox_mgmt_cidr }}/203.0.113.0\/24/g' \
-e "s/{{ lab_router_port | default('8443') }}/8443/g" \
"$1" > "$2"
}
structural_lint() {
local f="$1"
grep -q '^\*filter' "$f" || { echo " missing *filter table declaration"; return 1; }
grep -q '^COMMIT' "$f" || { echo " missing COMMIT"; return 1; }
grep -v '^#' "$f" | grep -q '{{' && { echo " unrendered {{ }} placeholder left in a non-comment line"; return 1; }
# every -A/-I rule line should end in a real target (ACCEPT/DROP/REJECT/LOG/a custom chain)
while IFS= read -r line; do
[[ "$line" =~ ^-[AI]\ ]] || continue
[[ "$line" =~ -j\ [A-Za-z0-9_-]+ ]] || { echo " rule missing a -j target: $line"; return 1; }
done < "$f"
return 0
}
HAVE_SUDO=0
sudo -n true 2>/dev/null && HAVE_SUDO=1
FAIL=0
for f in proxmox-host.rules rn-platform-01.rules rn-lab-host-01.rules; do
render "$f" "$TMPDIR/$f"
echo "==> $f"
if [ "$HAVE_SUDO" = "1" ] && command -v iptables-restore >/dev/null 2>&1; then
if sudo -n iptables-restore --test < "$TMPDIR/$f"; then
echo "OK: $f passed a real iptables-restore --test"
else
echo "FAIL: $f did not pass iptables-restore --test" >&2; FAIL=1
fi
else
if structural_lint "$TMPDIR/$f"; then
echo "OK: $f passes structural lint (no root available for a real iptables-restore --test — see script header)"
else
echo "FAIL: $f failed structural lint" >&2; FAIL=1
fi
fi
done
exit $FAIL