Template NM WAN pieces: netplan split, cellular keyfile, new host variables

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KqhjP7nud5gdBg25zCiRk9
This commit is contained in:
2026-09-23 18:27:19 -04:00
co-authored by Claude Sonnet 5
parent 640d6d245c
commit 65c0356463
9 changed files with 109 additions and 37 deletions
+23
View File
@@ -0,0 +1,23 @@
wifis:
@WIFI_WAN_IFACE@:
renderer: NetworkManager
optional: true
dhcp4: true
# IPv6 is deliberate as of 2026-07-12: NM does SLAAC/DHCPv6 itself.
# Before this it only worked by accident — dracut's runtime catch-all
# /run/systemd/network/zzzz-dracut-default.network had networkd
# co-managing wlan0 (second DHCPv4 client + surprise IPv6); deploy.sh
# now masks that file. Privacy (temporary) addresses stay off so hbd
# and DNS see one stable source address per uplink.
dhcp6: true
ipv6-privacy: false
# never-default: see the Starlink stanza above — same reasoning, applies to every
# van-failover-managed WAN.
networkmanager:
passthrough:
ipv4.never-default: "true"
access-points:
"Wapana":
auth:
key-management: "psk"
password: "6e1335fd97165a7d2618bec19824be363a2766d7765f91aa14773d871eaa59dc"
-23
View File
@@ -33,26 +33,3 @@ network:
networkmanager: networkmanager:
passthrough: passthrough:
ipv4.never-default: "true" ipv4.never-default: "true"
wifis:
@WIFI_WAN_IFACE@:
renderer: NetworkManager
optional: true
dhcp4: true
# IPv6 is deliberate as of 2026-07-12: NM does SLAAC/DHCPv6 itself.
# Before this it only worked by accident — dracut's runtime catch-all
# /run/systemd/network/zzzz-dracut-default.network had networkd
# co-managing wlan0 (second DHCPv4 client + surprise IPv6); deploy.sh
# now masks that file. Privacy (temporary) addresses stay off so hbd
# and DNS see one stable source address per uplink.
dhcp6: true
ipv6-privacy: false
# never-default: see the Starlink stanza above — same reasoning, applies to every
# van-failover-managed WAN.
networkmanager:
passthrough:
ipv4.never-default: "true"
access-points:
"Wapana":
auth:
key-management: "psk"
password: "6e1335fd97165a7d2618bec19824be363a2766d7765f91aa14773d871eaa59dc"
+19
View File
@@ -0,0 +1,19 @@
# NetworkManager connection for the cellular WAN (see failover/config.json, van-failover).
# Created by deploy.sh only when absent; never rewritten. No modem needs to be present.
# never-default: van-failover is the sole owner of the default route (same as the Wi-Fi/Starlink profiles).
[connection]
id=@CELLULAR_CONN_NAME@
uuid=@CELLULAR_UUID@
type=gsm
autoconnect=true
[gsm]
apn=@CELLULAR_APN@
[ipv4]
method=auto
route-metric=300
never-default=true
[ipv6]
method=auto
+22 -7
View File
@@ -9,6 +9,7 @@
# # POSTCHECK_WAIT=<s>: seconds retried probes may wait in total (default 15; 0 = single attempt) # # POSTCHECK_WAIT=<s>: seconds retried probes may wait in total (default 15; 0 = single attempt)
# ./deploy.sh --classify-unit <ActiveState> <Result> <ExecMainStatus> # prints ok|finished|wait|warn (used by post_check; for tests) # ./deploy.sh --classify-unit <ActiveState> <Result> <ExecMainStatus> # prints ok|finished|wait|warn (used by post_check; for tests)
# ./deploy.sh --classify-radio # reads `iw dev <if> info` text on stdin, prints beaconing|silent (no ssid line = silent; used by post_check; for tests) # ./deploy.sh --classify-radio # reads `iw dev <if> info` text on stdin, prints beaconing|silent (no ssid line = silent; used by post_check; for tests)
# ./deploy.sh --render-wan-netplan # rendered /etc/netplan/50-van-wan.yaml (ethernet part + wifis stanza when WIFI_WAN_IFACE is set)
# ./deploy.sh render <file> # preview a templated file on stdout # ./deploy.sh render <file> # preview a templated file on stdout
# ./deploy.sh --redact # filter stdin, masking secret values (used by --diff; for tests) # ./deploy.sh --redact # filter stdin, masking secret values (used by --diff; for tests)
# Idempotent. Netplan (wlan0 + Starlink NIC = NM-managed WANs; eth0 is a LAN # Idempotent. Netplan (wlan0 + Starlink NIC = NM-managed WANs; eth0 is a LAN
@@ -25,18 +26,20 @@ VANLINK_CONF=${VANLINK_CONF:-/etc/vanlink.conf}
# Must be defined (non-empty) in the host file; deploy aborts naming any missing. # Must be defined (non-empty) in the host file; deploy aborts naming any missing.
REQUIRED_VARS=( REQUIRED_VARS=(
WIFI_5G_IFACE WIFI_2G_IFACE LAN_IFACE STARLINK_IFACE WIFI_WAN_IFACE WIFI_5G_IFACE WIFI_2G_IFACE LAN_IFACE STARLINK_IFACE
GATEWAY_IP GATEWAY_PREFIX GATEWAY_NETWORK DHCP_RANGE_START DHCP_RANGE_END GATEWAY_IP GATEWAY_PREFIX GATEWAY_NETWORK DHCP_RANGE_START DHCP_RANGE_END
DNS_RESOLVERS MODEM_USB_VENDOR AP_SSID_5G AP_SSID_2G AP_5G_RADIO DNS_RESOLVERS MODEM_USB_VENDOR AP_SSID_5G AP_SSID_2G AP_5G_RADIO
ENABLE_COCKPIT ENABLE_LI3 ENABLE_NVME_WATCH ENABLE_HA ENABLE_FRIGATE ENABLE_COCKPIT ENABLE_LI3 ENABLE_NVME_WATCH ENABLE_HA ENABLE_FRIGATE
ENABLE_NM_WAN
) )
# May be empty or absent; NVME_* are required only when ENABLE_NVME_WATCH=1. # May be empty or absent; NVME_* are required only when ENABLE_NVME_WATCH=1.
OPTIONAL_VARS=( OPTIONAL_VARS=(
WIFI_WAN_IFACE CELLULAR_CONN_NAME CELLULAR_APN
ONBOARD_LAN_IFACE NVME_DEVICE NVME_FS_DEVICE ONBOARD_LAN_IFACE NVME_DEVICE NVME_FS_DEVICE
USB_BACKUP_ROOT_PART USB_BACKUP_MOUNT SD_BACKUP_UUID USB_BACKUP_ROOT_PART USB_BACKUP_MOUNT SD_BACKUP_UUID
) )
# Computed in load_conf; usable as @TOKEN@ like the rest. # Computed in load_conf; usable as @TOKEN@ like the rest.
DERIVED_VARS=(ONBOARD_LAN_UNMANAGED) DERIVED_VARS=(ONBOARD_LAN_UNMANAGED CELLULAR_UUID)
TOKEN_VARS=("${REQUIRED_VARS[@]}" "${OPTIONAL_VARS[@]}" "${DERIVED_VARS[@]}") TOKEN_VARS=("${REQUIRED_VARS[@]}" "${OPTIONAL_VARS[@]}" "${DERIVED_VARS[@]}")
TOKEN_RE='@[A-Z][A-Z0-9_]*@' TOKEN_RE='@[A-Z][A-Z0-9_]*@'
@@ -53,7 +56,7 @@ load_conf() { # load_conf <file> — source and validate a host config
[ -n "${!v:-}" ] || missing+=("$v") [ -n "${!v:-}" ] || missing+=("$v")
done done
[ ${#missing[@]} -eq 0 ] || die "$f is missing required variable(s): ${missing[*]}" [ ${#missing[@]} -eq 0 ] || die "$f is missing required variable(s): ${missing[*]}"
for flag in ENABLE_COCKPIT ENABLE_LI3 ENABLE_NVME_WATCH ENABLE_HA ENABLE_FRIGATE; do for flag in ENABLE_COCKPIT ENABLE_LI3 ENABLE_NVME_WATCH ENABLE_HA ENABLE_FRIGATE ENABLE_NM_WAN; do
case ${!flag} in case ${!flag} in
0|1) ;; 0|1) ;;
*) die "$f: $flag must be 0 or 1 (got '${!flag}')" ;; *) die "$f: $flag must be 0 or 1 (got '${!flag}')" ;;
@@ -64,6 +67,7 @@ load_conf() { # load_conf <file> — source and validate a host config
|| die "$f: ENABLE_NVME_WATCH=1 needs NVME_DEVICE and NVME_FS_DEVICE" || die "$f: ENABLE_NVME_WATCH=1 needs NVME_DEVICE and NVME_FS_DEVICE"
fi fi
ONBOARD_LAN_UNMANAGED=${ONBOARD_LAN_IFACE:+;interface-name:$ONBOARD_LAN_IFACE} ONBOARD_LAN_UNMANAGED=${ONBOARD_LAN_IFACE:+;interface-name:$ONBOARD_LAN_IFACE}
CELLULAR_UUID=$(uuidgen -s -n @dns -N "vanlink-${CELLULAR_CONN_NAME:-}" 2>/dev/null || true)
} }
render() { # render <file> -> stdout, with @TOKEN@ placeholders substituted render() { # render <file> -> stdout, with @TOKEN@ placeholders substituted
@@ -74,6 +78,16 @@ render() { # render <file> -> stdout, with @TOKEN@ placeholders substituted
sed "${args[@]}" "$1" sed "${args[@]}" "$1"
} }
# The netplan is two templates: ap/50-van-wan.yaml (ethernet/Starlink) and
# ap/50-van-wan-wifi.yaml (the wifis: stanza, appended verbatim only for hosts
# with a Wi-Fi uplink, so a host without one gets no empty/invalid wifis: block).
render_wan_netplan() { # ap/50-van-wan.yaml, plus the wifis stanza when the host has a Wi-Fi uplink
render ap/50-van-wan.yaml
if [ -n "${WIFI_WAN_IFACE:-}" ]; then
render ap/50-van-wan-wifi.yaml
fi
}
check_rendered() { # check_rendered <src> <rendered-file> — fail on a leftover @TOKEN@ check_rendered() { # check_rendered <src> <rendered-file> — fail on a leftover @TOKEN@
local left local left
left=$(grep -oE "$TOKEN_RE" "$2" | sort -u | tr '\n' ' ') || true left=$(grep -oE "$TOKEN_RE" "$2" | sort -u | tr '\n' ' ') || true
@@ -277,6 +291,7 @@ case "${1:-}" in
--redact) # ./deploy.sh --redact — stdin through redact_secrets (no config, no root) --redact) # ./deploy.sh --redact — stdin through redact_secrets (no config, no root)
redact_secrets redact_secrets
exit 0 ;; exit 0 ;;
--render-wan-netplan) load_conf "$VANLINK_CONF"; render_wan_netplan; exit 0 ;;
--classify-unit) # ./deploy.sh --classify-unit <ActiveState> <Result> <ExecMainStatus> --classify-unit) # ./deploy.sh --classify-unit <ActiveState> <Result> <ExecMainStatus>
classify_unit "${2:-}" "${3:-}" "${4:-}" classify_unit "${2:-}" "${3:-}" "${4:-}"
exit 0 ;; exit 0 ;;
@@ -365,10 +380,10 @@ echo "== netplan drift check =="
# see note below), so it's easy to edit ap/50-van-wan.yaml and forget the # see note below), so it's easy to edit ap/50-van-wan.yaml and forget the
# manual `netplan apply` step. Warn loudly rather than silently drifting. # manual `netplan apply` step. Warn loudly rather than silently drifting.
if [ ! -f /etc/netplan/50-van-wan.yaml ]; then if [ ! -f /etc/netplan/50-van-wan.yaml ]; then
warn "netplan: /etc/netplan/50-van-wan.yaml is missing — repo config was never deployed. Run: sudo ./deploy.sh render ap/50-van-wan.yaml | sudo tee /etc/netplan/50-van-wan.yaml && sudo netplan generate && sudo netplan apply" warn "netplan: /etc/netplan/50-van-wan.yaml is missing — repo config was never deployed. Run: sudo ./deploy.sh --render-wan-netplan | sudo tee /etc/netplan/50-van-wan.yaml && sudo netplan generate && sudo netplan apply"
elif ! diff -q <(render ap/50-van-wan.yaml) /etc/netplan/50-van-wan.yaml >/dev/null 2>&1; then elif ! diff -q <(render_wan_netplan) /etc/netplan/50-van-wan.yaml >/dev/null 2>&1; then
warn "netplan: /etc/netplan/50-van-wan.yaml differs from ap/50-van-wan.yaml (rendered). Deploy manually: sudo ./deploy.sh render ap/50-van-wan.yaml | sudo tee /etc/netplan/50-van-wan.yaml && sudo netplan generate && sudo netplan apply" warn "netplan: /etc/netplan/50-van-wan.yaml differs from ap/50-van-wan.yaml (rendered). Deploy manually: sudo ./deploy.sh --render-wan-netplan | sudo tee /etc/netplan/50-van-wan.yaml && sudo netplan generate && sudo netplan apply"
diff -u /etc/netplan/50-van-wan.yaml <(render ap/50-van-wan.yaml) || true diff -u /etc/netplan/50-van-wan.yaml <(render_wan_netplan) || true
fi fi
echo "== access point ==" echo "== access point =="
+1 -1
View File
@@ -9,6 +9,6 @@
"wans": [ "wans": [
{ "name": "wifi", "device": "@WIFI_WAN_IFACE@", "metric": 100 }, { "name": "wifi", "device": "@WIFI_WAN_IFACE@", "metric": 100 },
{ "name": "starlink", "device": "@STARLINK_IFACE@", "metric": 200 }, { "name": "starlink", "device": "@STARLINK_IFACE@", "metric": 200 },
{ "name": "cellular", "connection": "Koodo", "metric": 300 } { "name": "cellular", "connection": "@CELLULAR_CONN_NAME@", "metric": 300 }
] ]
} }
+5 -1
View File
@@ -5,7 +5,7 @@ WIFI_5G_IFACE=wlp1s0 # MediaTek MT7925E (PCIe); 2.4GHz AP is the sepa
WIFI_2G_IFACE=wlx3c3332002066 # D-Link USB dongle WIFI_2G_IFACE=wlx3c3332002066 # D-Link USB dongle
LAN_IFACE=enP1p17s0 LAN_IFACE=enP1p17s0
STARLINK_IFACE=enP2p33s0 STARLINK_IFACE=enP2p33s0
WIFI_WAN_IFACE=wlan0 # not present on this host (kept for the failover config) WIFI_WAN_IFACE= # none: no spare radio (the R5C's two radios are both APs)
ONBOARD_LAN_IFACE= ONBOARD_LAN_IFACE=
GATEWAY_IP="10.42.1.1" GATEWAY_IP="10.42.1.1"
@@ -33,3 +33,7 @@ ENABLE_LI3=1
ENABLE_NVME_WATCH=0 # no NVMe on this board ENABLE_NVME_WATCH=0 # no NVMe on this board
ENABLE_HA=1 ENABLE_HA=1
ENABLE_FRIGATE=1 ENABLE_FRIGATE=1
ENABLE_NM_WAN=1
CELLULAR_CONN_NAME=Koodo
CELLULAR_APN=sp.koodo.com
+5 -1
View File
@@ -12,7 +12,7 @@ WIFI_5G_IFACE=wlp1s0 # 5GHz AP radio (hostapd)
WIFI_2G_IFACE=wlx000000000000 # 2.4GHz AP radio (hostapd-2g) WIFI_2G_IFACE=wlx000000000000 # 2.4GHz AP radio (hostapd-2g)
LAN_IFACE=enp1s0 # wired LAN port, bridged into br0 LAN_IFACE=enp1s0 # wired LAN port, bridged into br0
STARLINK_IFACE=enp2s0 # Starlink uplink (NetworkManager WAN) STARLINK_IFACE=enp2s0 # Starlink uplink (NetworkManager WAN)
WIFI_WAN_IFACE=wlan0 # Wi-Fi uplink (NetworkManager WAN) WIFI_WAN_IFACE=wlan0 # optional: Wi-Fi uplink (NetworkManager WAN); empty = none
ONBOARD_LAN_IFACE= # optional: extra onboard LAN port bridged into br0 (e.g. eth0); empty = none ONBOARD_LAN_IFACE= # optional: extra onboard LAN port bridged into br0 (e.g. eth0); empty = none
# --- Access point ------------------------------------------------------------- # --- Access point -------------------------------------------------------------
@@ -47,3 +47,7 @@ ENABLE_LI3=0 # Li3 house-battery BLE -> MQTT service
ENABLE_NVME_WATCH=0 # NVMe timeout watchdog (needs an NVMe drive) ENABLE_NVME_WATCH=0 # NVMe timeout watchdog (needs an NVMe drive)
ENABLE_HA=0 # Home Assistant + ESPHome (podman quadlets) ENABLE_HA=0 # Home Assistant + ESPHome (podman quadlets)
ENABLE_FRIGATE=0 # Frigate NVR (podman quadlet) ENABLE_FRIGATE=0 # Frigate NVR (podman quadlet)
ENABLE_NM_WAN=1 # NetworkManager owns the WANs (netplan, cellular profile) via --apply-network
CELLULAR_CONN_NAME=Koodo # optional: NM name of the cellular connection (also used by van-failover)
CELLULAR_APN=sp.koodo.com # optional: cellular APN
+4
View File
@@ -33,3 +33,7 @@ ENABLE_LI3=0
ENABLE_NVME_WATCH=0 # boots from mmcblk0; no NVMe fitted ENABLE_NVME_WATCH=0 # boots from mmcblk0; no NVMe fitted
ENABLE_HA=1 ENABLE_HA=1
ENABLE_FRIGATE=0 ENABLE_FRIGATE=0
ENABLE_NM_WAN=1
CELLULAR_CONN_NAME=Koodo
CELLULAR_APN=sp.koodo.com
+30 -4
View File
@@ -37,7 +37,8 @@ fi
# (plain `install` would ship the placeholder unrendered — the 2026-09-23 failure) # (plain `install` would ship the placeholder unrendered — the 2026-09-23 failure)
# vanrouter.js: installed by the loop in the cockpit block; 50-van-wan.yaml: reference copy, # vanrouter.js: installed by the loop in the cockpit block; 50-van-wan.yaml: reference copy,
# rendered by hand via `deploy.sh render` (deploy.sh only warns when it differs) # rendered by hand via `deploy.sh render` (deploy.sh only warns when it differs)
exempt=" cockpit/vanrouter/vanrouter.js ap/50-van-wan.yaml " # 50-van-wan-wifi.yaml, cellular.nmconnection: installed by dedicated code, not `install_rendered <src> /abs/dst` literal lines
exempt=" cockpit/vanrouter/vanrouter.js ap/50-van-wan.yaml ap/50-van-wan-wifi.yaml ap/cellular.nmconnection "
for f in $(./deploy.sh --templates); do for f in $(./deploy.sh --templates); do
case "$exempt" in *" $f "*) continue ;; esac case "$exempt" in *" $f "*) continue ;; esac
if grep -qE "^[[:space:]]*install_rendered +$f[[:space:]]" deploy.sh; then if grep -qE "^[[:space:]]*install_rendered +$f[[:space:]]" deploy.sh; then
@@ -62,9 +63,11 @@ expect van ap/nftables.conf 'ip saddr 10.42.1.0/24'
expect wan ap/nftables.conf 'ip saddr 10.42.0.0/24' expect wan ap/nftables.conf 'ip saddr 10.42.0.0/24'
expect van ap/van-br0-setup.service 'brctl addif br0 enP1p17s0' expect van ap/van-br0-setup.service 'brctl addif br0 enP1p17s0'
expect wan ap/van-br0-setup.service 'brctl addif br0 enx00e04c331140' expect wan ap/van-br0-setup.service 'brctl addif br0 enx00e04c331140'
expect van failover/config.json '"device": "wlan0"' expect van failover/config.json '"device": ""'
expect van router-status/config.json '"wifi_wan_iface": "wlan0"' expect wan failover/config.json '"device": "wlan0"'
expect van failover/van-wlan-watchdog.service 'van-wlan-watchdog wlan0' expect van router-status/config.json '"wifi_wan_iface": ""'
expect van failover/van-wlan-watchdog.service 'ExecStart=/usr/local/sbin/van-wlan-watchdog'
expect wan failover/van-wlan-watchdog.service 'van-wlan-watchdog wlan0'
expect van ap/van-ap-unmanaged.conf 'interface-name:enP1p17s0' expect van ap/van-ap-unmanaged.conf 'interface-name:enP1p17s0'
expect wan ap/van-ap-unmanaged.conf 'interface-name:enx00e04c331140;interface-name:eth0' expect wan ap/van-ap-unmanaged.conf 'interface-name:enx00e04c331140;interface-name:eth0'
expect wan ap/23-van-lan-eth0.network 'Name=eth0' expect wan ap/23-van-lan-eth0.network 'Name=eth0'
@@ -202,4 +205,27 @@ for row in "beaconing:$r_ok" "silent:$r_bad" "silent:"; do
if [ "$got" = "$want" ]; then ok "--classify-radio -> $want"; else bad "--classify-radio gave '$got', want '$want'"; fi if [ "$got" = "$want" ]; then ok "--classify-radio -> $want"; else bad "--classify-radio gave '$got', want '$want'"; fi
done done
# 14. the netplan split leaves the Pi 5's rendered WAN netplan byte-identical, and the R5C (no Wi-Fi WAN) omits wifis
old=$(mktemp)
git show 223652b:ap/50-van-wan.yaml > "$old"
want=$(VANLINK_CONF=hosts/wan.conf ./deploy.sh render "$old")
tmpw=$(mktemp)
# render_wan_netplan is internal; expose it through --render-wan-netplan (documented in the header)
VANLINK_CONF=hosts/wan.conf ./deploy.sh --render-wan-netplan > "$tmpw"
if [ "$want" = "$(cat "$tmpw")" ]; then ok "wan.conf: split netplan renders identical to the old single template"; else bad "wan.conf: netplan render changed by the split"; diff <(echo "$want") "$tmpw" | head -10 | sed 's/^/ /'; fi
if VANLINK_CONF=hosts/van.conf ./deploy.sh --render-wan-netplan | grep -q '^ wifis:'; then bad "van.conf: wifis stanza must be omitted (WIFI_WAN_IFACE empty)"; else ok "van.conf: no wifis stanza"; fi
rm -f "$tmpw" "$old"
# 15. cellular connection name comes from the host config
expect van failover/config.json '"connection": "Koodo"'
expect wan failover/config.json '"connection": "Koodo"'
# 16. cellular keyfile template renders a valid NM gsm profile with a stable uuid
for h in van wan; do
k=$(VANLINK_CONF=hosts/$h.conf ./deploy.sh render ap/cellular.nmconnection)
for pat in '^id=Koodo$' '^type=gsm$' '^apn=sp.koodo.com$' '^route-metric=300$' '^never-default=true$' '^uuid=[0-9a-f-]{36}$'; do
if echo "$k" | grep -qE "$pat"; then ok "$h: cellular keyfile has $pat"; else bad "$h: cellular keyfile lacks $pat"; fi
done
done
exit "$fail" exit "$fail"