From d4e98d8dd57149238bd3ff1b1150f38f86e86679 Mon Sep 17 00:00:00 2001 From: kurogeek Date: Fri, 18 Sep 2026 09:19:16 +0000 Subject: [PATCH] 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). --- modules/clan/router/wifi.nix | 30 ++++++++++++------------------ 1 file changed, 12 insertions(+), 18 deletions(-) diff --git a/modules/clan/router/wifi.nix b/modules/clan/router/wifi.nix index 8b21768..fe8e2cf 100644 --- a/modules/clan/router/wifi.nix +++ b/modules/clan/router/wifi.nix @@ -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 = [ {