at 25.11-pre 22 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 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}