Author SHA1 Message Date
kurogeek 4146f2c878 router/dhcp: drop Restart= already set by the nixpkgs kea unit 2026-09-18 10:08:03 +00:00
kurogeek 9ea058bfb8 router/dhcp: make Kea wait for its VLAN sockets
kea-dhcp4-server orders after network-online.target, which under networkd
only waits for the WAN carrier: the vlan-* links are RequiredForOnline=no.
On stellio Kea started before networkd had addressed vlan-lan/vlan-mgmt,
failed both binds and, per its default, kept running with no socket at
all (ss -ulnp showed nothing on :67). Wireless and wired clients'
DISCOVERs reached vlan-lan and were never answered.

Set service-sockets-require-all with a bounded retry so Kea keeps trying
while networkd catches up, and Restart=on-failure on the unit for the
case it still gives up.

Verified with checks.x86_64-linux.router.
2026-09-18 10:06:01 +00:00
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
2 changed files with 36 additions and 19 deletions
+17 -1
View File
@@ -12,7 +12,19 @@ in
services.kea.dhcp4 = { services.kea.dhcp4 = {
enable = true; enable = true;
settings = { settings = {
interfaces-config.interfaces = lib.mapAttrsToList (name: _: "vlan-${name}") dhcpVlans; interfaces-config = {
interfaces = lib.mapAttrsToList (name: _: "vlan-${name}") dhcpVlans;
# The unit orders after network-online.target, which under networkd
# only waits for the WAN carrier (the vlan-* links are
# RequiredForOnline=no), so Kea can start before vlan-* have their
# addresses. By default it then logs the failed bind and runs with no
# socket at all: clients' DISCOVERs reach vlan-lan and nobody answers.
# Insist on every socket and keep retrying while networkd catches up;
# if it still cannot bind, exit and let systemd restart the unit.
service-sockets-require-all = true;
service-sockets-max-retries = 60;
service-sockets-retry-wait-time = 1000;
};
lease-database = { lease-database = {
type = "memfile"; type = "memfile";
persist = true; persist = true;
@@ -44,6 +56,10 @@ in
}; };
}; };
# The nixpkgs unit already has Restart=on-failure; space the restarts out so
# a persistent bind failure does not trip the start-rate limit.
systemd.services.kea-dhcp4-server.serviceConfig.RestartSec = 5;
services.blocky = { services.blocky = {
enable = true; enable = true;
settings = { settings = {
+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 = [
{ {