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