ap: parallel 2.4GHz AP (hostapd-2g) on the Linksys WUSB6300v2 dongle

Second radio (wlxd8ec5e2faa8c, RTL8822BU/rtw88), same VanLink SSID+PSK,
ch6 HT20, bridged into br0 next to the 5GHz AP. Runs as its own hostapd
instance + own watchdog so a USB wedge on one radio never darkens the
other; van-ap-watchdog now takes conf path + unit name as args (defaults
unchanged). rtw88.conf disables deep power-save (rtw89 gotcha analog)
and the driver's USB2->3 self-upgrade (USB3 noise in the 2.4GHz band —
note: dongle currently sits in a USB3 port, so it still enumerates
SuperSpeed; move to a USB2 port if 2.4GHz range disappoints).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Andreas Wrede
2026-07-06 10:03:16 -04:00
co-authored by Claude Fable 5
parent a7819edbc2
commit e652b0f7bd
9 changed files with 106 additions and 17 deletions
+10 -5
View File
@@ -49,7 +49,7 @@ This directory is the source of truth. The live system files live under `/etc`,
| Web UI | **Cockpit** + `vanrouter` plugin | | Web UI | **Cockpit** + `vanrouter` plugin |
### Key network facts ### Key network facts
- LAN: `10.42.0.0/24`, gateway `10.42.0.1`, DHCP `.10.254` — shared by Wi-Fi (`VanLink`) and the wired port `enx00e04c331140`, both bridged into `br0`. - LAN: `10.42.0.0/24`, gateway `10.42.0.1`, DHCP `.10.254` — shared by Wi-Fi (`VanLink`, dual-band: 5GHz `wlxc83a35a4ee55` + 2.4GHz `wlxd8ec5e2faa8c`) and the wired port `enx00e04c331140`, all bridged into `br0`.
- WAN priority (metrics): **wifi 100 → eth/Starlink 200 → 4G 300** (lower = preferred). - WAN priority (metrics): **wifi 100 → eth/Starlink 200 → 4G 300** (lower = preferred).
- Management / recovery: ethernet `192.168.10.251`, wifi `192.168.10.27`, ZeroTier `192.168.196.22`. - Management / recovery: ethernet `192.168.10.251`, wifi `192.168.10.27`, ZeroTier `192.168.196.22`.
- Cockpit: `https://192.168.10.251:9090` (or `.27`, or ZeroTier). Log in with a Unix account; enable *Administrative access* for action buttons. - Cockpit: `https://192.168.10.251:9090` (or `.27`, or ZeroTier). Log in with a Unix account; enable *Administrative access* for action buttons.
@@ -73,7 +73,11 @@ This directory is the source of truth. The live system files live under `/etc`,
| `default-hostapd` | `/etc/default/hostapd` | `DAEMON_CONF=...` | | `default-hostapd` | `/etc/default/hostapd` | `DAEMON_CONF=...` |
| `van-ap-dnsmasq.conf` | `/etc/van-ap/dnsmasq.conf` | DHCP/DNS bound to AP iface (`bind-dynamic`, so it does not clash with systemd-resolved) | | `van-ap-dnsmasq.conf` | `/etc/van-ap/dnsmasq.conf` | DHCP/DNS bound to AP iface (`bind-dynamic`, so it does not clash with systemd-resolved) |
| `van-ap-dnsmasq.service` | `/etc/systemd/system/van-ap-dnsmasq.service` | dedicated dnsmasq unit (uses the `dnsmasq-base` binary; the distro dnsmasq service is NOT used) | | `van-ap-dnsmasq.service` | `/etc/systemd/system/van-ap-dnsmasq.service` | dedicated dnsmasq unit (uses the `dnsmasq-base` binary; the distro dnsmasq service is NOT used) |
| `hostapd-2g.conf` | `/etc/hostapd/hostapd-2g.conf` | 2.4GHz AP (Linksys WUSB6300v2 / RTL8822BU): same SSID+PSK, ch6, HT20. Own hostapd instance so one radio wedging never kills the other. |
| `hostapd-2g.service` | `/etc/systemd/system/hostapd-2g.service` | dedicated unit for the 2.4GHz hostapd (same Restart=always / no-start-limit semantics as the 5GHz drop-in) |
| `van-ap-watchdog-2g.service` | `/etc/systemd/system/van-ap-watchdog-2g.service` | second watchdog instance: `van-ap-watchdog /etc/hostapd/hostapd-2g.conf hostapd-2g` |
| `10-van-ap.network` | `/etc/systemd/network/10-van-ap.network` | brings the AP wlan up with **no** IP (it's a `br0` member; hostapd enslaves it) | | `10-van-ap.network` | `/etc/systemd/network/10-van-ap.network` | brings the AP wlan up with **no** IP (it's a `br0` member; hostapd enslaves it) |
| `11-van-ap-2g.network` | `/etc/systemd/network/11-van-ap-2g.network` | same, for the 2.4GHz wlan |
| `20-van-br0.netdev` | `/etc/systemd/network/20-van-br0.netdev` | creates the LAN bridge `br0` (STP off) | | `20-van-br0.netdev` | `/etc/systemd/network/20-van-br0.netdev` | creates the LAN bridge `br0` (STP off) |
| `21-van-br0.network` | `/etc/systemd/network/21-van-br0.network` | static `10.42.0.1/24` on `br0` (`ConfigureWithoutCarrier`) | | `21-van-br0.network` | `/etc/systemd/network/21-van-br0.network` | static `10.42.0.1/24` on `br0` (`ConfigureWithoutCarrier`) |
| `22-van-lan.network` | `/etc/systemd/network/22-van-lan.network` | enslaves the wired LAN port `enx00e04c331140` to `br0` | | `22-van-lan.network` | `/etc/systemd/network/22-van-lan.network` | enslaves the wired LAN port `enx00e04c331140` to `br0` |
@@ -82,6 +86,7 @@ This directory is the source of truth. The live system files live under `/etc`,
| `99-van-router.conf` | `/etc/sysctl.d/99-van-router.conf` | `net.ipv4.ip_forward=1` | | `99-van-router.conf` | `/etc/sysctl.d/99-van-router.conf` | `net.ipv4.ip_forward=1` |
| `regdomain.service` | `/etc/systemd/system/regdomain.service` | `iw reg set CA` at boot, before NetworkManager | | `regdomain.service` | `/etc/systemd/system/regdomain.service` | `iw reg set CA` at boot, before NetworkManager |
| `rtw89.conf` | `/etc/modprobe.d/rtw89.conf` | `options rtw89_core disable_ps_mode=Y` (else AP drops beacon when idle) | | `rtw89.conf` | `/etc/modprobe.d/rtw89.conf` | `options rtw89_core disable_ps_mode=Y` (else AP drops beacon when idle) |
| `rtw88.conf` | `/etc/modprobe.d/rtw88.conf` | 2.4GHz dongle: `disable_lps_deep=Y` (same PS reasoning) + `switch_usb_mode=N` (don't self-upgrade to USB3 — it radiates into 2.4GHz) |
### `failover/` — multi-WAN ### `failover/` — multi-WAN
| file | → installs to | purpose | | file | → installs to | purpose |
@@ -141,12 +146,12 @@ Not touched by `deploy.sh` — the VM's LAN address/name come from `ap/van-ap-dn
## 3. Components & how to operate ## 3. Components & how to operate
### Access Point ### Access Point
- Change SSID / channel / password: edit `ap/hostapd.conf`, then `sudo cp` it to `/etc/hostapd/hostapd.conf` (or `./deploy.sh`) and `sudo systemctl restart hostapd`. - **Dual band, one SSID**: 5GHz (`hostapd.conf` → unit `hostapd`, ch149 80MHz VHT+HE) and 2.4GHz (`hostapd-2g.conf` → unit `hostapd-2g`, ch6 HT20, for range + 2.4-only IoT). Same SSID/PSK — clients pick their band. Deliberately two hostapd processes: a USB wedge on one radio never takes the other down, and each has its own watchdog (`van-ap-watchdog`, `van-ap-watchdog-2g`).
- Width is 80MHz VHT + HE (WiFi-6). Channel 149 (UNII-3, non-DFS). - Change SSID / channel / password: edit `ap/hostapd.conf` and/or `ap/hostapd-2g.conf`, then `sudo cp` to `/etc/hostapd/` (or `./deploy.sh`) and restart the matching unit.
- DHCP range / DNS options: `ap/van-ap-dnsmasq.conf` + restart `van-ap-dnsmasq`. - DHCP range / DNS options: `ap/van-ap-dnsmasq.conf` + restart `van-ap-dnsmasq`.
- Services: `hostapd`, `van-ap-dnsmasq`, `systemd-networkd`, `nftables`, `regdomain`. - Services: `hostapd`, `hostapd-2g`, `van-ap-dnsmasq`, `systemd-networkd`, `nftables`, `regdomain`.
- **Do not** add `noscan` to hostapd.conf — Ubuntu's hostapd rejects it and fails to start. - **Do not** add `noscan` to hostapd.conf — Ubuntu's hostapd rejects it and fails to start.
- Verify it's actually beaconing with `iw dev wlxc83a35a4ee55 info` (ssid+channel+width present), not just `systemctl is-active hostapd`. - Verify each is actually beaconing with `iw dev wlxc83a35a4ee55 info` / `iw dev wlxd8ec5e2faa8c info` (ssid+channel+width present), not just `systemctl is-active`.
### Multi-WAN failover (`van-failover`) ### Multi-WAN failover (`van-failover`)
- Probes each WAN's real internet every few seconds (HTTP-204 check, captive-portal-aware), demotes a failed WAN by raising its route-metric, fails back on recovery (hysteresis). - Probes each WAN's real internet every few seconds (HTTP-204 check, captive-portal-aware), demotes a failed WAN by raising its route-metric, fails back on recovery (hysteresis).
+11
View File
@@ -0,0 +1,11 @@
[Match]
Name=wlxd8ec5e2faa8c
[Link]
RequiredForOnline=no
# No IP here: like the 5GHz AP wlan (10-van-ap.network), this is a br0 member —
# hostapd-2g adds it to the bridge after putting the radio into AP mode. networkd
# must NOT enslave it; this file only brings the link up.
[Network]
ConfigureWithoutCarrier=yes
+25
View File
@@ -0,0 +1,25 @@
interface=wlxd8ec5e2faa8c
# Second, independent hostapd instance (hostapd-2g.service) — 2.4GHz band of the same
# VanLink network, bridged into the same br0 segment as the 5GHz AP and the wired port.
# Separate process on purpose: a USB wedge on one radio must never take down the other.
bridge=br0
# Same ctrl dir as the 5GHz AP — sockets are per-interface, hostapd_cli/watchdog use -i.
ctrl_interface=/var/run/hostapd
driver=nl80211
# Same SSID + PSK as 5GHz: dual-band clients pick a band themselves, 2.4-only IoT
# devices just join. Rename here (e.g. VanLink-2G) if band pinning is ever needed.
ssid=VanLink
country_code=CA
ieee80211d=1
hw_mode=g
channel=6
wmm_enabled=1
# HT20 only — 40MHz in 2.4GHz overlaps most of the band and coexistence would force
# it back to 20MHz near any neighbor anyway.
ieee80211n=1
ht_capab=[LDPC][SHORT-GI-20]
auth_algs=1
wpa=2
wpa_passphrase=1foot11foot11
wpa_key_mgmt=WPA-PSK
rsn_pairwise=CCMP
+15
View File
@@ -0,0 +1,15 @@
[Unit]
Description=VanLink 2.4GHz AP (hostapd on wlxd8ec5e2faa8c, RTL8822BU)
# Same USB re-enumeration hazards as the 5GHz AP (see hostapd-restart.conf for the
# full story): never stop retrying, and back off a few seconds so the USB device can
# re-enumerate before the next attempt.
StartLimitIntervalSec=0
After=network.target
[Service]
ExecStart=/usr/sbin/hostapd /etc/hostapd/hostapd-2g.conf
Restart=always
RestartSec=5
[Install]
WantedBy=multi-user.target
+10
View File
@@ -0,0 +1,10 @@
# 2.4GHz AP dongle (Linksys WUSB6300 v2, RTL8822BU, rtw88_8822bu).
# Deep power-save has no business on an always-on AP radio — same reasoning as the
# rtw89 disable_ps_mode gotcha on the 5GHz dongle (rtw89.conf).
options rtw88_core disable_lps_deep=Y
# Don't let the driver switch a USB2-enumerated dongle up to USB3: USB3 signalling
# radiates broadband noise right across the 2.4GHz band, and HT20 rates fit in USB2
# easily. NOTE: this only blocks the driver's own USB2->3 switch — a dongle sitting in
# a USB3 port still enumerates at SuperSpeed (it currently does); move it to a USB2
# port if 2.4GHz range/interference ever looks poor.
options rtw88_usb switch_usb_mode=N
+3 -3
View File
@@ -1,4 +1,4 @@
[keyfile] [keyfile]
# The AP wlan and the wired LAN port are both owned by systemd-networkd/hostapd # The AP wlans (5GHz + 2.4GHz) and the wired LAN port are all owned by
# (bridged into br0), so NetworkManager must not touch either of them. # systemd-networkd/hostapd (bridged into br0), so NetworkManager must not touch them.
unmanaged-devices=interface-name:wlxc83a35a4ee55;interface-name:enx00e04c331140 unmanaged-devices=interface-name:wlxc83a35a4ee55;interface-name:wlxd8ec5e2faa8c;interface-name:enx00e04c331140
+10 -5
View File
@@ -18,6 +18,10 @@ exited, so Restart=always never fired. The link check catches exactly that. If t
unhealthy for `fail_threshold` checks in a row, it clears any failed state and restarts unhealthy for `fail_threshold` checks in a row, it clears any failed state and restarts
hostapd. If the interface is simply gone (mid re-enumeration) it waits — there is hostapd. If the interface is simply gone (mid re-enumeration) it waits — there is
nothing to restart onto, and Restart=always reclaims it when it reappears. Stdlib only. nothing to restart onto, and Restart=always reclaims it when it reappears. Stdlib only.
Usage: van-ap-watchdog [hostapd.conf path] [systemd unit]
Defaults watch the 5GHz AP (/etc/hostapd/hostapd.conf, unit hostapd); the 2.4GHz
instance (van-ap-watchdog-2g.service) passes hostapd-2g.conf + hostapd-2g.
""" """
import re import re
@@ -26,7 +30,8 @@ import sys
import time import time
from pathlib import Path from pathlib import Path
HOSTAPD_CONF = "/etc/hostapd/hostapd.conf" HOSTAPD_CONF = sys.argv[1] if len(sys.argv) > 1 else "/etc/hostapd/hostapd.conf"
HOSTAPD_UNIT = sys.argv[2] if len(sys.argv) > 2 else "hostapd"
CTRL_DIR = "/var/run/hostapd" CTRL_DIR = "/var/run/hostapd"
INTERVAL = 15 # seconds between health checks (backstop cadence, not first response) INTERVAL = 15 # seconds between health checks (backstop cadence, not first response)
FAIL_THRESHOLD = 2 # consecutive bad checks before restarting (debounces re-enum blips) FAIL_THRESHOLD = 2 # consecutive bad checks before restarting (debounces re-enum blips)
@@ -98,15 +103,15 @@ def ap_enabled(ifname):
def recover(ifname): def recover(ifname):
log(f"AP {ifname} not beaconing — clearing failed state and restarting hostapd", "warn") log(f"AP {ifname} not beaconing — clearing failed state and restarting {HOSTAPD_UNIT}", "warn")
# reset-failed first in case some other path parked the unit in a failed state; # reset-failed first in case some other path parked the unit in a failed state;
# harmless when it isn't. # harmless when it isn't.
subprocess.run(["systemctl", "reset-failed", "hostapd"], subprocess.run(["systemctl", "reset-failed", HOSTAPD_UNIT],
capture_output=True, text=True) capture_output=True, text=True)
r = subprocess.run(["systemctl", "restart", "hostapd"], r = subprocess.run(["systemctl", "restart", HOSTAPD_UNIT],
capture_output=True, text=True) capture_output=True, text=True)
if r.returncode != 0: if r.returncode != 0:
log(f"hostapd restart returned {r.returncode}: {r.stderr.strip()}", "warn") log(f"{HOSTAPD_UNIT} restart returned {r.returncode}: {r.stderr.strip()}", "warn")
def main(): def main():
+13
View File
@@ -0,0 +1,13 @@
[Unit]
Description=VanLink 2.4GHz AP watchdog — recover hostapd-2g if the radio wedges after a USB glitch
After=hostapd-2g.service
Wants=hostapd-2g.service
[Service]
Type=simple
ExecStart=/usr/local/sbin/van-ap-watchdog /etc/hostapd/hostapd-2g.conf hostapd-2g
Restart=always
RestartSec=5
[Install]
WantedBy=multi-user.target
+9 -4
View File
@@ -13,6 +13,11 @@ install -D -m0644 ap/hostapd-restart.conf /etc/systemd/system/hostapd.service.
install -D -m0644 ap/default-hostapd /etc/default/hostapd install -D -m0644 ap/default-hostapd /etc/default/hostapd
install -D -m0755 ap/van-ap-watchdog /usr/local/sbin/van-ap-watchdog install -D -m0755 ap/van-ap-watchdog /usr/local/sbin/van-ap-watchdog
install -D -m0644 ap/van-ap-watchdog.service /etc/systemd/system/van-ap-watchdog.service install -D -m0644 ap/van-ap-watchdog.service /etc/systemd/system/van-ap-watchdog.service
install -D -m0644 ap/hostapd-2g.conf /etc/hostapd/hostapd-2g.conf
install -D -m0644 ap/hostapd-2g.service /etc/systemd/system/hostapd-2g.service
install -D -m0644 ap/11-van-ap-2g.network /etc/systemd/network/11-van-ap-2g.network
install -D -m0644 ap/van-ap-watchdog-2g.service /etc/systemd/system/van-ap-watchdog-2g.service
install -D -m0644 ap/rtw88.conf /etc/modprobe.d/rtw88.conf
install -D -m0644 ap/van-ap-dnsmasq.conf /etc/van-ap/dnsmasq.conf install -D -m0644 ap/van-ap-dnsmasq.conf /etc/van-ap/dnsmasq.conf
install -D -m0644 ap/van-ap-dnsmasq.service /etc/systemd/system/van-ap-dnsmasq.service install -D -m0644 ap/van-ap-dnsmasq.service /etc/systemd/system/van-ap-dnsmasq.service
install -D -m0644 ap/10-van-ap.network /etc/systemd/network/10-van-ap.network install -D -m0644 ap/10-van-ap.network /etc/systemd/network/10-van-ap.network
@@ -85,7 +90,7 @@ systemctl mask systemd-networkd-wait-online.service >/dev/null 2>&1 || true
# pick up the lid drop-in (re-execs logind; does NOT drop the network) # pick up the lid drop-in (re-execs logind; does NOT drop the network)
systemctl restart systemd-logind >/dev/null 2>&1 || true systemctl restart systemd-logind >/dev/null 2>&1 || true
systemctl unmask hostapd >/dev/null 2>&1 || true systemctl unmask hostapd >/dev/null 2>&1 || true
systemctl enable regdomain.service hostapd van-ap-dnsmasq nftables systemd-networkd van-failover van-thermal van-battery van-ap-watchdog >/dev/null 2>&1 || true systemctl enable regdomain.service hostapd hostapd-2g van-ap-dnsmasq nftables systemd-networkd van-failover van-thermal van-battery van-ap-watchdog van-ap-watchdog-2g >/dev/null 2>&1 || true
systemctl restart van-thermal van-battery systemctl restart van-thermal van-battery
# Heartbeat: only enable/start once the client binary is installed (README §4). # Heartbeat: only enable/start once the client binary is installed (README §4).
if [ -x /home/andreas/bin/hbc ]; then if [ -x /home/andreas/bin/hbc ]; then
@@ -98,9 +103,9 @@ nmcli general reload 2>/dev/null || systemctl reload NetworkManager 2>/dev/null
# restart in dependency order: br0 + AP iface + LAN member first, then hostapd adds # restart in dependency order: br0 + AP iface + LAN member first, then hostapd adds
# the wlan to br0, then dnsmasq binds br0, then NAT/failover # the wlan to br0, then dnsmasq binds br0, then NAT/failover
systemctl restart systemd-networkd systemctl restart systemd-networkd
systemctl restart hostapd van-ap-dnsmasq nftables van-failover systemctl restart hostapd hostapd-2g van-ap-dnsmasq nftables van-failover
# AP watchdog last, after hostapd is back up (it only ever restarts a wedged hostapd) # AP watchdogs last, after hostapd is back up (they only ever restart a wedged hostapd)
systemctl restart van-ap-watchdog systemctl restart van-ap-watchdog van-ap-watchdog-2g
networkctl reload 2>/dev/null || true networkctl reload 2>/dev/null || true
echo echo