Files
lab/labsim/sim-net-apply.sh

60 lines
3.0 KiB
Bash
Raw Normal View History

#!/usr/bin/env bash
# Apply -- or drift-check -- the labsim routing config on all four VMs.
#
# ./sim-net-apply.sh check what the VMs run vs what sim-net-config.py says
# ./sim-net-apply.sh apply push the generated config over the serial console
#
# `check` is the one you want most of the time. The whole failure mode this
# guards against is somebody (including me) fixing something on a VM over SSH
# and never writing it down, so the next rebuild silently loses it.
#
# Applied over the serial console rather than SSH because a freshly installed
# sim router holds the same addresses as its peer -- there is a window where it
# is not safely reachable over the network at all. See console-apply.py.
set -uo pipefail
HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
ACTION="${1:-check}"
WORK="$(mktemp -d)"; trap 'rm -rf "$WORK"' EXIT
# role : vm : address : regex selecting the subtrees this generator owns
TARGETS=(
labsim: default-deny firewall policy, proven in the sim Policy: internal VLANs reach each other and the internet; the internet initiates nothing inward. That was already the effect of the IPv4 ruleset, but built as a blacklist -- default-action accept plus explicit drops per WAN interface. Identical behaviour right up until a WAN is added, at which point it is open and nothing looks wrong. This expresses it as a whitelist. Two findings from the sim, both of which would have been outages in production: `set` on a rule number is ADDITIVE. The sim already had a rule 10 carrying inbound/outbound interface constraints; `set ... rule 10 state established` ANDed onto it, producing a stateful-accept that applied to one interface pair only. Return traffic from the internet then matched no rule and hit the default drop, so LAN hosts could reach nothing outbound. The generator now deletes each filter before rebuilding it, so the code owns the subtree. It is one commit, so nftables is rebuilt atomically -- there is no window without a firewall. DHCP lease renewal is unicast UDP to port 68 and conntrack does not reliably cover it. Without an explicit rule the WAN keeps working until the lease expires and then dies -- a delayed failure that looks nothing like a firewall change. Also added a loopback accept for both families, absent from the v6 policy since it went default-deny. Verified in labsim: inter-VLAN ok, LAN-to-internet ok, internet-to-router dropped, and internet-to-LAN dropped with the drop counter incrementing by exactly the packets sent, after routing the test through the router rather than around it via the hypervisor. Also extends the drift check to the firewall subtree, which it did not cover -- so it had been reporting "in sync" while that subtree was uncaptured. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DMVzWZgiKW2wquf5z8S1yH
2026-08-22 22:25:55 +01:00
"primary:labsim-vyos:172.31.1.252:^set (protocols (bgp|failover|static)|policy (prefix-list|route-map)|nat source rule 1[12]0|interfaces (pppoe|bonding bond0 vif 5[13])|firewall (group interface-group LAN|ipv4|ipv6))"
"secondary:labsim-vyos2:172.31.1.253:^set (protocols bgp|policy (prefix-list|route-map)|firewall (group interface-group LAN|ipv4|ipv6))"
"isp-dhcp:labsim-isp-dhcp:192.168.122.136:^set (interfaces ethernet|nat source|service dhcp-server|firewall ipv4 forward|system host-name)"
"isp-pppoe:labsim-isp-pppoe:192.168.122.63:^set (interfaces ethernet|nat source|service pppoe-server|firewall ipv4 forward|system host-name)"
)
# Sim-only credential; these VMs hold nothing real and are not reachable from
# outside the hypervisor.
SSH_OPTS=(-o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null
-o LogLevel=ERROR -o PreferredAuthentications=password -o ConnectTimeout=5)
live() { timeout 30 sshpass -p vyos ssh "${SSH_OPTS[@]}" "vyos@$1" \
"/opt/vyatta/bin/vyatta-op-cmd-wrapper show configuration commands" 2>/dev/null; }
norm() { sed "s/'//g" | grep -v 'hw-id\|offload' | sort -u; }
rc=0
for t in "${TARGETS[@]}"; do
IFS=: read -r role vm addr rx <<<"$t"
"$HERE/sim-net-config.py" --role "$role" >"$WORK/$role.conf" 2>/dev/null || {
printf ' %-11s GENERATE FAILED\n' "$role"; rc=1; continue; }
if [ "$ACTION" = apply ]; then
printf ' %-11s applying to %s over console...\n' "$role" "$vm"
"$HERE/console-apply.py" --vm "$vm" --config "$WORK/$role.conf" || rc=1
continue
fi
if ! live "$addr" >"$WORK/$role.live" || [ ! -s "$WORK/$role.live" ]; then
printf ' %-11s UNREACHABLE (%s)\n' "$role" "$addr"; rc=1; continue
fi
grep -E '^set ' "$WORK/$role.conf" | norm >"$WORK/$role.g"
grep -E "$rx" "$WORK/$role.live" | norm >"$WORK/$role.l"
if d="$(diff "$WORK/$role.g" "$WORK/$role.l")" && [ -z "$d" ]; then
printf ' %-11s in sync (%s commands)\n' "$role" "$(wc -l <"$WORK/$role.g")"
else
printf ' %-11s DRIFT — "<" only in code, ">" only on the VM:\n' "$role"
printf '%s\n' "$d" | sed 's/^/ /'
rc=1
fi
done
exit $rc