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).
This commit is contained in:
2026-09-18 09:22:57 +00:00
parent 60aac6efb2
commit d4e98d8dd5
+12 -18
View File
@@ -12,7 +12,6 @@
{
config,
lib,
pkgs,
...
}:
let
@@ -126,21 +125,6 @@ in
# Regulatory database for the kernel, so countryCode actually applies.
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) (
gen:
let
@@ -184,12 +168,22 @@ in
};
# 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 (
map (
b:
lib.nameValuePair "27-wifi-${b.iface}" {
matchConfig.Name = b.iface;
matchConfig = {
Name = b.iface;
WLANInterfaceType = "ap";
};
networkConfig.Bridge = "br0";
bridgeVLANs = [
{