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