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.
64 lines
2.4 KiB
Bash
Executable File
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
|