1{ config, lib, options, pkgs, utils, ... }:
2
3with lib;
4
5let
6 package = if cfg.allowAuxiliaryImperativeNetworks
7 then pkgs.wpa_supplicant_ro_ssids
8 else pkgs.wpa_supplicant;
9
10 cfg = config.networking.wireless;
11 opt = options.networking.wireless;
12
13 wpa3Protocols = [ "SAE" "FT-SAE" ];
14 hasMixedWPA = opts:
15 let
16 hasWPA3 = !mutuallyExclusive opts.authProtocols wpa3Protocols;
17 others = subtractLists wpa3Protocols opts.authProtocols;
18 in hasWPA3 && others != [];
19
20 # Gives a WPA3 network higher priority
21 increaseWPA3Priority = opts:
22 opts // optionalAttrs (hasMixedWPA opts)
23 { priority = if opts.priority == null
24 then 1
25 else opts.priority + 1;
26 };
27
28 # Creates a WPA2 fallback network
29 mkWPA2Fallback = opts:
30 opts // { authProtocols = subtractLists wpa3Protocols opts.authProtocols; };
31
32 # Networks attrset as a list
33 networkList = mapAttrsToList (ssid: opts: opts // { inherit ssid; })
34 cfg.networks;
35
36 # List of all networks (normal + generated fallbacks)
37 allNetworks =
38 if cfg.fallbackToWPA2
39 then map increaseWPA3Priority networkList
40 ++ map mkWPA2Fallback (filter hasMixedWPA networkList)
41 else networkList;
42
43 # Content of wpa_supplicant.conf
44 generatedConfig = concatStringsSep "\n" (
45 (map mkNetwork allNetworks)
46 ++ optional cfg.userControlled.enable (concatStringsSep "\n"
47 [ "ctrl_interface=/run/wpa_supplicant"
48 "ctrl_interface_group=${cfg.userControlled.group}"
49 "update_config=1"
50 ])
51 ++ [ "pmf=1" ]
52 ++ optional cfg.scanOnLowSignal ''bgscan="simple:30:-70:3600"''
53 ++ optional (cfg.extraConfig != "") cfg.extraConfig);
54
55 configIsGenerated = with cfg;
56 networks != {} || extraConfig != "" || userControlled.enable;
57
58 # the original configuration file
59 configFile =
60 if configIsGenerated
61 then pkgs.writeText "wpa_supplicant.conf" generatedConfig
62 else "/etc/wpa_supplicant.conf";
63 # the config file with environment variables replaced
64 finalConfig = ''"$RUNTIME_DIRECTORY"/wpa_supplicant.conf'';
65
66 # Creates a network block for wpa_supplicant.conf
67 mkNetwork = opts:
68 let
69 quote = x: ''"${x}"'';
70 indent = x: " " + x;
71
72 pskString = if opts.psk != null
73 then quote opts.psk
74 else opts.pskRaw;
75
76 options = [
77 "ssid=${quote opts.ssid}"
78 (if pskString != null || opts.auth != null
79 then "key_mgmt=${concatStringsSep " " opts.authProtocols}"
80 else "key_mgmt=NONE")
81 ] ++ optional opts.hidden "scan_ssid=1"
82 ++ optional (pskString != null) "psk=${pskString}"
83 ++ optionals (opts.auth != null) (filter (x: x != "") (splitString "\n" opts.auth))
84 ++ optional (opts.priority != null) "priority=${toString opts.priority}"
85 ++ optional (opts.extraConfig != "") opts.extraConfig;
86 in ''
87 network={
88 ${concatMapStringsSep "\n" indent options}
89 }
90 '';
91
92 # Creates a systemd unit for wpa_supplicant bound to a given (or any) interface
93 mkUnit = iface:
94 let
95 deviceUnit = optional (iface != null) "sys-subsystem-net-devices-${utils.escapeSystemdPath iface}.device";
96 configStr = if cfg.allowAuxiliaryImperativeNetworks
97 then "-c /etc/wpa_supplicant.conf -I ${finalConfig}"
98 else "-c ${finalConfig}";
99 in {
100 description = "WPA Supplicant instance" + optionalString (iface != null) " for interface ${iface}";
101
102 after = deviceUnit;
103 before = [ "network.target" ];
104 wants = [ "network.target" ];
105 requires = deviceUnit;
106 wantedBy = [ "multi-user.target" ];
107 stopIfChanged = false;
108
109 path = [ package ];
110 serviceConfig.RuntimeDirectory = "wpa_supplicant";
111 serviceConfig.RuntimeDirectoryMode = "700";
112 serviceConfig.EnvironmentFile = mkIf (cfg.environmentFile != null)
113 (builtins.toString cfg.environmentFile);
114
115 script =
116 ''
117 ${optionalString (configIsGenerated && !cfg.allowAuxiliaryImperativeNetworks) ''
118 if [ -f /etc/wpa_supplicant.conf ]; then
119 echo >&2 "<3>/etc/wpa_supplicant.conf present but ignored. Generated ${configFile} is used instead."
120 fi
121 ''}
122
123 # substitute environment variables
124 ${pkgs.gawk}/bin/awk '{
125 for(varname in ENVIRON)
126 gsub("@"varname"@", ENVIRON[varname])
127 print
128 }' "${configFile}" > "${finalConfig}"
129
130 iface_args="-s ${optionalString cfg.dbusControlled "-u"} -D${cfg.driver} ${configStr}"
131
132 ${if iface == null then ''
133 # detect interfaces automatically
134
135 # check if there are no wireless interfaces
136 if ! find -H /sys/class/net/* -name wireless | grep -q .; then
137 # if so, wait until one appears
138 echo "Waiting for wireless interfaces"
139 grep -q '^ACTION=add' < <(stdbuf -oL -- udevadm monitor -s net/wlan -pu)
140 # Note: the above line has been carefully written:
141 # 1. The process substitution avoids udevadm hanging (after grep has quit)
142 # until it tries to write to the pipe again. Not even pipefail works here.
143 # 2. stdbuf is needed because udevadm output is buffered by default and grep
144 # may hang until more udev events enter the pipe.
145 fi
146
147 # add any interface found to the daemon arguments
148 for name in $(find -H /sys/class/net/* -name wireless | cut -d/ -f 5); do
149 echo "Adding interface $name"
150 args+="''${args:+ -N} -i$name $iface_args"
151 done
152 '' else ''
153 # add known interface to the daemon arguments
154 args="-i${iface} $iface_args"
155 ''}
156
157 # finally start daemon
158 exec wpa_supplicant $args
159 '';
160 };
161
162 systemctl = "/run/current-system/systemd/bin/systemctl";
163
164in {
165 options = {
166 networking.wireless = {
167 enable = mkEnableOption (lib.mdDoc "wpa_supplicant");
168
169 interfaces = mkOption {
170 type = types.listOf types.str;
171 default = [];
172 example = [ "wlan0" "wlan1" ];
173 description = lib.mdDoc ''
174 The interfaces {command}`wpa_supplicant` will use. If empty, it will
175 automatically use all wireless interfaces.
176
177 ::: {.note}
178 A separate wpa_supplicant instance will be started for each interface.
179 :::
180 '';
181 };
182
183 driver = mkOption {
184 type = types.str;
185 default = "nl80211,wext";
186 description = lib.mdDoc "Force a specific wpa_supplicant driver.";
187 };
188
189 allowAuxiliaryImperativeNetworks = mkEnableOption (lib.mdDoc "support for imperative & declarative networks") // {
190 description = lib.mdDoc ''
191 Whether to allow configuring networks "imperatively" (e.g. via
192 `wpa_supplicant_gui`) and declaratively via
193 [](#opt-networking.wireless.networks).
194
195 Please note that this adds a custom patch to `wpa_supplicant`.
196 '';
197 };
198
199 scanOnLowSignal = mkOption {
200 type = types.bool;
201 default = true;
202 description = lib.mdDoc ''
203 Whether to periodically scan for (better) networks when the signal of
204 the current one is low. This will make roaming between access points
205 faster, but will consume more power.
206 '';
207 };
208
209 fallbackToWPA2 = mkOption {
210 type = types.bool;
211 default = true;
212 description = lib.mdDoc ''
213 Whether to fall back to WPA2 authentication protocols if WPA3 failed.
214 This allows old wireless cards (that lack recent features required by
215 WPA3) to connect to mixed WPA2/WPA3 access points.
216
217 To avoid possible downgrade attacks, disable this options.
218 '';
219 };
220
221 environmentFile = mkOption {
222 type = types.nullOr types.path;
223 default = null;
224 example = "/run/secrets/wireless.env";
225 description = lib.mdDoc ''
226 File consisting of lines of the form `varname=value`
227 to define variables for the wireless configuration.
228
229 See section "EnvironmentFile=" in {manpage}`systemd.exec(5)` for a syntax reference.
230
231 Secrets (PSKs, passwords, etc.) can be provided without adding them to
232 the world-readable Nix store by defining them in the environment file and
233 referring to them in option {option}`networking.wireless.networks`
234 with the syntax `@varname@`. Example:
235
236 ```
237 # content of /run/secrets/wireless.env
238 PSK_HOME=mypassword
239 PASS_WORK=myworkpassword
240 ```
241
242 ```
243 # wireless-related configuration
244 networking.wireless.environmentFile = "/run/secrets/wireless.env";
245 networking.wireless.networks = {
246 home.psk = "@PSK_HOME@";
247 work.auth = '''
248 eap=PEAP
249 identity="my-user@example.com"
250 password="@PASS_WORK@"
251 ''';
252 };
253 ```
254 '';
255 };
256
257 networks = mkOption {
258 type = types.attrsOf (types.submodule {
259 options = {
260 psk = mkOption {
261 type = types.nullOr types.str;
262 default = null;
263 description = lib.mdDoc ''
264 The network's pre-shared key in plaintext defaulting
265 to being a network without any authentication.
266
267 ::: {.warning}
268 Be aware that this will be written to the nix store
269 in plaintext! Use an environment variable instead.
270 :::
271
272 ::: {.note}
273 Mutually exclusive with {var}`pskRaw`.
274 :::
275 '';
276 };
277
278 pskRaw = mkOption {
279 type = types.nullOr types.str;
280 default = null;
281 description = lib.mdDoc ''
282 The network's pre-shared key in hex defaulting
283 to being a network without any authentication.
284
285 ::: {.warning}
286 Be aware that this will be written to the nix store
287 in plaintext! Use an environment variable instead.
288 :::
289
290 ::: {.note}
291 Mutually exclusive with {var}`psk`.
292 :::
293 '';
294 };
295
296 authProtocols = mkOption {
297 default = [
298 # WPA2 and WPA3
299 "WPA-PSK" "WPA-EAP" "SAE"
300 # 802.11r variants of the above
301 "FT-PSK" "FT-EAP" "FT-SAE"
302 ];
303 # The list can be obtained by running this command
304 # awk '
305 # /^# key_mgmt: /{ run=1 }
306 # /^#$/{ run=0 }
307 # /^# [A-Z0-9-]{2,}/{ if(run){printf("\"%s\"\n", $2)} }
308 # ' /run/current-system/sw/share/doc/wpa_supplicant/wpa_supplicant.conf.example
309 type = types.listOf (types.enum [
310 "WPA-PSK"
311 "WPA-EAP"
312 "IEEE8021X"
313 "NONE"
314 "WPA-NONE"
315 "FT-PSK"
316 "FT-EAP"
317 "FT-EAP-SHA384"
318 "WPA-PSK-SHA256"
319 "WPA-EAP-SHA256"
320 "SAE"
321 "FT-SAE"
322 "WPA-EAP-SUITE-B"
323 "WPA-EAP-SUITE-B-192"
324 "OSEN"
325 "FILS-SHA256"
326 "FILS-SHA384"
327 "FT-FILS-SHA256"
328 "FT-FILS-SHA384"
329 "OWE"
330 "DPP"
331 ]);
332 description = lib.mdDoc ''
333 The list of authentication protocols accepted by this network.
334 This corresponds to the `key_mgmt` option in wpa_supplicant.
335 '';
336 };
337
338 auth = mkOption {
339 type = types.nullOr types.str;
340 default = null;
341 example = ''
342 eap=PEAP
343 identity="user@example.com"
344 password="@EXAMPLE_PASSWORD@"
345 '';
346 description = lib.mdDoc ''
347 Use this option to configure advanced authentication methods like EAP.
348 See
349 {manpage}`wpa_supplicant.conf(5)`
350 for example configurations.
351
352 ::: {.warning}
353 Be aware that this will be written to the nix store
354 in plaintext! Use an environment variable for secrets.
355 :::
356
357 ::: {.note}
358 Mutually exclusive with {var}`psk` and
359 {var}`pskRaw`.
360 :::
361 '';
362 };
363
364 hidden = mkOption {
365 type = types.bool;
366 default = false;
367 description = lib.mdDoc ''
368 Set this to `true` if the SSID of the network is hidden.
369 '';
370 example = literalExpression ''
371 { echelon = {
372 hidden = true;
373 psk = "abcdefgh";
374 };
375 }
376 '';
377 };
378
379 priority = mkOption {
380 type = types.nullOr types.int;
381 default = null;
382 description = lib.mdDoc ''
383 By default, all networks will get same priority group (0). If some of the
384 networks are more desirable, this field can be used to change the order in
385 which wpa_supplicant goes through the networks when selecting a BSS. The
386 priority groups will be iterated in decreasing priority (i.e., the larger the
387 priority value, the sooner the network is matched against the scan results).
388 Within each priority group, networks will be selected based on security
389 policy, signal strength, etc.
390 '';
391 };
392
393 extraConfig = mkOption {
394 type = types.str;
395 default = "";
396 example = ''
397 bssid_blacklist=02:11:22:33:44:55 02:22:aa:44:55:66
398 '';
399 description = lib.mdDoc ''
400 Extra configuration lines appended to the network block.
401 See
402 {manpage}`wpa_supplicant.conf(5)`
403 for available options.
404 '';
405 };
406
407 };
408 });
409 description = lib.mdDoc ''
410 The network definitions to automatically connect to when
411 {command}`wpa_supplicant` is running. If this
412 parameter is left empty wpa_supplicant will use
413 /etc/wpa_supplicant.conf as the configuration file.
414 '';
415 default = {};
416 example = literalExpression ''
417 { echelon = { # SSID with no spaces or special characters
418 psk = "abcdefgh"; # (password will be written to /nix/store!)
419 };
420
421 echelon = { # safe version of the above: read PSK from the
422 psk = "@PSK_ECHELON@"; # variable PSK_ECHELON, defined in environmentFile,
423 }; # this won't leak into /nix/store
424
425 "echelon's AP" = { # SSID with spaces and/or special characters
426 psk = "ijklmnop"; # (password will be written to /nix/store!)
427 };
428
429 "free.wifi" = {}; # Public wireless network
430 }
431 '';
432 };
433
434 userControlled = {
435 enable = mkOption {
436 type = types.bool;
437 default = false;
438 description = lib.mdDoc ''
439 Allow normal users to control wpa_supplicant through wpa_gui or wpa_cli.
440 This is useful for laptop users that switch networks a lot and don't want
441 to depend on a large package such as NetworkManager just to pick nearby
442 access points.
443
444 When using a declarative network specification you cannot persist any
445 settings via wpa_gui or wpa_cli.
446 '';
447 };
448
449 group = mkOption {
450 type = types.str;
451 default = "wheel";
452 example = "network";
453 description = lib.mdDoc "Members of this group can control wpa_supplicant.";
454 };
455 };
456
457 dbusControlled = mkOption {
458 type = types.bool;
459 default = lib.length cfg.interfaces < 2;
460 defaultText = literalExpression "length config.${opt.interfaces} < 2";
461 description = lib.mdDoc ''
462 Whether to enable the DBus control interface.
463 This is only needed when using NetworkManager or connman.
464 '';
465 };
466
467 extraConfig = mkOption {
468 type = types.str;
469 default = "";
470 example = ''
471 p2p_disabled=1
472 '';
473 description = lib.mdDoc ''
474 Extra lines appended to the configuration file.
475 See
476 {manpage}`wpa_supplicant.conf(5)`
477 for available options.
478 '';
479 };
480 };
481 };
482
483 config = mkIf cfg.enable {
484 assertions = flip mapAttrsToList cfg.networks (name: cfg: {
485 assertion = with cfg; count (x: x != null) [ psk pskRaw auth ] <= 1;
486 message = ''options networking.wireless."${name}".{psk,pskRaw,auth} are mutually exclusive'';
487 }) ++ [
488 {
489 assertion = length cfg.interfaces > 1 -> !cfg.dbusControlled;
490 message =
491 let daemon = if config.networking.networkmanager.enable then "NetworkManager" else
492 if config.services.connman.enable then "connman" else null;
493 n = toString (length cfg.interfaces);
494 in ''
495 It's not possible to run multiple wpa_supplicant instances with DBus support.
496 Note: you're seeing this error because `networking.wireless.interfaces` has
497 ${n} entries, implying an equal number of wpa_supplicant instances.
498 '' + optionalString (daemon != null) ''
499 You don't need to change `networking.wireless.interfaces` when using ${daemon}:
500 in this case the interfaces will be configured automatically for you.
501 '';
502 }
503 ];
504
505 hardware.wirelessRegulatoryDatabase = true;
506
507 environment.systemPackages = [ package ];
508 services.dbus.packages = optional cfg.dbusControlled package;
509
510 systemd.services =
511 if cfg.interfaces == []
512 then { wpa_supplicant = mkUnit null; }
513 else listToAttrs (map (i: nameValuePair "wpa_supplicant-${i}" (mkUnit i)) cfg.interfaces);
514
515 # Restart wpa_supplicant after resuming from sleep
516 powerManagement.resumeCommands = concatStringsSep "\n" (
517 optional (cfg.interfaces == []) "${systemctl} try-restart wpa_supplicant"
518 ++ map (i: "${systemctl} try-restart wpa_supplicant-${i}") cfg.interfaces
519 );
520
521 # Restart wpa_supplicant when a wlan device appears or disappears. This is
522 # only needed when an interface hasn't been specified by the user.
523 services.udev.extraRules = optionalString (cfg.interfaces == []) ''
524 ACTION=="add|remove", SUBSYSTEM=="net", ENV{DEVTYPE}=="wlan", \
525 RUN+="${systemctl} try-restart wpa_supplicant.service"
526 '';
527 };
528
529 meta.maintainers = with lib.maintainers; [ globin rnhmjoj ];
530}