at 23.11-pre 19 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 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}