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