nixpkgs/nixos/modules/services/networking/wpa_supplicant.nix
rnhmjoj 99e8af51b2
nixos/wireless: use udev to wait for interfaces
I may have finally found a clean solution to the issues[1][2][3] with
the automatic discovery of wireless network interfaces.

[1]: https://github.com/NixOS/nixpkgs/issues/101963
[2]: https://github.com/NixOS/nixpkgs/issues/23196
[3]: https://github.com/NixOS/nixpkgs/pull/125917#issuecomment-856000426

Currently the start script fails right away if no interface is available
by the time it's running, possibly leaving the system without network.
This happens when running a little early in the boot. A solution is to
instead wait for at least one interface to appear before scanning the
/sys/class/net/ directory. This is done here by listening for the right
udev events (from the net/wlan subsystem) using the `udevadm monitor`
command and grep to match its output.

This methods guarantees the availability of at least one interface to
wpa_supplicant, but won't add additional interfaces once it has started.
However, if the current interface is lost, say unplugged, the service is
automatically stopped and will be restarted as soon as a one (not
necessarily the same) is detected. It would be possible make this fully
dynamic by running another service that continously listen for udev
events and manages the main wpa_supplicant daemon, but this is probably
overkill.

I tested the following cases:

  - one interface, starting at boot, w/o predictable naming scheme
  - two interfaces, starting at boot (intel wireless and a usb adapter),
    w/o predictable naming scheme
  - one interface after the system booted, w/o predictable naming scheme
  - two interfaces after the system booted, w/o predictable naming scheme
  - unplugging and plugging back the current interface
2021-08-12 00:31:06 +02:00

301 lines
10 KiB
Nix

