at 24.11-pre 20 kB view raw
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}