dixie: fail over USB Wi-Fi to internal CNVi

NM dispatcher plus a boot oneshot keep the AX9L (mt7921u) primary and
bring up the on-box iwlwifi profile if the dongle is gone. Connection
PSKs stay on-box; this only selects the radio.
This commit is contained in:
2026-09-23 14:03:30 -07:00
parent 687fac019f
commit c839f7c8ae
+116 -4
View File
@@ -128,6 +128,105 @@ let
TimeoutStartSec = "600"; TimeoutStartSec = "600";
}; };
}; };
# USB AX9L (mt7921u) is primary; internal CNVi (iwlwifi/wlo1) is fallback.
# Connection profiles stay on-box (PSK); this only selects which radio is up.
usbWifiMac = "74:19:f8:17:5e:53";
usbWifiConn = "ATTjkThnDS";
internalWifiConn = "ATTjkThnDS-iwlwifi";
internalWifiIf = "wlo1";
wifiUsbFailover = pkgs.writeShellScript "wifi-usb-failover" ''
set -eu
nmcli=${pkgs.networkmanager}/bin/nmcli
logger=${pkgs.util-linux}/bin/logger
systemd_run=${pkgs.systemd}/bin/systemd-run
tr=${pkgs.coreutils}/bin/tr
USB_MAC="${usbWifiMac}"
USB_CONN="${usbWifiConn}"
FALLBACK_CONN="${internalWifiConn}"
INTERNAL_IF="${internalWifiIf}"
log() { "$logger" -t wifi-usb-failover -- "$*"; }
iface_mac() {
"$tr" 'A-F' 'a-f' < "/sys/class/net/$1/address" 2>/dev/null || true
}
usb_iface() {
for n in /sys/class/net/*; do
n=''${n##*/}
[ "$n" = "lo" ] && continue
[ "$(iface_mac "$n")" = "$USB_MAC" ] && { echo "$n"; return 0; }
done
return 1
}
is_usb_iface() {
[ "$(iface_mac "$1")" = "$USB_MAC" ]
}
demote_internal() {
"$nmcli" device set "$INTERNAL_IF" autoconnect no >/dev/null 2>&1 || true
"$nmcli" connection down "$FALLBACK_CONN" >/dev/null 2>&1 || true
"$nmcli" device disconnect "$INTERNAL_IF" >/dev/null 2>&1 || true
}
promote_internal() {
if usb_iface >/dev/null 2>&1; then
log "USB still present; not failing over"
return 0
fi
log "USB absent; bringing up $FALLBACK_CONN on $INTERNAL_IF"
"$nmcli" device set "$INTERNAL_IF" autoconnect yes >/dev/null 2>&1 || true
"$nmcli" connection up "$FALLBACK_CONN" ifname "$INTERNAL_IF" >/dev/null 2>&1 || true
}
settle() {
i=0
while [ "$i" -lt 20 ]; do
if usb_iface >/dev/null 2>&1; then
u=$(usb_iface)
log "settle: USB $u present; demoting internal"
demote_internal
if ${pkgs.iproute2}/bin/ip -4 -o addr show dev "$u" | ${pkgs.gnugrep}/bin/grep -q ' inet '; then
return 0
fi
"$nmcli" connection up "$USB_CONN" ifname "$u" >/dev/null 2>&1 || true
return 0
fi
i=$((i + 1))
sleep 1
done
log "settle: no USB after 20s; failing over"
promote_internal
}
IFACE=''${1-}
ACTION=''${2-}
case "$IFACE" in
boot|settle) settle; exit 0 ;;
after-usb-down) sleep 3; promote_internal; exit 0 ;;
esac
case "$ACTION" in
up)
if is_usb_iface "$IFACE"; then
log "USB $IFACE up; demoting $INTERNAL_IF"
demote_internal
fi
;;
down)
if is_usb_iface "$IFACE"; then
log "USB $IFACE down; scheduling failover check"
"$systemd_run" --quiet --collect --unit=wifi-usb-failover-down \
--property=Type=oneshot "$0" after-usb-down >/dev/null 2>&1 || true
fi
;;
esac
exit 0
'';
in in
{ {
imports = [ imports = [
@@ -203,11 +302,24 @@ in
upstream = "http://127.0.0.1:11434"; upstream = "http://127.0.0.1:11434";
}; };
# Wi-Fi only host. NetworkManager comes from the base config; the AX9L (mt7921u) becomes # Wi-Fi only host. NetworkManager comes from the base config; the AX9L (mt7921u) is
# the primary once plugged in and the internal CNVi radio stays as fallback. Connection # primary and the internal CNVi radio is fallback. Connection profiles are copied
# profiles are copied from the live-USB session at install time (see the install notes), # from the live-USB session at install time (see the install notes), not stored here.
# not stored in this repo.
networking.networkmanager.wifi.backend = "wpa_supplicant"; networking.networkmanager.wifi.backend = "wpa_supplicant";
networking.networkmanager.dispatcherScripts = [
{ source = wifiUsbFailover; type = "basic"; }
];
systemd.services.wifi-usb-failover-boot = {
description = "Prefer USB Wi-Fi; fail over to internal CNVi if the dongle is absent";
after = [ "NetworkManager.service" ];
wants = [ "NetworkManager.service" ];
wantedBy = [ "multi-user.target" ];
serviceConfig = {
Type = "oneshot";
RemainAfterExit = true;
ExecStart = "${wifiUsbFailover} boot";
};
};
# Reachable only over the tailnet: 11434 (router), 11433 (vision), 11435 (tether) # Reachable only over the tailnet: 11434 (router), 11433 (vision), 11435 (tether)
# on tailscale0, and SSH from the LAN (base config opens 22) for bring-up. # on tailscale0, and SSH from the LAN (base config opens 22) for bring-up.