Merge #361424: refactor lib.packagesFromDirectoryRecursive (v2)
[NixPkgs.git] / nixos / modules / services / networking / wpa_supplicant.nix
blob536aebe4931c4f32d811300fe3903de7e3033767
2   config,
3   lib,
4   options,
5   pkgs,
6   utils,
7   ...
8 }:
10 with lib;
12 let
13   cfg = config.networking.wireless;
14   opt = options.networking.wireless;
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 != [ ];
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     };
36   # Creates a WPA2 fallback network
37   mkWPA2Fallback = opts: opts // { authProtocols = subtractLists wpa3Protocols opts.authProtocols; };
39   # Networks attrset as a list
40   networkList = mapAttrsToList (ssid: opts: opts // { inherit ssid; }) cfg.networks;
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;
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   );
65   configIsGenerated = with cfg; networks != { } || extraConfig != "" || userControlled.enable;
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";
74   # Creates a network block for wpa_supplicant.conf
75   mkNetwork =
76     opts:
77     let
78       quote = x: ''"${x}"'';
79       indent = x: "  " + x;
81       pskString = if opts.psk != null then quote opts.psk else opts.pskRaw;
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         ++ optional (opts.extraConfig != "") opts.extraConfig;
98     in
99     ''
100       network={
101       ${concatMapStringsSep "\n" indent options}
102       }
103     '';
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}";
121       after = deviceUnit;
122       before = [ "network.target" ];
123       wants = [ "network.target" ];
124       requires = deviceUnit;
125       wantedBy = [ "multi-user.target" ];
126       stopIfChanged = false;
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";
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         ''}
143         # ensure wpa_supplicant.conf exists, or the daemon will fail to start
144         ${optionalString cfg.allowAuxiliaryImperativeNetworks ''
145           touch /etc/wpa_supplicant.conf
146         ''}
148         iface_args="-s ${optionalString cfg.dbusControlled "-u"} -D${cfg.driver} ${configStr}"
150         ${
151           if iface == null then
152             ''
153               # detect interfaces automatically
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
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         }
180         # finally start daemon
181         exec wpa_supplicant $args
182       '';
183     };
185   systemctl = "/run/current-system/systemd/bin/systemctl";
189   options = {
190     networking.wireless = {
191       enable = mkEnableOption "wpa_supplicant";
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.
204           ::: {.note}
205           A separate wpa_supplicant instance will be started for each interface.
206           :::
207         '';
208       };
210       driver = mkOption {
211         type = types.str;
212         default = "nl80211,wext";
213         description = "Force a specific wpa_supplicant driver.";
214       };
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).
224             Please note that this adds a custom patch to `wpa_supplicant`.
225           '';
226         };
228       scanOnLowSignal = mkOption {
229         type = types.bool;
230         default = true;
231         description = ''
232           Whether to periodically scan for (better) networks when the signal of
233           the current one is low. This will make roaming between access points
234           faster, but will consume more power.
235         '';
236       };
238       fallbackToWPA2 = mkOption {
239         type = types.bool;
240         default = true;
241         description = ''
242           Whether to fall back to WPA2 authentication protocols if WPA3 failed.
243           This allows old wireless cards (that lack recent features required by
244           WPA3) to connect to mixed WPA2/WPA3 access points.
246           To avoid possible downgrade attacks, disable this options.
247         '';
248       };
250       secretsFile = mkOption {
251         type = types.nullOr types.path;
252         default = null;
253         example = "/run/secrets/wireless.conf";
254         description = ''
255           File consisting of lines of the form `varname=value`
256           to define variables for the wireless configuration.
258           Secrets (PSKs, passwords, etc.) can be provided without adding them to
259           the world-readable Nix store by defining them in the secrets file and
260           referring to them in option [](#opt-networking.wireless.networks)
261           with the syntax `ext:secretname`. Example:
263           ```
264           # content of /run/secrets/wireless.conf
265           psk_home=mypassword
266           psk_other=6a381cea59c7a2d6b30736ba0e6f397f7564a044bcdb7a327a1d16a1ed91b327
267           pass_work=myworkpassword
269           # wireless-related configuration
270           networking.wireless.secretsFile = "/run/secrets/wireless.conf";
271           networking.wireless.networks = {
272             home.pskRaw = "ext:psk_home";
273             other.pskRaw = "ext:psk_other";
274             work.auth = '''
275               eap=PEAP
276               identity="my-user@example.com"
277               password=ext:pass_work
278             ''';
279           };
280           ```
281         '';
282       };
284       networks = mkOption {
285         type = types.attrsOf (
286           types.submodule {
287             options = {
288               psk = mkOption {
289                 type = types.nullOr (types.strMatching "[[:print:]]{8,63}");
290                 default = null;
291                 description = ''
292                   The network's pre-shared key in plaintext defaulting
293                   to being a network without any authentication.
295                   ::: {.warning}
296                   Be aware that this will be written to the Nix store
297                   in plaintext! Use {var}`pskRaw` with an external
298                   reference to keep it safe.
299                   :::
301                   ::: {.note}
302                   Mutually exclusive with {var}`pskRaw`.
303                   :::
304                 '';
305               };
307               pskRaw = mkOption {
308                 type = types.nullOr (types.strMatching "([[:xdigit:]]{64})|(ext:[^=]+)");
309                 default = null;
310                 example = "ext:name_of_the_secret_here";
311                 description = ''
312                   Either the raw pre-shared key in hexadecimal format
313                   or the name of the secret (as defined inside
314                   [](#opt-networking.wireless.secretsFile) and prefixed
315                   with `ext:`) containing the network pre-shared key.
317                   ::: {.warning}
318                   Be aware that this will be written to the Nix store
319                   in plaintext! Always use an external reference.
320                   :::
322                   ::: {.note}
323                   The external secret can be either the plaintext
324                   passphrase or the raw pre-shared key.
325                   :::
327                   ::: {.note}
328                   Mutually exclusive with {var}`psk` and {var}`auth`.
329                   :::
330                 '';
331               };
333               authProtocols = mkOption {
334                 default = [
335                   # WPA2 and WPA3
336                   "WPA-PSK"
337                   "WPA-EAP"
338                   "SAE"
339                   # 802.11r variants of the above
340                   "FT-PSK"
341                   "FT-EAP"
342                   "FT-SAE"
343                 ];
344                 # The list can be obtained by running this command
345                 # awk '
346                 #   /^# key_mgmt: /{ run=1 }
347                 #   /^#$/{ run=0 }
348                 #   /^# [A-Z0-9-]{2,}/{ if(run){printf("\"%s\"\n", $2)} }
349                 # ' /run/current-system/sw/share/doc/wpa_supplicant/wpa_supplicant.conf.example
350                 type = types.listOf (
351                   types.enum [
352                     "WPA-PSK"
353                     "WPA-EAP"
354                     "IEEE8021X"
355                     "NONE"
356                     "WPA-NONE"
357                     "FT-PSK"
358                     "FT-EAP"
359                     "FT-EAP-SHA384"
360                     "WPA-PSK-SHA256"
361                     "WPA-EAP-SHA256"
362                     "SAE"
363                     "FT-SAE"
364                     "WPA-EAP-SUITE-B"
365                     "WPA-EAP-SUITE-B-192"
366                     "OSEN"
367                     "FILS-SHA256"
368                     "FILS-SHA384"
369                     "FT-FILS-SHA256"
370                     "FT-FILS-SHA384"
371                     "OWE"
372                     "DPP"
373                   ]
374                 );
375                 description = ''
376                   The list of authentication protocols accepted by this network.
377                   This corresponds to the `key_mgmt` option in wpa_supplicant.
378                 '';
379               };
381               auth = mkOption {
382                 type = types.nullOr types.str;
383                 default = null;
384                 example = ''
385                   eap=PEAP
386                   identity="user@example.com"
387                   password=ext:example_password
388                 '';
389                 description = ''
390                   Use this option to configure advanced authentication methods
391                   like EAP. See {manpage}`wpa_supplicant.conf(5)` for example
392                   configurations.
394                   ::: {.warning}
395                   Be aware that this will be written to the Nix store
396                   in plaintext! Use an external reference like
397                   `ext:secretname` for secrets.
398                   :::
400                   ::: {.note}
401                   Mutually exclusive with {var}`psk` and {var}`pskRaw`.
402                   :::
403                 '';
404               };
406               hidden = mkOption {
407                 type = types.bool;
408                 default = false;
409                 description = ''
410                   Set this to `true` if the SSID of the network is hidden.
411                 '';
412                 example = literalExpression ''
413                   { echelon = {
414                       hidden = true;
415                       psk = "abcdefgh";
416                     };
417                   }
418                 '';
419               };
421               priority = mkOption {
422                 type = types.nullOr types.int;
423                 default = null;
424                 description = ''
425                   By default, all networks will get same priority group (0). If
426                   some of the networks are more desirable, this field can be used
427                   to change the order in which wpa_supplicant goes through the
428                   networks when selecting a BSS. The priority groups will be
429                   iterated in decreasing priority (i.e., the larger the priority
430                   value, the sooner the network is matched against the scan
431                   results). Within each priority group, networks will be selected
432                   based on security policy, signal strength, etc.
433                 '';
434               };
436               extraConfig = mkOption {
437                 type = types.str;
438                 default = "";
439                 example = ''
440                   bssid_blacklist=02:11:22:33:44:55 02:22:aa:44:55:66
441                 '';
442                 description = ''
443                   Extra configuration lines appended to the network block.
444                   See {manpage}`wpa_supplicant.conf(5)` for available options.
445                 '';
446               };
448             };
449           }
450         );
451         description = ''
452           The network definitions to automatically connect to when
453            {command}`wpa_supplicant` is running. If this
454            parameter is left empty wpa_supplicant will use
455           /etc/wpa_supplicant.conf as the configuration file.
456         '';
457         default = { };
458         example = literalExpression ''
459           { echelon = {                   # SSID with no spaces or special characters
460               psk = "abcdefgh";           # (password will be written to /nix/store!)
461             };
463             echelon = {                   # safe version of the above: read PSK from the
464               pskRaw = "ext:psk_echelon"; # variable psk_echelon, defined in secretsFile,
465             };                            # this won't leak into /nix/store
467             "echelon's AP" = {            # SSID with spaces and/or special characters
468                psk = "ijklmnop";          # (password will be written to /nix/store!)
469             };
471             "free.wifi" = {};             # Public wireless network
472           }
473         '';
474       };
476       userControlled = {
477         enable = mkOption {
478           type = types.bool;
479           default = false;
480           description = ''
481             Allow normal users to control wpa_supplicant through wpa_gui or wpa_cli.
482             This is useful for laptop users that switch networks a lot and don't want
483             to depend on a large package such as NetworkManager just to pick nearby
484             access points.
486             When using a declarative network specification you cannot persist any
487             settings via wpa_gui or wpa_cli.
488           '';
489         };
491         group = mkOption {
492           type = types.str;
493           default = "wheel";
494           example = "network";
495           description = "Members of this group can control wpa_supplicant.";
496         };
497       };
499       dbusControlled = mkOption {
500         type = types.bool;
501         default = lib.length cfg.interfaces < 2;
502         defaultText = literalExpression "length config.${opt.interfaces} < 2";
503         description = ''
504           Whether to enable the DBus control interface.
505           This is only needed when using NetworkManager or connman.
506         '';
507       };
509       extraConfig = mkOption {
510         type = types.str;
511         default = "";
512         example = ''
513           p2p_disabled=1
514         '';
515         description = ''
516           Extra lines appended to the configuration file.
517           See
518           {manpage}`wpa_supplicant.conf(5)`
519           for available options.
520         '';
521       };
522     };
523   };
525   imports = [
526     (mkRemovedOptionModule [ "networking" "wireless" "environmentFile" ] ''
527       Secrets are now handled by the `networking.wireless.secretsFile` and
528       `networking.wireless.networks.<name>.pskRaw` options.
529       The change is motivated by a mechanism recently added by wpa_supplicant
530       itself to separate secrets from configuration, making the previous
531       method obsolete.
533       The syntax of the `secretsFile` is the same as before, except the
534       values are interpreted literally, unlike environment variables.
535       To update, remove quotes or character escapes, if necessary, and
536       apply the following changes to your configuration:
537         {
538           home.psk = "@psk_home@";          →  home.pskRaw = "ext:psk_home";
539           other.pskRaw = "@psk_other@";     →  other.pskRaw = "ext:psk_other";
540           work.auth = '''
541             eap=PEAP
542             identity="my-user@example.com"
543             password=@pass_work@            →  password=ext:pass_work
544           ''';
545         }
546     '')
547   ];
549   config = mkIf cfg.enable {
550     assertions =
551       flip mapAttrsToList cfg.networks (
552         name: cfg: {
553           assertion =
554             with cfg;
555             count (x: x != null) [
556               psk
557               pskRaw
558               auth
559             ] <= 1;
560           message = ''options networking.wireless."${name}".{psk,pskRaw,auth} are mutually exclusive'';
561         }
562       )
563       ++ [
564         {
565           assertion = length cfg.interfaces > 1 -> !cfg.dbusControlled;
566           message =
567             let
568               daemon =
569                 if config.networking.networkmanager.enable then
570                   "NetworkManager"
571                 else if config.services.connman.enable then
572                   "connman"
573                 else
574                   null;
575               n = toString (length cfg.interfaces);
576             in
577             ''
578               It's not possible to run multiple wpa_supplicant instances with DBus support.
579               Note: you're seeing this error because `networking.wireless.interfaces` has
580               ${n} entries, implying an equal number of wpa_supplicant instances.
581             ''
582             + optionalString (daemon != null) ''
583               You don't need to change `networking.wireless.interfaces` when using ${daemon}:
584               in this case the interfaces will be configured automatically for you.
585             '';
586         }
587       ];
589     hardware.wirelessRegulatoryDatabase = true;
591     environment.systemPackages = [ pkgs.wpa_supplicant ];
592     services.dbus.packages = optional cfg.dbusControlled pkgs.wpa_supplicant;
594     systemd.services =
595       if cfg.interfaces == [ ] then
596         { wpa_supplicant = mkUnit null; }
597       else
598         listToAttrs (map (i: nameValuePair "wpa_supplicant-${i}" (mkUnit i)) cfg.interfaces);
600     # Restart wpa_supplicant after resuming from sleep
601     powerManagement.resumeCommands = concatStringsSep "\n" (
602       optional (cfg.interfaces == [ ]) "${systemctl} try-restart wpa_supplicant"
603       ++ map (i: "${systemctl} try-restart wpa_supplicant-${i}") cfg.interfaces
604     );
606     # Restart wpa_supplicant when a wlan device appears or disappears. This is
607     # only needed when an interface hasn't been specified by the user.
608     services.udev.extraRules = optionalString (cfg.interfaces == [ ]) ''
609       ACTION=="add|remove", SUBSYSTEM=="net", ENV{DEVTYPE}=="wlan", \
610       RUN+="${systemctl} try-restart wpa_supplicant.service"
611     '';
612   };
614   meta.maintainers = with lib.maintainers; [ rnhmjoj ];