From 65c0356463bc0735636bc34f9b0a0232260c9a71 Mon Sep 17 00:00:00 2001 From: Andreas Wrede Date: Wed, 23 Sep 2026 18:27:19 -0400 Subject: [PATCH] Template NM WAN pieces: netplan split, cellular keyfile, new host variables Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01KqhjP7nud5gdBg25zCiRk9 --- ap/50-van-wan-wifi.yaml | 23 +++++++++++++++++++++++ ap/50-van-wan.yaml | 23 ----------------------- ap/cellular.nmconnection | 19 +++++++++++++++++++ deploy.sh | 29 ++++++++++++++++++++++------- failover/config.json | 2 +- hosts/van.conf | 6 +++++- hosts/vanlink.conf.example | 6 +++++- hosts/wan.conf | 4 ++++ tests/check-templates.sh | 34 ++++++++++++++++++++++++++++++---- 9 files changed, 109 insertions(+), 37 deletions(-) create mode 100644 ap/50-van-wan-wifi.yaml create mode 100644 ap/cellular.nmconnection diff --git a/ap/50-van-wan-wifi.yaml b/ap/50-van-wan-wifi.yaml new file mode 100644 index 0000000..34ce282 --- /dev/null +++ b/ap/50-van-wan-wifi.yaml @@ -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" diff --git a/ap/50-van-wan.yaml b/ap/50-van-wan.yaml index 787b76a..617b21c 100644 --- a/ap/50-van-wan.yaml +++ b/ap/50-van-wan.yaml @@ -33,26 +33,3 @@ network: networkmanager: passthrough: 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" diff --git a/ap/cellular.nmconnection b/ap/cellular.nmconnection new file mode 100644 index 0000000..37422e3 --- /dev/null +++ b/ap/cellular.nmconnection @@ -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 diff --git a/deploy.sh b/deploy.sh index 313ad66..063c477 100755 --- a/deploy.sh +++ b/deploy.sh @@ -9,6 +9,7 @@ # # POSTCHECK_WAIT=: seconds retried probes may wait in total (default 15; 0 = single attempt) # ./deploy.sh --classify-unit # prints ok|finished|wait|warn (used by post_check; for tests) # ./deploy.sh --classify-radio # reads `iw dev 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 # preview a templated file on stdout # ./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 @@ -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. 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 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_NM_WAN ) # May be empty or absent; NVME_* are required only when ENABLE_NVME_WATCH=1. OPTIONAL_VARS=( + WIFI_WAN_IFACE CELLULAR_CONN_NAME CELLULAR_APN ONBOARD_LAN_IFACE NVME_DEVICE NVME_FS_DEVICE USB_BACKUP_ROOT_PART USB_BACKUP_MOUNT SD_BACKUP_UUID ) # 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_RE='@[A-Z][A-Z0-9_]*@' @@ -53,7 +56,7 @@ load_conf() { # load_conf — source and validate a host config [ -n "${!v:-}" ] || missing+=("$v") done [ ${#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 0|1) ;; *) die "$f: $flag must be 0 or 1 (got '${!flag}')" ;; @@ -64,6 +67,7 @@ load_conf() { # load_conf — source and validate a host config || die "$f: ENABLE_NVME_WATCH=1 needs NVME_DEVICE and NVME_FS_DEVICE" fi 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 -> stdout, with @TOKEN@ placeholders substituted @@ -74,6 +78,16 @@ render() { # render -> stdout, with @TOKEN@ placeholders substituted 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 — fail on a leftover @TOKEN@ local left 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_secrets exit 0 ;; + --render-wan-netplan) load_conf "$VANLINK_CONF"; render_wan_netplan; exit 0 ;; --classify-unit) # ./deploy.sh --classify-unit classify_unit "${2:-}" "${3:-}" "${4:-}" 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 # manual `netplan apply` step. Warn loudly rather than silently drifting. 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" -elif ! diff -q <(render ap/50-van-wan.yaml) /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" - diff -u /etc/netplan/50-van-wan.yaml <(render ap/50-van-wan.yaml) || true + 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_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-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_wan_netplan) || true fi echo "== access point ==" diff --git a/failover/config.json b/failover/config.json index ad9ae11..00dd091 100644 --- a/failover/config.json +++ b/failover/config.json @@ -9,6 +9,6 @@ "wans": [ { "name": "wifi", "device": "@WIFI_WAN_IFACE@", "metric": 100 }, { "name": "starlink", "device": "@STARLINK_IFACE@", "metric": 200 }, - { "name": "cellular", "connection": "Koodo", "metric": 300 } + { "name": "cellular", "connection": "@CELLULAR_CONN_NAME@", "metric": 300 } ] } diff --git a/hosts/van.conf b/hosts/van.conf index c6b3319..a7e9bc3 100644 --- a/hosts/van.conf +++ b/hosts/van.conf @@ -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 LAN_IFACE=enP1p17s0 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= GATEWAY_IP="10.42.1.1" @@ -33,3 +33,7 @@ ENABLE_LI3=1 ENABLE_NVME_WATCH=0 # no NVMe on this board ENABLE_HA=1 ENABLE_FRIGATE=1 +ENABLE_NM_WAN=1 + +CELLULAR_CONN_NAME=Koodo +CELLULAR_APN=sp.koodo.com diff --git a/hosts/vanlink.conf.example b/hosts/vanlink.conf.example index c3576f0..20a02dc 100644 --- a/hosts/vanlink.conf.example +++ b/hosts/vanlink.conf.example @@ -12,7 +12,7 @@ WIFI_5G_IFACE=wlp1s0 # 5GHz AP radio (hostapd) WIFI_2G_IFACE=wlx000000000000 # 2.4GHz AP radio (hostapd-2g) LAN_IFACE=enp1s0 # wired LAN port, bridged into br0 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 # --- 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_HA=0 # Home Assistant + ESPHome (podman quadlets) 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 diff --git a/hosts/wan.conf b/hosts/wan.conf index 7e7efa5..295f975 100644 --- a/hosts/wan.conf +++ b/hosts/wan.conf @@ -33,3 +33,7 @@ ENABLE_LI3=0 ENABLE_NVME_WATCH=0 # boots from mmcblk0; no NVMe fitted ENABLE_HA=1 ENABLE_FRIGATE=0 +ENABLE_NM_WAN=1 + +CELLULAR_CONN_NAME=Koodo +CELLULAR_APN=sp.koodo.com diff --git a/tests/check-templates.sh b/tests/check-templates.sh index 78992f6..ae1b41f 100755 --- a/tests/check-templates.sh +++ b/tests/check-templates.sh @@ -37,7 +37,8 @@ fi # (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, # 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 /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 case "$exempt" in *" $f "*) continue ;; esac 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 van ap/van-br0-setup.service 'brctl addif br0 enP1p17s0' expect wan ap/van-br0-setup.service 'brctl addif br0 enx00e04c331140' -expect van failover/config.json '"device": "wlan0"' -expect van router-status/config.json '"wifi_wan_iface": "wlan0"' -expect van failover/van-wlan-watchdog.service 'van-wlan-watchdog wlan0' +expect van failover/config.json '"device": ""' +expect wan failover/config.json '"device": "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 wan ap/van-ap-unmanaged.conf 'interface-name:enx00e04c331140;interface-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 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"