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