2 Commits
Author SHA1 Message Date
kurogeek f98226bc6e router/wifi: assert no supplicant/NetworkManager owns the radios
With the AP-typed match, the station-mode netdev is unmanaged until
hostapd flips it, so any wpa_supplicant/iwd/NetworkManager in the
importing config would grab the radio first and keep resetting it to
managed mode under hostapd. Fail at eval instead of at runtime.
2026-09-18 09:36:07 +00:00
kurogeek d4e98d8dd5 router/wifi: match BSS .network files on WLANInterfaceType=ap
networkd enslaves the radio to br0 once, at link init. On real hardware
(GL-MT6000, mt798x-wmac) that happens while the netdev is still in station
mode, so the kernel rejects the bridge join (IFF_DONT_BRIDGE), the link is
parked in networkd's failed state and never retried: link_reconfigure_impl()
is a no-op while the matching .network file is unchanged, and the udev
'iw set type __ap' hook meant to pre-empt this loses the race. Wireless
clients associate but their DHCP never reaches vlan-lan.

Match on WLANInterfaceType=ap as well as the name. The station-mode netdev
then matches nothing (unmanaged) and the file first matches when hostapd
has switched the radio to AP and raised carrier, so the enslave succeeds
on the first try. Drop the udev hook.

Verified with checks.x86_64-linux.router (hwsim STA lease over the bridge).
2026-09-18 09:22:57 +00:00
+19 -18
View File
@@ -12,7 +12,6 @@
{ {
config, config,
lib, lib,
pkgs,
... ...
}: }:
let let
@@ -121,26 +120,18 @@ in
); );
message = "router: wifi.radios.<radio>.macAddress is required for radios broadcasting more than one network."; message = "router: wifi.radios.<radio>.macAddress is required for radios broadcasting more than one network.";
} }
{
assertion =
!config.networking.wireless.enable
&& !config.networking.wireless.iwd.enable
&& !config.networking.networkmanager.enable;
message = "router: wifi.enable needs the radios for hostapd; disable networking.wireless (wpa_supplicant), iwd and NetworkManager.";
}
]; ];
# Regulatory database for the kernel, so countryCode actually applies. # Regulatory database for the kernel, so countryCode actually applies.
hardware.wirelessRegulatoryDatabase = true; hardware.wirelessRegulatoryDatabase = true;
# The kernel refuses to bridge a wireless interface in station mode, and
# networkd stops retrying before hostapd switches the radio to AP mode;
# so put it in AP mode the moment it appears (kernel name or the renamed
# one, whichever the user configured). hostapd finds it already there.
services.udev.extraRules = lib.concatMapStrings (
radio:
let
run = ''RUN+="${pkgs.iw}/bin/iw dev ${radio} set type __ap"'';
in
''
ACTION=="add", SUBSYSTEM=="net", KERNEL=="${radio}", ${run}
ACTION=="add", SUBSYSTEM=="net", NAME=="${radio}", ${run}
''
) (lib.attrNames wifi.radios);
clan.core.vars.generators = lib.genAttrs (map (name: "wifi-${name}-passphrase") secured) ( clan.core.vars.generators = lib.genAttrs (map (name: "wifi-${name}-passphrase") secured) (
gen: gen:
let let
@@ -184,12 +175,22 @@ in
}; };
# Each BSS is an untagged access port of its VLAN on br0 (cf. accessPorts # Each BSS is an untagged access port of its VLAN on br0 (cf. accessPorts
# in network.nix); networkd enslaves the interface once hostapd creates it. # in network.nix). The kernel refuses to bridge a wireless interface in
# station mode (IFF_DONT_BRIDGE), and a failed enslave parks the link in
# networkd's `failed` state for good: networkd only re-evaluates a link
# when its matching .network file changes. So match on the AP interface
# type as well as the name: the radio's initial station-mode netdev
# matches nothing (unmanaged), and once hostapd switches it to AP and
# brings the carrier up, networkd matches this file for the first time
# and enslaves it. Extra BSSes are created by hostapd in AP mode already.
systemd.network.networks = lib.listToAttrs ( systemd.network.networks = lib.listToAttrs (
map ( map (
b: b:
lib.nameValuePair "27-wifi-${b.iface}" { lib.nameValuePair "27-wifi-${b.iface}" {
matchConfig.Name = b.iface; matchConfig = {
Name = b.iface;
WLANInterfaceType = "ap";
};
networkConfig.Bridge = "br0"; networkConfig.Bridge = "br0";
bridgeVLANs = [ bridgeVLANs = [
{ {