{ config, lib, pkgs, utils, ... }:
with lib;
let
package = if cfg.allowAuxiliaryImperativeNetworks
then pkgs.wpa_supplicant_ro_ssids
else pkgs.wpa_supplicant;
cfg = config.networking.wireless;
configFile = if cfg.networks != {} || cfg.extraConfig != "" || cfg.userControlled.enable then pkgs.writeText "wpa_supplicant.conf" ''
${optionalString cfg.userControlled.enable ''
ctrl_interface=DIR=/run/wpa_supplicant GROUP=${cfg.userControlled.group}
update_config=1''}
${cfg.extraConfig}
${concatStringsSep "\n" (mapAttrsToList (ssid: config: with config; let
key = if psk != null
then ''"${psk}"''
else pskRaw;
baseAuth = if key != null
then "psk=${key}"
else "key_mgmt=NONE";
in ''
network={
ssid="${ssid}"
${optionalString (priority != null) ''priority=${toString priority}''}
${optionalString hidden "scan_ssid=1"}
${if (auth != null) then auth else baseAuth}
${extraConfig}
}
'') cfg.networks)}
'' else "/etc/wpa_supplicant.conf";
in {
options = {
networking.wireless = {
enable = mkEnableOption "wpa_supplicant";
interfaces = mkOption {
type = types.listOf types.str;
default = [];
example = [ "wlan0" "wlan1" ];
description = ''
The interfaces <command>wpa_supplicant</command> will use. If empty, it will
automatically use all wireless interfaces.
'';
};
driver = mkOption {
type = types.str;
default = "nl80211,wext";
description = "Force a specific wpa_supplicant driver.";
};
allowAuxiliaryImperativeNetworks = mkEnableOption "support for imperative & declarative networks" // {
description = ''
Whether to allow configuring networks "imperatively" (e.g. via
<package>wpa_supplicant_gui</package>) and declaratively via
<xref linkend="opt-networking.wireless.networks" />.
Please note that this adds a custom patch to <package>wpa_supplicant</package>.
'';
};
networks = mkOption {
type = types.attrsOf (types.submodule {
options = {
psk = mkOption {
type = types.nullOr types.str;
default = null;
description = ''
The network's pre-shared key in plaintext defaulting
to being a network without any authentication.
Be aware that these will be written to the nix store
in plaintext!
Mutually exclusive with <varname>pskRaw</varname>.
'';
};
pskRaw = mkOption {
type = types.nullOr types.str;
default = null;
description = ''
The network's pre-shared key in hex defaulting
to being a network without any authentication.
Mutually exclusive with <varname>psk</varname>.
'';
};
auth = mkOption {
type = types.nullOr types.str;
default = null;
example = ''
key_mgmt=WPA-EAP
eap=PEAP
identity="user@example.com"
password="secret"
'';
description = ''
Use this option to configure advanced authentication methods like EAP.
See
<citerefentry>
<refentrytitle>wpa_supplicant.conf</refentrytitle>
<manvolnum>5</manvolnum>
</citerefentry>
for example configurations.
Mutually exclusive with <varname>psk</varname> and <varname>pskRaw</varname>.
'';
};
hidden = mkOption {
type = types.bool;
default = false;
description = ''
Set this to <literal>true</literal> if the SSID of the network is hidden.
'';
example = literalExample ''
{ echelon = {
hidden = true;
psk = "abcdefgh";
};
}
'';
};
priority = mkOption {
type = types.nullOr types.int;
default = null;
description = ''
By default, all networks will get same priority group (0). If some of the
networks are more desirable, this field can be used to change the order in
which wpa_supplicant goes through the networks when selecting a BSS. The
priority groups will be iterated in decreasing priority (i.e., the larger the
priority value, the sooner the network is matched against the scan results).
Within each priority group, networks will be selected based on security
policy, signal strength, etc.
'';
};
extraConfig = mkOption {
type = types.str;
default = "";
example = ''
bssid_blacklist=02:11:22:33:44:55 02:22:aa:44:55:66
'';
description = ''
Extra configuration lines appended to the network block.
See
<citerefentry>
<refentrytitle>wpa_supplicant.conf</refentrytitle>
<manvolnum>5</manvolnum>
</citerefentry>
for available options.
'';
};
};
});
description = ''
The network definitions to automatically connect to when
<command>wpa_supplicant</command> is running. If this
parameter is left empty wpa_supplicant will use
/etc/wpa_supplicant.conf as the configuration file.
'';
default = {};
example = literalExample ''
{ echelon = { # SSID with no spaces or special characters
psk = "abcdefgh";
};
"echelon's AP" = { # SSID with spaces and/or special characters
psk = "ijklmnop";
};
"free.wifi" = {}; # Public wireless network
}
'';
};
userControlled = {
enable = mkOption {
type = types.bool;
default = false;
description = ''
Allow normal users to control wpa_supplicant through wpa_gui or wpa_cli.
This is useful for laptop users that switch networks a lot and don't want
to depend on a large package such as NetworkManager just to pick nearby
access points.
When using a declarative network specification you cannot persist any
settings via wpa_gui or wpa_cli.
'';
};
group = mkOption {
type = types.str;
default = "wheel";
example = "network";
description = "Members of this group can control wpa_supplicant.";
};
};
extraConfig = mkOption {
type = types.str;
default = "";
example = ''
p2p_disabled=1
'';
description = ''
Extra lines appended to the configuration file.
See
<citerefentry>
<refentrytitle>wpa_supplicant.conf</refentrytitle>
<manvolnum>5</manvolnum>
</citerefentry>
for available options.
'';
};
};
};
config = mkIf cfg.enable {
assertions = flip mapAttrsToList cfg.networks (name: cfg: {
assertion = with cfg; count (x: x != null) [ psk pskRaw auth ] <= 1;
message = ''options networking.wireless."${name}".{psk,pskRaw,auth} are mutually exclusive'';
});
environment.systemPackages = [ package ];
services.dbus.packages = [ package ];
services.udev.packages = [ pkgs.crda ];
# FIXME: start a separate wpa_supplicant instance per interface.
systemd.services.wpa_supplicant = let
ifaces = cfg.interfaces;
deviceUnit = interface: [ "sys-subsystem-net-devices-${utils.escapeSystemdPath interface}.device" ];
in {
description = "WPA Supplicant";
after = lib.concatMap deviceUnit ifaces;
before = [ "network.target" ];
wants = [ "network.target" ];
requires = lib.concatMap deviceUnit ifaces;
wantedBy = [ "multi-user.target" ];
stopIfChanged = false;
path = [ package pkgs.udev ];
script = let
configStr = if cfg.allowAuxiliaryImperativeNetworks
then "-c /etc/wpa_supplicant.conf -I ${configFile}"
else "-c ${configFile}";
in ''
if [ -f /etc/wpa_supplicant.conf -a "/etc/wpa_supplicant.conf" != "${configFile}" ]; then
echo >&2 "<3>/etc/wpa_supplicant.conf present but ignored. Generated ${configFile} is used instead."
fi
iface_args="-s -u -D${cfg.driver} ${configStr}"
${if ifaces == [] then ''
# detect interfaces automatically
# check if there are no wireless interface
if ! find -H /sys/class/net/* -name wireless | grep -q .; then
# if so, wait until one appears
echo "Waiting for wireless interfaces"
grep -q '^ACTION=add' < <(stdbuf -oL -- udevadm monitor -s net/wlan -pu)
# Note: the above line has been carefully written:
# 1. The process substitution avoids udevadm hanging (after grep has quit)
# until it tries to write to the pipe again. Not even pipefail works here.
# 2. stdbuf is needed because udevadm output is buffered by default and grep
# may hang until more udev events enter the pipe.
fi
# add any interface found to the daemon arguments
for name in $(find -H /sys/class/net/* -name wireless | cut -d/ -f 5); do
echo "Adding interface $name"
args+="''${args:+ -N} -i$name $iface_args"
done
'' else ''
# add known interfaces to the daemon arguments
args="${concatMapStringsSep " -N " (i: "-i${i} $iface_args") ifaces}"
''}
# finally start daemon
exec wpa_supplicant $args
'';
};
powerManagement.resumeCommands = ''
/run/current-system/systemd/bin/systemctl try-restart wpa_supplicant
'';
# Restart wpa_supplicant when a wlan device appears or disappears.
services.udev.extraRules = ''
ACTION=="add|remove", SUBSYSTEM=="net", ENV{DEVTYPE}=="wlan", RUN+="/run/current-system/systemd/bin/systemctl try-restart wpa_supplicant.service"
'';
};
meta.maintainers = with lib.maintainers; [ globin ];
}