feat(system-manager): port networking.wireless and generic DHCP

Run the upstream NixOS wpa_supplicant module under system-manager through a
compat shim. system-manager already reuses nixpkgs' systemdUtils,
environment.etc and userborn, so the unit, the generated config and the
wpa_supplicant user come out unchanged; only four NixOS-only options needed
stubbing, two of which are re-expressed on top of environment.etc.

Extract the wireless config to hosts/wireless.nix so both trees share one
source: they sit at the same depth, so every path resolves identically.
electra's generated wpa_supplicant.conf is now byte-identical to atlas'
except for the country line.

Take over networkd from netplan on electra with the same generic match rules
NixOS uses, which also covers ethernet and USB tethering, neither of which
was configured before. netplan is neutered by masking its generator rather
than uninstalling it, since purging netplan.io would take cloud-init,
ubuntu-minimal and ubuntu-server-minimal with it.

Assisted-by: pi (claude-opus-5)
This commit is contained in:
Gabriel Fontes
2026-08-13 16:36:09 -03:00
parent 91005fcfb9
commit a587e539e9
11 changed files with 209 additions and 59 deletions
+3 -59
View File
@@ -1,63 +1,7 @@
{
config,
...
}: {
{...}: {
imports = [../../../wireless.nix];
hardware.bluetooth = {
enable = true;
};
# Wireless secrets stored through sops
sops.secrets.wireless = {
sopsFile = ../../../secrets.yaml;
owner = config.users.users.wpa_supplicant.name;
group = config.users.users.wpa_supplicant.group;
};
networking.wireless = {
enable = true;
fallbackToWPA2 = false;
# Declarative
secretsFile = config.sops.secrets.wireless.path;
networks = {
"CAT_HOUSE" = {
pskRaw = "ext:cat_house";
};
"Marcos_2.4Ghz" = {
pskRaw = "ext:marcos_24";
};
"Marcos_5Ghz" = {
pskRaw = "ext:marcos_50";
};
"Misterio" = {
pskRaw = "ext:misterio";
authProtocols = ["WPA-PSK"];
# extraConfig = ''
# mesh_fwding=1
# '';
};
"VIVOFIBRA-FC41-5G" = {
pskRaw = "ext:marcos_santos_5g";
};
"Nijland" = {
pskRaw = "ext:nijland";
};
"eduroam" = {
authProtocols = ["WPA-EAP"];
auth = ''
pairwise=CCMP
group=CCMP TKIP
eap=TTLS
domain_suffix_match="semfio.usp.br"
ca_cert="${./eduroam-cert.pem}"
identity="10856803@usp.br"
password=ext:eduroam
phase2="auth=MSCHAPV2"
'';
};
};
# Imperative
allowAuxiliaryImperativeNetworks = true;
userControlled = true;
};
}
@@ -15,6 +15,7 @@ in {
inputs.home-manager.nixosModules.home-manager
inputs.nix-system-graphics.systemModules.default
./greetd.nix
./network.nix
./nix.nix
./pam.nix
./sops.nix
@@ -0,0 +1,45 @@
{...}: {
# systemd-networkd has no implicit "manage everything" default: a link that
# matches no .network file stays unmanaged and is never even brought up. On
# NixOS this file is generated for you (nixos/modules/tasks/
# network-interfaces-systemd.nix, genericDhcpNetworks); here netplan used to
# do it. These mirror the upstream match rules, so nothing is named
# explicitly and new hardware works on plug-in.
environment.etc = {
# Type=ether with no kind covers physical ethernet, USB dongles and USB
# tethered phones alike.
"systemd/network/99-ethernet-default-dhcp.network".text = ''
[Match]
Type=ether
Kind=!*
[Network]
DHCP=yes
IPv6PrivacyExtensions=kernel
'';
# One above ethernet's default of 1024, so wired wins when both are up.
"systemd/network/99-wireless-client-dhcp.network".text = ''
[Match]
WLANInterfaceType=station
[Network]
DHCP=yes
IPv6PrivacyExtensions=kernel
[DHCPv4]
RouteMetric=1025
[IPv6AcceptRA]
RouteMetric=1025
'';
# Neuter netplan without uninstalling it: purging netplan.io would take
# cloud-init, ubuntu-minimal and ubuntu-server-minimal with it. Masking the
# generator is reverted by `system-manager deactivate`, unlike apt state.
#
# NOTE: systemd.generators would be the natural home for this, but
# system-manager declares that option without ever wiring it up.
"systemd/system-generators/netplan".source = "/dev/null";
};
}
@@ -0,0 +1,18 @@
{...}: {
imports = [../../../wireless.nix];
# The upstream module emits no country line; Ubuntu's netplan-generated
# config used to set this, and dropping it would silently relax the
# regulatory limits.
networking.wireless.extraConfig = "country=BR";
# Ubuntu ships two supplicants that would fight ours over the interface and
# over /run/wpa_supplicant (which our unit claims via RuntimeDirectory=):
# the templated one, and the DBus-activated one. The latter must be masked
# rather than merely disabled, since any client touching
# fi.w1.wpa_supplicant1 would otherwise start it again.
systemd.maskedUnits = [
"wpa_supplicant.service"
"wpa_supplicant@.service"
];
}
@@ -34,6 +34,7 @@ in {
"render"
"sudo"
"video"
"wpa_supplicant"
];
};
};
@@ -37,6 +37,16 @@ autoinstall:
- wpasupplicant
runcmd:
# The installer's netplan wifi config is load-bearing for exactly one
# boot: the bootstrap below needs the network before system-manager
# exists to configure it. Once activated, system-manager owns
# /etc/systemd/network and wpa_supplicant (see hosts/system-manager/
# common/optional/network.nix), and netplan's generator is masked.
# Stop cloud-init from re-rendering network config on later boots.
- |
printf 'network: {config: disabled}\n' \
> /etc/cloud/cloud.cfg.d/99-disable-network-config.cfg
# This laptop must not expose an SSH server. Keep the client for Git and
# ssh-keygen, but remove and mask the server even if the image included it.
- |
+6
View File
@@ -2,8 +2,14 @@
imports = [
../common/global
../common/users/gabriel
../common/optional/wireless.nix
];
_module.args.systemManagerHostName = "electra";
nixpkgs.hostPlatform = "x86_64-linux";
# Pinned rather than auto-detected: with an empty list the upstream module
# installs a udev rule that calls /run/current-system/systemd/bin/systemctl,
# a path that does not exist outside NixOS.
networking.wireless.interfaces = ["wlp0s20f3"];
}
+55
View File
@@ -0,0 +1,55 @@
{config, ...}: {
sops.secrets.wireless = {
sopsFile = ./secrets.yaml;
owner = config.users.users.wpa_supplicant.name;
group = config.users.users.wpa_supplicant.group;
};
networking.wireless = {
enable = true;
fallbackToWPA2 = false;
# Declarative
secretsFile = config.sops.secrets.wireless.path;
networks = {
"CAT_HOUSE" = {
pskRaw = "ext:cat_house";
};
"Marcos_2.4Ghz" = {
pskRaw = "ext:marcos_24";
};
"Marcos_5Ghz" = {
pskRaw = "ext:marcos_50";
};
"Misterio" = {
pskRaw = "ext:misterio";
authProtocols = ["WPA-PSK"];
# extraConfig = ''
# mesh_fwding=1
# '';
};
"VIVOFIBRA-FC41-5G" = {
pskRaw = "ext:marcos_santos_5g";
};
"Nijland" = {
pskRaw = "ext:nijland";
};
"eduroam" = {
authProtocols = ["WPA-EAP"];
auth = ''
pairwise=CCMP
group=CCMP TKIP
eap=TTLS
domain_suffix_match="semfio.usp.br"
ca_cert="${./eduroam-cert.pem}"
identity="10856803@usp.br"
password=ext:eduroam
phase2="auth=MSCHAPV2"
'';
};
};
# Imperative
allowAuxiliaryImperativeNetworks = true;
userControlled = true;
};
}
+1
View File
@@ -2,4 +2,5 @@
hydra-auto-upgrade = import ./hydra-auto-upgrade.nix;
nix-registry = import ./nix-registry.nix;
unix-chkpwd = import ./unix-chkpwd.nix;
wireless = import ./wireless.nix;
}
+69
View File
@@ -0,0 +1,69 @@
# Compat shim: lets the upstream NixOS `networking.wireless` (wpa_supplicant)
# module evaluate under system-manager.
#
# system-manager reuses nixpkgs' systemdUtils, environment.etc and userborn, so
# the unit, the generated wpa_supplicant.conf and the wpa_supplicant user all
# come out unchanged. Only a handful of NixOS-only options are missing: three
# are read but never acted on, and the two that do matter are re-expressed on
# top of environment.etc, which the host distro reads from the same paths.
{
config,
lib,
inputs,
...
}: let
cfg = config.services;
in {
imports = [
"${inputs.nixpkgs}/nixos/modules/services/networking/wpa_supplicant.nix"
];
options = {
# Set by the wpa_supplicant module; the host distro ships its own regdb
# (Ubuntu: /lib/firmware/regulatory.db).
hardware.wirelessRegulatoryDatabase = lib.mkOption {
type = lib.types.bool;
default = false;
internal = true;
};
# Only read by the module's assertions.
networking.networkmanager.enable = lib.mkOption {
type = lib.types.bool;
default = false;
internal = true;
};
services.connman.enable = lib.mkOption {
type = lib.types.bool;
default = false;
internal = true;
};
services.dbus.packages = lib.mkOption {
type = lib.types.listOf lib.types.package;
default = [];
description = "Packages whose DBus system policy should be linked into /etc.";
};
services.udev.extraRules = lib.mkOption {
type = lib.types.lines;
default = "";
description = "Extra udev rules, written to /etc/udev/rules.d.";
};
};
config = {
environment.etc = lib.mkMerge [
(lib.mkIf (cfg.udev.extraRules != "") {
"udev/rules.d/99-system-manager.rules".text = cfg.udev.extraRules;
})
# The attribute name may not carry string context, hence the discard; the
# value keeps its reference to the store path.
(lib.listToAttrs (map (file: {
name = "dbus-1/system.d/" + builtins.unsafeDiscardStringContext (baseNameOf file);
value = {source = file;};
})
(lib.concatMap (pkg: lib.filesystem.listFilesRecursive "${pkg}/share/dbus-1/system.d")
(lib.filter (pkg: builtins.pathExists "${pkg}/share/dbus-1/system.d") cfg.dbus.packages))))
];
};
}