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
+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]
# The AP wlan and the wired LAN port are both owned by systemd-networkd/hostapd
# (bridged into br0), so NetworkManager must not touch either of them.
unmanaged-devices=interface-name:wlxc83a35a4ee55;interface-name:enx00e04c331140
# The AP wlans (5GHz + 2.4GHz) and the wired LAN port are all owned by
# systemd-networkd/hostapd (bridged into br0), so NetworkManager must not touch them.
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
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.
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
@@ -26,7 +30,8 @@ import sys
import time
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"
INTERVAL = 15 # seconds between health checks (backstop cadence, not first response)
FAIL_THRESHOLD = 2 # consecutive bad checks before restarting (debounces re-enum blips)
@@ -98,15 +103,15 @@ def ap_enabled(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;
# harmless when it isn't.
subprocess.run(["systemctl", "reset-failed", "hostapd"],
subprocess.run(["systemctl", "reset-failed", HOSTAPD_UNIT],
capture_output=True, text=True)
r = subprocess.run(["systemctl", "restart", "hostapd"],
r = subprocess.run(["systemctl", "restart", HOSTAPD_UNIT],
capture_output=True, text=True)
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():
+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