at master 23 kB view raw
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}