at 23.05-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 ${pkgs.gawk}/bin/awk '{ 125 for(varname in ENVIRON) 126 gsub("@"varname"@", ENVIRON[varname]) 127 print 128 }' "${configFile}" > "${finalConfig}" 129 130 iface_args="-s ${optionalString cfg.dbusControlled "-u"} -D${cfg.driver} ${configStr}" 131 132 ${if iface == null then '' 133 # detect interfaces automatically 134 135 # check if there are no wireless interfaces 136 if ! find -H /sys/class/net/* -name wireless | grep -q .; then 137 # if so, wait until one appears 138 echo "Waiting for wireless interfaces" 139 grep -q '^ACTION=add' < <(stdbuf -oL -- udevadm monitor -s net/wlan -pu) 140 # Note: the above line has been carefully written: 141 # 1. The process substitution avoids udevadm hanging (after grep has quit) 142 # until it tries to write to the pipe again. Not even pipefail works here. 143 # 2. stdbuf is needed because udevadm output is buffered by default and grep 144 # may hang until more udev events enter the pipe. 145 fi 146 147 # add any interface found to the daemon arguments 148 for name in $(find -H /sys/class/net/* -name wireless | cut -d/ -f 5); do 149 echo "Adding interface $name" 150 args+="''${args:+ -N} -i$name $iface_args" 151 done 152 '' else '' 153 # add known interface to the daemon arguments 154 args="-i${iface} $iface_args" 155 ''} 156 157 # finally start daemon 158 exec wpa_supplicant $args 159 ''; 160 }; 161 162 systemctl = "/run/current-system/systemd/bin/systemctl"; 163 164in { 165 options = { 166 networking.wireless = { 167 enable = mkEnableOption (lib.mdDoc "wpa_supplicant"); 168 169 interfaces = mkOption { 170 type = types.listOf types.str; 171 default = []; 172 example = [ "wlan0" "wlan1" ]; 173 description = lib.mdDoc '' 174 The interfaces {command}`wpa_supplicant` will use. If empty, it will 175 automatically use all wireless interfaces. 176 177 ::: {.note} 178 A separate wpa_supplicant instance will be started for each interface. 179 ::: 180 ''; 181 }; 182 183 driver = mkOption { 184 type = types.str; 185 default = "nl80211,wext"; 186 description = lib.mdDoc "Force a specific wpa_supplicant driver."; 187 }; 188 189 allowAuxiliaryImperativeNetworks = mkEnableOption (lib.mdDoc "support for imperative & declarative networks") // { 190 description = lib.mdDoc '' 191 Whether to allow configuring networks "imperatively" (e.g. via 192 `wpa_supplicant_gui`) and declaratively via 193 [](#opt-networking.wireless.networks). 194 195 Please note that this adds a custom patch to `wpa_supplicant`. 196 ''; 197 }; 198 199 scanOnLowSignal = mkOption { 200 type = types.bool; 201 default = true; 202 description = lib.mdDoc '' 203 Whether to periodically scan for (better) networks when the signal of 204 the current one is low. This will make roaming between access points 205 faster, but will consume more power. 206 ''; 207 }; 208 209 fallbackToWPA2 = mkOption { 210 type = types.bool; 211 default = true; 212 description = lib.mdDoc '' 213 Whether to fall back to WPA2 authentication protocols if WPA3 failed. 214 This allows old wireless cards (that lack recent features required by 215 WPA3) to connect to mixed WPA2/WPA3 access points. 216 217 To avoid possible downgrade attacks, disable this options. 218 ''; 219 }; 220 221 environmentFile = mkOption { 222 type = types.nullOr types.path; 223 default = null; 224 example = "/run/secrets/wireless.env"; 225 description = lib.mdDoc '' 226 File consisting of lines of the form `varname=value` 227 to define variables for the wireless configuration. 228 229 See section "EnvironmentFile=" in {manpage}`systemd.exec(5)` for a syntax reference. 230 231 Secrets (PSKs, passwords, etc.) can be provided without adding them to 232 the world-readable Nix store by defining them in the environment file and 233 referring to them in option {option}`networking.wireless.networks` 234 with the syntax `@varname@`. Example: 235 236 ``` 237 # content of /run/secrets/wireless.env 238 PSK_HOME=mypassword 239 PASS_WORK=myworkpassword 240 ``` 241 242 ``` 243 # wireless-related configuration 244 networking.wireless.environmentFile = "/run/secrets/wireless.env"; 245 networking.wireless.networks = { 246 home.psk = "@PSK_HOME@"; 247 work.auth = ''' 248 eap=PEAP 249 identity="my-user@example.com" 250 password="@PASS_WORK@" 251 '''; 252 }; 253 ``` 254 ''; 255 }; 256 257 networks = mkOption { 258 type = types.attrsOf (types.submodule { 259 options = { 260 psk = mkOption { 261 type = types.nullOr types.str; 262 default = null; 263 description = lib.mdDoc '' 264 The network's pre-shared key in plaintext defaulting 265 to being a network without any authentication. 266 267 ::: {.warning} 268 Be aware that this will be written to the nix store 269 in plaintext! Use an environment variable instead. 270 ::: 271 272 ::: {.note} 273 Mutually exclusive with {var}`pskRaw`. 274 ::: 275 ''; 276 }; 277 278 pskRaw = mkOption { 279 type = types.nullOr types.str; 280 default = null; 281 description = lib.mdDoc '' 282 The network's pre-shared key in hex defaulting 283 to being a network without any authentication. 284 285 ::: {.warning} 286 Be aware that this will be written to the nix store 287 in plaintext! Use an environment variable instead. 288 ::: 289 290 ::: {.note} 291 Mutually exclusive with {var}`psk`. 292 ::: 293 ''; 294 }; 295 296 authProtocols = mkOption { 297 default = [ 298 # WPA2 and WPA3 299 "WPA-PSK" "WPA-EAP" "SAE" 300 # 802.11r variants of the above 301 "FT-PSK" "FT-EAP" "FT-SAE" 302 ]; 303 # The list can be obtained by running this command 304 # awk ' 305 # /^# key_mgmt: /{ run=1 } 306 # /^#$/{ run=0 } 307 # /^# [A-Z0-9-]{2,}/{ if(run){printf("\"%s\"\n", $2)} } 308 # ' /run/current-system/sw/share/doc/wpa_supplicant/wpa_supplicant.conf.example 309 type = types.listOf (types.enum [ 310 "WPA-PSK" 311 "WPA-EAP" 312 "IEEE8021X" 313 "NONE" 314 "WPA-NONE" 315 "FT-PSK" 316 "FT-EAP" 317 "FT-EAP-SHA384" 318 "WPA-PSK-SHA256" 319 "WPA-EAP-SHA256" 320 "SAE" 321 "FT-SAE" 322 "WPA-EAP-SUITE-B" 323 "WPA-EAP-SUITE-B-192" 324 "OSEN" 325 "FILS-SHA256" 326 "FILS-SHA384" 327 "FT-FILS-SHA256" 328 "FT-FILS-SHA384" 329 "OWE" 330 "DPP" 331 ]); 332 description = lib.mdDoc '' 333 The list of authentication protocols accepted by this network. 334 This corresponds to the `key_mgmt` option in wpa_supplicant. 335 ''; 336 }; 337 338 auth = mkOption { 339 type = types.nullOr types.str; 340 default = null; 341 example = '' 342 eap=PEAP 343 identity="user@example.com" 344 password="@EXAMPLE_PASSWORD@" 345 ''; 346 description = lib.mdDoc '' 347 Use this option to configure advanced authentication methods like EAP. 348 See 349 {manpage}`wpa_supplicant.conf(5)` 350 for example configurations. 351 352 ::: {.warning} 353 Be aware that this will be written to the nix store 354 in plaintext! Use an environment variable for secrets. 355 ::: 356 357 ::: {.note} 358 Mutually exclusive with {var}`psk` and 359 {var}`pskRaw`. 360 ::: 361 ''; 362 }; 363 364 hidden = mkOption { 365 type = types.bool; 366 default = false; 367 description = lib.mdDoc '' 368 Set this to `true` if the SSID of the network is hidden. 369 ''; 370 example = literalExpression '' 371 { echelon = { 372 hidden = true; 373 psk = "abcdefgh"; 374 }; 375 } 376 ''; 377 }; 378 379 priority = mkOption { 380 type = types.nullOr types.int; 381 default = null; 382 description = lib.mdDoc '' 383 By default, all networks will get same priority group (0). If some of the 384 networks are more desirable, this field can be used to change the order in 385 which wpa_supplicant goes through the networks when selecting a BSS. The 386 priority groups will be iterated in decreasing priority (i.e., the larger the 387 priority value, the sooner the network is matched against the scan results). 388 Within each priority group, networks will be selected based on security 389 policy, signal strength, etc. 390 ''; 391 }; 392 393 extraConfig = mkOption { 394 type = types.str; 395 default = ""; 396 example = '' 397 bssid_blacklist=02:11:22:33:44:55 02:22:aa:44:55:66 398 ''; 399 description = lib.mdDoc '' 400 Extra configuration lines appended to the network block. 401 See 402 {manpage}`wpa_supplicant.conf(5)` 403 for available options. 404 ''; 405 }; 406 407 }; 408 }); 409 description = lib.mdDoc '' 410 The network definitions to automatically connect to when 411 {command}`wpa_supplicant` is running. If this 412 parameter is left empty wpa_supplicant will use 413 /etc/wpa_supplicant.conf as the configuration file. 414 ''; 415 default = {}; 416 example = literalExpression '' 417 { echelon = { # SSID with no spaces or special characters 418 psk = "abcdefgh"; # (password will be written to /nix/store!) 419 }; 420 421 echelon = { # safe version of the above: read PSK from the 422 psk = "@PSK_ECHELON@"; # variable PSK_ECHELON, defined in environmentFile, 423 }; # this won't leak into /nix/store 424 425 "echelon's AP" = { # SSID with spaces and/or special characters 426 psk = "ijklmnop"; # (password will be written to /nix/store!) 427 }; 428 429 "free.wifi" = {}; # Public wireless network 430 } 431 ''; 432 }; 433 434 userControlled = { 435 enable = mkOption { 436 type = types.bool; 437 default = false; 438 description = lib.mdDoc '' 439 Allow normal users to control wpa_supplicant through wpa_gui or wpa_cli. 440 This is useful for laptop users that switch networks a lot and don't want 441 to depend on a large package such as NetworkManager just to pick nearby 442 access points. 443 444 When using a declarative network specification you cannot persist any 445 settings via wpa_gui or wpa_cli. 446 ''; 447 }; 448 449 group = mkOption { 450 type = types.str; 451 default = "wheel"; 452 example = "network"; 453 description = lib.mdDoc "Members of this group can control wpa_supplicant."; 454 }; 455 }; 456 457 dbusControlled = mkOption { 458 type = types.bool; 459 default = lib.length cfg.interfaces < 2; 460 defaultText = literalExpression "length config.${opt.interfaces} < 2"; 461 description = lib.mdDoc '' 462 Whether to enable the DBus control interface. 463 This is only needed when using NetworkManager or connman. 464 ''; 465 }; 466 467 extraConfig = mkOption { 468 type = types.str; 469 default = ""; 470 example = '' 471 p2p_disabled=1 472 ''; 473 description = lib.mdDoc '' 474 Extra lines appended to the configuration file. 475 See 476 {manpage}`wpa_supplicant.conf(5)` 477 for available options. 478 ''; 479 }; 480 }; 481 }; 482 483 config = mkIf cfg.enable { 484 assertions = flip mapAttrsToList cfg.networks (name: cfg: { 485 assertion = with cfg; count (x: x != null) [ psk pskRaw auth ] <= 1; 486 message = ''options networking.wireless."${name}".{psk,pskRaw,auth} are mutually exclusive''; 487 }) ++ [ 488 { 489 assertion = length cfg.interfaces > 1 -> !cfg.dbusControlled; 490 message = 491 let daemon = if config.networking.networkmanager.enable then "NetworkManager" else 492 if config.services.connman.enable then "connman" else null; 493 n = toString (length cfg.interfaces); 494 in '' 495 It's not possible to run multiple wpa_supplicant instances with DBus support. 496 Note: you're seeing this error because `networking.wireless.interfaces` has 497 ${n} entries, implying an equal number of wpa_supplicant instances. 498 '' + optionalString (daemon != null) '' 499 You don't need to change `networking.wireless.interfaces` when using ${daemon}: 500 in this case the interfaces will be configured automatically for you. 501 ''; 502 } 503 ]; 504 505 hardware.wirelessRegulatoryDatabase = true; 506 507 environment.systemPackages = [ package ]; 508 services.dbus.packages = optional cfg.dbusControlled package; 509 510 systemd.services = 511 if cfg.interfaces == [] 512 then { wpa_supplicant = mkUnit null; } 513 else listToAttrs (map (i: nameValuePair "wpa_supplicant-${i}" (mkUnit i)) cfg.interfaces); 514 515 # Restart wpa_supplicant after resuming from sleep 516 powerManagement.resumeCommands = concatStringsSep "\n" ( 517 optional (cfg.interfaces == []) "${systemctl} try-restart wpa_supplicant" 518 ++ map (i: "${systemctl} try-restart wpa_supplicant-${i}") cfg.interfaces 519 ); 520 521 # Restart wpa_supplicant when a wlan device appears or disappears. This is 522 # only needed when an interface hasn't been specified by the user. 523 services.udev.extraRules = optionalString (cfg.interfaces == []) '' 524 ACTION=="add|remove", SUBSYSTEM=="net", ENV{DEVTYPE}=="wlan", \ 525 RUN+="${systemctl} try-restart wpa_supplicant.service" 526 ''; 527 }; 528 529 meta.maintainers = with lib.maintainers; [ globin rnhmjoj ]; 530}