at 24.11-pre 60 kB view raw
1{ config, lib, pkgs, utils, ... }: 2# All hope abandon ye who enter here. hostapd's configuration 3# format is ... special, and you won't be able to infer any 4# of their assumptions from just reading the "documentation" 5# (i.e. the example config). Assume footguns at all points - 6# to make informed decisions you will probably need to look 7# at hostapd's code. You have been warned, proceed with care. 8let 9 inherit 10 (lib) 11 attrNames 12 attrValues 13 concatLists 14 concatMap 15 concatMapStrings 16 concatStringsSep 17 count 18 escapeShellArg 19 filter 20 flip 21 generators 22 getAttr 23 hasPrefix 24 imap0 25 isInt 26 isString 27 length 28 literalExpression 29 maintainers 30 mapAttrsToList 31 mkDefault 32 mkEnableOption 33 mkIf 34 mkOption 35 mkPackageOption 36 mkRemovedOptionModule 37 optional 38 optionalAttrs 39 optionalString 40 optionals 41 singleton 42 stringLength 43 toLower 44 types 45 unique 46 ; 47 48 cfg = config.services.hostapd; 49 50 extraSettingsFormat = { 51 type = let 52 singleAtom = types.oneOf [ types.bool types.int types.str ]; 53 atom = types.either singleAtom (types.listOf singleAtom) // { 54 description = "atom (bool, int or string) or a list of them for duplicate keys"; 55 }; 56 in types.attrsOf atom; 57 58 generate = name: value: pkgs.writeText name (generators.toKeyValue { 59 listsAsDuplicateKeys = true; 60 mkKeyValue = generators.mkKeyValueDefault { 61 mkValueString = v: 62 if isInt v then toString v 63 else if isString v then v 64 else if true == v then "1" 65 else if false == v then "0" 66 else throw "unsupported type ${builtins.typeOf v}: ${(generators.toPretty {}) v}"; 67 } "="; 68 } value); 69 }; 70 71 # Generates the header for a single BSS (i.e. WiFi network) 72 writeBssHeader = radio: bss: bssIdx: pkgs.writeText "hostapd-radio-${radio}-bss-${bss}.conf" '' 73 ''\n''\n# BSS ${toString bssIdx}: ${bss} 74 ################################ 75 76 ${if bssIdx == 0 then "interface" else "bss"}=${bss} 77 ''; 78 79 makeRadioRuntimeFiles = radio: radioCfg: 80 pkgs.writeShellScript "make-hostapd-${radio}-files" ('' 81 set -euo pipefail 82 83 hostapd_config_file=/run/hostapd/${escapeShellArg radio}.hostapd.conf 84 rm -f "$hostapd_config_file" 85 cat > "$hostapd_config_file" <<EOF 86 # Radio base configuration: ${radio} 87 ################################ 88 89 EOF 90 91 cat ${escapeShellArg (extraSettingsFormat.generate "hostapd-radio-${radio}-extra.conf" radioCfg.settings)} >> "$hostapd_config_file" 92 ${concatMapStrings (script: "${script} \"$hostapd_config_file\"\n") (attrValues radioCfg.dynamicConfigScripts)} 93 '' 94 + concatMapStrings (x: "${x}\n") (imap0 (i: f: f i) 95 (mapAttrsToList (bss: bssCfg: bssIdx: '' 96 ''\n# BSS configuration: ${bss} 97 98 mac_allow_file=/run/hostapd/${escapeShellArg bss}.mac.allow 99 rm -f "$mac_allow_file" 100 touch "$mac_allow_file" 101 102 mac_deny_file=/run/hostapd/${escapeShellArg bss}.mac.deny 103 rm -f "$mac_deny_file" 104 touch "$mac_deny_file" 105 106 cat ${writeBssHeader radio bss bssIdx} >> "$hostapd_config_file" 107 cat ${escapeShellArg (extraSettingsFormat.generate "hostapd-radio-${radio}-bss-${bss}-extra.conf" bssCfg.settings)} >> "$hostapd_config_file" 108 ${concatMapStrings (script: "${script} \"$hostapd_config_file\" \"$mac_allow_file\" \"$mac_deny_file\"\n") (attrValues bssCfg.dynamicConfigScripts)} 109 '') radioCfg.networks))); 110 111 runtimeConfigFiles = mapAttrsToList (radio: _: "/run/hostapd/${radio}.hostapd.conf") cfg.radios; 112in { 113 meta.maintainers = with maintainers; [ oddlama ]; 114 115 options = { 116 services.hostapd = { 117 enable = mkEnableOption '' 118 hostapd, a user space daemon for access point and 119 authentication servers. It implements IEEE 802.11 access point management, 120 IEEE 802.1X/WPA/WPA2/EAP Authenticators, RADIUS client, EAP server, and RADIUS 121 authentication server 122 ''; 123 124 package = mkPackageOption pkgs "hostapd" {}; 125 126 radios = mkOption { 127 default = {}; 128 example = literalExpression '' 129 { 130 # Simple 2.4GHz AP 131 wlp2s0 = { 132 # countryCode = "US"; 133 networks.wlp2s0 = { 134 ssid = "AP 1"; 135 authentication.saePasswords = [{ password = "a flakey password"; }]; # Use saePasswordsFile if possible. 136 }; 137 }; 138 139 # WiFi 5 (5GHz) with two advertised networks 140 wlp3s0 = { 141 band = "5g"; 142 channel = 0; # Enable automatic channel selection (ACS). Use only if your hardware supports it. 143 # countryCode = "US"; 144 networks.wlp3s0 = { 145 ssid = "My AP"; 146 authentication.saePasswords = [{ password = "a flakey password"; }]; # Use saePasswordsFile if possible. 147 }; 148 networks.wlp3s0-1 = { 149 ssid = "Open AP with WiFi5"; 150 authentication.mode = "none"; 151 }; 152 }; 153 154 # Legacy WPA2 example 155 wlp4s0 = { 156 # countryCode = "US"; 157 networks.wlp4s0 = { 158 ssid = "AP 2"; 159 authentication = { 160 mode = "wpa2-sha256"; 161 wpaPassword = "a flakey password"; # Use wpaPasswordFile if possible. 162 }; 163 }; 164 }; 165 } 166 ''; 167 description = '' 168 This option allows you to define APs for one or multiple physical radios. 169 At least one radio must be specified. 170 171 For each radio, hostapd requires a separate logical interface (like wlp3s0, wlp3s1, ...). 172 A default interface is usually be created automatically by your system, but to use 173 multiple radios of a single device, it may be required to create additional logical interfaces 174 for example by using {option}`networking.wlanInterfaces`. 175 176 Each physical radio can only support a single hardware-mode that is configured via 177 ({option}`services.hostapd.radios.<radio>.band`). To create a dual-band 178 or tri-band AP, you will have to use a device that has multiple physical radios 179 and supports configuring multiple APs (Refer to valid interface combinations in 180 {command}`iw list`). 181 ''; 182 type = types.attrsOf (types.submodule (radioSubmod: { 183 options = { 184 driver = mkOption { 185 default = "nl80211"; 186 example = "none"; 187 type = types.str; 188 description = '' 189 The driver {command}`hostapd` will use. 190 {var}`nl80211` is used with all Linux mac80211 drivers. 191 {var}`none` is used if building a standalone RADIUS server that does 192 not control any wireless/wired driver. 193 Most applications will probably use the default. 194 ''; 195 }; 196 197 noScan = mkOption { 198 type = types.bool; 199 default = false; 200 description = '' 201 Disables scan for overlapping BSSs in HT40+/- mode. 202 Caution: turning this on will likely violate regulatory requirements! 203 ''; 204 }; 205 206 countryCode = mkOption { 207 default = null; 208 example = "US"; 209 type = types.nullOr types.str; 210 description = '' 211 Country code (ISO/IEC 3166-1). Used to set regulatory domain. 212 Set as needed to indicate country in which device is operating. 213 This can limit available channels and transmit power. 214 These two octets are used as the first two octets of the Country String 215 (dot11CountryString). 216 217 Setting this will force you to also enable IEEE 802.11d and IEEE 802.11h. 218 219 IEEE 802.11d: This advertises the countryCode and the set of allowed channels 220 and transmit power levels based on the regulatory limits. 221 222 IEEE802.11h: This enables radar detection and DFS (Dynamic Frequency Selection) 223 support if available. DFS support is required on outdoor 5 GHz channels in most 224 countries of the world. 225 ''; 226 }; 227 228 band = mkOption { 229 default = "2g"; 230 type = types.enum ["2g" "5g" "6g" "60g"]; 231 description = '' 232 Specifies the frequency band to use, possible values are 2g for 2.4 GHz, 233 5g for 5 GHz, 6g for 6 GHz and 60g for 60 GHz. 234 ''; 235 }; 236 237 channel = mkOption { 238 default = 7; 239 example = 11; 240 type = types.int; 241 description = '' 242 The channel to operate on. Use 0 to enable ACS (Automatic Channel Selection). 243 Beware that not every device supports ACS in which case {command}`hostapd` 244 will fail to start. 245 ''; 246 }; 247 248 settings = mkOption { 249 default = {}; 250 example = { acs_exclude_dfs = true; }; 251 type = types.submodule { 252 freeformType = extraSettingsFormat.type; 253 }; 254 description = '' 255 Extra configuration options to put at the end of global initialization, before defining BSSs. 256 To find out which options are global and which are per-bss you have to read hostapd's source code, 257 which is non-trivial and not documented otherwise. 258 259 Lists will be converted to multiple definitions of the same key, and booleans to 0/1. 260 Otherwise, the inputs are not modified or checked for correctness. 261 ''; 262 }; 263 264 dynamicConfigScripts = mkOption { 265 default = {}; 266 type = types.attrsOf types.path; 267 example = literalExpression '' 268 { 269 exampleDynamicConfig = pkgs.writeShellScript "dynamic-config" ''' 270 HOSTAPD_CONFIG=$1 271 272 cat >> "$HOSTAPD_CONFIG" << EOF 273 # Add some dynamically generated statements here, 274 # for example based on the physical adapter in use 275 EOF 276 '''; 277 } 278 ''; 279 description = '' 280 All of these scripts will be executed in lexicographical order before hostapd 281 is started, right after the global segment was generated and may dynamically 282 append global options the generated configuration file. 283 284 The first argument will point to the configuration file that you may append to. 285 ''; 286 }; 287 288 #### IEEE 802.11n (WiFi 4) related configuration 289 290 wifi4 = { 291 enable = mkOption { 292 default = true; 293 type = types.bool; 294 description = '' 295 Enables support for IEEE 802.11n (WiFi 4, HT). 296 This is enabled by default, since the vase majority of devices 297 are expected to support this. 298 ''; 299 }; 300 301 capabilities = mkOption { 302 type = types.listOf types.str; 303 default = ["HT40" "HT40-" "SHORT-GI-20" "SHORT-GI-40"]; 304 example = ["LDPC" "HT40+" "HT40-" "GF" "SHORT-GI-20" "SHORT-GI-40" "TX-STBC" "RX-STBC1"]; 305 description = '' 306 HT (High Throughput) capabilities given as a list of flags. 307 Please refer to the hostapd documentation for allowed values and 308 only set values supported by your physical adapter. 309 310 The default contains common values supported by most adapters. 311 ''; 312 }; 313 314 require = mkOption { 315 default = false; 316 type = types.bool; 317 description = "Require stations (clients) to support WiFi 4 (HT) and disassociate them if they don't."; 318 }; 319 }; 320 321 #### IEEE 802.11ac (WiFi 5) related configuration 322 323 wifi5 = { 324 enable = mkOption { 325 default = true; 326 type = types.bool; 327 description = "Enables support for IEEE 802.11ac (WiFi 5, VHT)"; 328 }; 329 330 capabilities = mkOption { 331 type = types.listOf types.str; 332 default = []; 333 example = ["SHORT-GI-80" "TX-STBC-2BY1" "RX-STBC-1" "RX-ANTENNA-PATTERN" "TX-ANTENNA-PATTERN"]; 334 description = '' 335 VHT (Very High Throughput) capabilities given as a list of flags. 336 Please refer to the hostapd documentation for allowed values and 337 only set values supported by your physical adapter. 338 ''; 339 }; 340 341 require = mkOption { 342 default = false; 343 type = types.bool; 344 description = "Require stations (clients) to support WiFi 5 (VHT) and disassociate them if they don't."; 345 }; 346 347 operatingChannelWidth = mkOption { 348 default = "20or40"; 349 type = types.enum ["20or40" "80" "160" "80+80"]; 350 apply = x: 351 getAttr x { 352 "20or40" = 0; 353 "80" = 1; 354 "160" = 2; 355 "80+80" = 3; 356 }; 357 description = '' 358 Determines the operating channel width for VHT. 359 360 - {var}`"20or40"`: 20 or 40 MHz operating channel width 361 - {var}`"80"`: 80 MHz channel width 362 - {var}`"160"`: 160 MHz channel width 363 - {var}`"80+80"`: 80+80 MHz channel width 364 ''; 365 }; 366 }; 367 368 #### IEEE 802.11ax (WiFi 6) related configuration 369 370 wifi6 = { 371 enable = mkOption { 372 default = false; 373 type = types.bool; 374 description = "Enables support for IEEE 802.11ax (WiFi 6, HE)"; 375 }; 376 377 require = mkOption { 378 default = false; 379 type = types.bool; 380 description = "Require stations (clients) to support WiFi 6 (HE) and disassociate them if they don't."; 381 }; 382 383 singleUserBeamformer = mkOption { 384 default = false; 385 type = types.bool; 386 description = "HE single user beamformer support"; 387 }; 388 389 singleUserBeamformee = mkOption { 390 default = false; 391 type = types.bool; 392 description = "HE single user beamformee support"; 393 }; 394 395 multiUserBeamformer = mkOption { 396 default = false; 397 type = types.bool; 398 description = "HE multi user beamformee support"; 399 }; 400 401 operatingChannelWidth = mkOption { 402 default = "20or40"; 403 type = types.enum ["20or40" "80" "160" "80+80"]; 404 apply = x: 405 getAttr x { 406 "20or40" = 0; 407 "80" = 1; 408 "160" = 2; 409 "80+80" = 3; 410 }; 411 description = '' 412 Determines the operating channel width for HE. 413 414 - {var}`"20or40"`: 20 or 40 MHz operating channel width 415 - {var}`"80"`: 80 MHz channel width 416 - {var}`"160"`: 160 MHz channel width 417 - {var}`"80+80"`: 80+80 MHz channel width 418 ''; 419 }; 420 }; 421 422 #### IEEE 802.11be (WiFi 7) related configuration 423 424 wifi7 = { 425 enable = mkOption { 426 default = false; 427 type = types.bool; 428 description = '' 429 Enables support for IEEE 802.11be (WiFi 7, EHT). This is currently experimental 430 and requires you to manually enable CONFIG_IEEE80211BE when building hostapd. 431 ''; 432 }; 433 434 singleUserBeamformer = mkOption { 435 default = false; 436 type = types.bool; 437 description = "EHT single user beamformer support"; 438 }; 439 440 singleUserBeamformee = mkOption { 441 default = false; 442 type = types.bool; 443 description = "EHT single user beamformee support"; 444 }; 445 446 multiUserBeamformer = mkOption { 447 default = false; 448 type = types.bool; 449 description = "EHT multi user beamformee support"; 450 }; 451 452 operatingChannelWidth = mkOption { 453 default = "20or40"; 454 type = types.enum ["20or40" "80" "160" "80+80"]; 455 apply = x: 456 getAttr x { 457 "20or40" = 0; 458 "80" = 1; 459 "160" = 2; 460 "80+80" = 3; 461 }; 462 description = '' 463 Determines the operating channel width for EHT. 464 465 - {var}`"20or40"`: 20 or 40 MHz operating channel width 466 - {var}`"80"`: 80 MHz channel width 467 - {var}`"160"`: 160 MHz channel width 468 - {var}`"80+80"`: 80+80 MHz channel width 469 ''; 470 }; 471 }; 472 473 #### BSS definitions 474 475 networks = mkOption { 476 default = {}; 477 example = literalExpression '' 478 { 479 wlp2s0 = { 480 ssid = "Primary advertised network"; 481 authentication.saePasswords = [{ password = "a flakey password"; }]; # Use saePasswordsFile if possible. 482 }; 483 wlp2s0-1 = { 484 ssid = "Secondary advertised network (Open)"; 485 authentication.mode = "none"; 486 }; 487 } 488 ''; 489 description = '' 490 This defines a BSS, colloquially known as a WiFi network. 491 You have to specify at least one. 492 ''; 493 type = types.attrsOf (types.submodule (bssSubmod: { 494 options = { 495 logLevel = mkOption { 496 default = 2; 497 type = types.int; 498 description = '' 499 Levels (minimum value for logged events): 500 0 = verbose debugging 501 1 = debugging 502 2 = informational messages 503 3 = notification 504 4 = warning 505 ''; 506 }; 507 508 group = mkOption { 509 default = "wheel"; 510 example = "network"; 511 type = types.str; 512 description = '' 513 Members of this group can access the control socket for this interface. 514 ''; 515 }; 516 517 utf8Ssid = mkOption { 518 default = true; 519 type = types.bool; 520 description = "Whether the SSID is to be interpreted using UTF-8 encoding."; 521 }; 522 523 ssid = mkOption { 524 example = " cool "; 525 type = types.str; 526 description = "SSID to be used in IEEE 802.11 management frames."; 527 }; 528 529 bssid = mkOption { 530 type = types.nullOr types.str; 531 default = null; 532 example = "11:22:33:44:55:66"; 533 description = '' 534 Specifies the BSSID for this BSS. Usually determined automatically, 535 but for now you have to manually specify them when using multiple BSS. 536 Try assigning related addresses from the locally administered MAC address ranges, 537 by reusing the hardware address but replacing the second nibble with 2, 6, A or E. 538 (e.g. if real address is `XX:XX:XX:XX:XX`, try `X2:XX:XX:XX:XX:XX`, `X6:XX:XX:XX:XX:XX`, ... 539 for the second, third, ... BSS) 540 ''; 541 }; 542 543 macAcl = mkOption { 544 default = "deny"; 545 type = types.enum ["deny" "allow" "radius"]; 546 apply = x: 547 getAttr x { 548 "deny" = 0; 549 "allow" = 1; 550 "radius" = 2; 551 }; 552 description = '' 553 Station MAC address -based authentication. The following modes are available: 554 555 - {var}`"deny"`: Allow unless listed in {option}`macDeny` (default) 556 - {var}`"allow"`: Deny unless listed in {option}`macAllow` 557 - {var}`"radius"`: Use external radius server, but check both {option}`macAllow` and {option}`macDeny` first 558 559 Please note that this kind of access control requires a driver that uses 560 hostapd to take care of management frame processing and as such, this can be 561 used with driver=hostap or driver=nl80211, but not with driver=atheros. 562 ''; 563 }; 564 565 macAllow = mkOption { 566 type = types.listOf types.str; 567 default = []; 568 example = ["11:22:33:44:55:66"]; 569 description = '' 570 Specifies the MAC addresses to allow if {option}`macAcl` is set to {var}`"allow"` or {var}`"radius"`. 571 These values will be world-readable in the Nix store. Values will automatically be merged with 572 {option}`macAllowFile` if necessary. 573 ''; 574 }; 575 576 macAllowFile = mkOption { 577 type = types.nullOr types.path; 578 default = null; 579 description = '' 580 Specifies a file containing the MAC addresses to allow if {option}`macAcl` is set to {var}`"allow"` or {var}`"radius"`. 581 The file should contain exactly one MAC address per line. Comments and empty lines are ignored, 582 only lines starting with a valid MAC address will be considered (e.g. `11:22:33:44:55:66`) and 583 any content after the MAC address is ignored. 584 ''; 585 }; 586 587 macDeny = mkOption { 588 type = types.listOf types.str; 589 default = []; 590 example = ["11:22:33:44:55:66"]; 591 description = '' 592 Specifies the MAC addresses to deny if {option}`macAcl` is set to {var}`"deny"` or {var}`"radius"`. 593 These values will be world-readable in the Nix store. Values will automatically be merged with 594 {option}`macDenyFile` if necessary. 595 ''; 596 }; 597 598 macDenyFile = mkOption { 599 type = types.nullOr types.path; 600 default = null; 601 description = '' 602 Specifies a file containing the MAC addresses to deny if {option}`macAcl` is set to {var}`"deny"` or {var}`"radius"`. 603 The file should contain exactly one MAC address per line. Comments and empty lines are ignored, 604 only lines starting with a valid MAC address will be considered (e.g. `11:22:33:44:55:66`) and 605 any content after the MAC address is ignored. 606 ''; 607 }; 608 609 ignoreBroadcastSsid = mkOption { 610 default = "disabled"; 611 type = types.enum ["disabled" "empty" "clear"]; 612 apply = x: 613 getAttr x { 614 "disabled" = 0; 615 "empty" = 1; 616 "clear" = 2; 617 }; 618 description = '' 619 Send empty SSID in beacons and ignore probe request frames that do not 620 specify full SSID, i.e., require stations to know SSID. Note that this does 621 not increase security, since your clients will then broadcast the SSID instead, 622 which can increase congestion. 623 624 - {var}`"disabled"`: Advertise ssid normally. 625 - {var}`"empty"`: send empty (length=0) SSID in beacon and ignore probe request for broadcast SSID 626 - {var}`"clear"`: clear SSID (ASCII 0), but keep the original length (this may be required with some 627 legacy clients that do not support empty SSID) and ignore probe requests for broadcast SSID. Only 628 use this if empty does not work with your clients. 629 ''; 630 }; 631 632 apIsolate = mkOption { 633 default = false; 634 type = types.bool; 635 description = '' 636 Isolate traffic between stations (clients) and prevent them from 637 communicating with each other. 638 ''; 639 }; 640 641 settings = mkOption { 642 default = {}; 643 example = { multi_ap = true; }; 644 type = types.submodule { 645 freeformType = extraSettingsFormat.type; 646 }; 647 description = '' 648 Extra configuration options to put at the end of this BSS's defintion in the 649 hostapd.conf for the associated interface. To find out which options are global 650 and which are per-bss you have to read hostapd's source code, which is non-trivial 651 and not documented otherwise. 652 653 Lists will be converted to multiple definitions of the same key, and booleans to 0/1. 654 Otherwise, the inputs are not modified or checked for correctness. 655 ''; 656 }; 657 658 dynamicConfigScripts = mkOption { 659 default = {}; 660 type = types.attrsOf types.path; 661 example = literalExpression '' 662 { 663 exampleDynamicConfig = pkgs.writeShellScript "dynamic-config" ''' 664 HOSTAPD_CONFIG=$1 665 # These always exist, but may or may not be used depending on the actual configuration 666 MAC_ALLOW_FILE=$2 667 MAC_DENY_FILE=$3 668 669 cat >> "$HOSTAPD_CONFIG" << EOF 670 # Add some dynamically generated statements here 671 EOF 672 '''; 673 } 674 ''; 675 description = '' 676 All of these scripts will be executed in lexicographical order before hostapd 677 is started, right after the bss segment was generated and may dynamically 678 append bss options to the generated configuration file. 679 680 The first argument will point to the configuration file that you may append to. 681 The second and third argument will point to this BSS's MAC allow and MAC deny file respectively. 682 ''; 683 }; 684 685 #### IEEE 802.11i (WPA) configuration 686 687 authentication = { 688 mode = mkOption { 689 default = "wpa3-sae"; 690 type = types.enum ["none" "wpa2-sha1" "wpa2-sha256" "wpa3-sae-transition" "wpa3-sae"]; 691 description = '' 692 Selects the authentication mode for this AP. 693 694 - {var}`"none"`: Don't configure any authentication. This will disable wpa alltogether 695 and create an open AP. Use {option}`settings` together with this option if you 696 want to configure the authentication manually. Any password options will still be 697 effective, if set. 698 - {var}`"wpa2-sha1"`: Not recommended. WPA2-Personal using HMAC-SHA1. Passwords are set 699 using {option}`wpaPassword` or preferably by {option}`wpaPasswordFile` or {option}`wpaPskFile`. 700 - {var}`"wpa2-sha256"`: WPA2-Personal using HMAC-SHA256 (IEEE 802.11i/RSN). Passwords are set 701 using {option}`wpaPassword` or preferably by {option}`wpaPasswordFile` or {option}`wpaPskFile`. 702 - {var}`"wpa3-sae-transition"`: Use WPA3-Personal (SAE) if possible, otherwise fallback 703 to WPA2-SHA256. Only use if necessary and switch to the newer WPA3-SAE when possible. 704 You will have to specify both {option}`wpaPassword` and {option}`saePasswords` (or one of their alternatives). 705 - {var}`"wpa3-sae"`: Use WPA3-Personal (SAE). This is currently the recommended way to 706 setup a secured WiFi AP (as of March 2023) and therefore the default. Passwords are set 707 using either {option}`saePasswords` or preferably {option}`saePasswordsFile`. 708 ''; 709 }; 710 711 pairwiseCiphers = mkOption { 712 default = ["CCMP"]; 713 example = ["CCMP-256" "GCMP-256"]; 714 type = types.listOf types.str; 715 description = '' 716 Set of accepted cipher suites (encryption algorithms) for pairwise keys (unicast packets). 717 By default this allows just CCMP, which is the only commonly supported secure option. 718 Use {option}`enableRecommendedPairwiseCiphers` to also enable newer recommended ciphers. 719 720 Please refer to the hostapd documentation for allowed values. Generally, only 721 CCMP or GCMP modes should be considered safe options. Most devices support CCMP while 722 GCMP is often only available with devices supporting WiFi 5 (IEEE 802.11ac) or higher. 723 ''; 724 }; 725 726 enableRecommendedPairwiseCiphers = mkOption { 727 default = false; 728 example = true; 729 type = types.bool; 730 description = '' 731 Additionally enable the recommended set of pairwise ciphers. 732 This enables newer secure ciphers, additionally to those defined in {option}`pairwiseCiphers`. 733 You will have to test whether your hardware supports these by trial-and-error, because 734 even if `iw list` indicates hardware support, your driver might not expose it. 735 736 Beware {command}`hostapd` will most likely not return a useful error message in case 737 this is enabled despite the driver or hardware not supporting the newer ciphers. 738 Look out for messages like `Failed to set beacon parameters`. 739 ''; 740 }; 741 742 wpaPassword = mkOption { 743 default = null; 744 example = "a flakey password"; 745 type = types.nullOr types.str; 746 description = '' 747 Sets the password for WPA-PSK that will be converted to the pre-shared key. 748 The password length must be in the range [8, 63] characters. While some devices 749 may allow arbitrary characters (such as UTF-8) to be used, but the standard specifies 750 that each character in the passphrase must be an ASCII character in the range [0x20, 0x7e] 751 (IEEE Std. 802.11i-2004, Annex H.4.1). Use emojis at your own risk. 752 753 Not used when {option}`mode` is {var}`"wpa3-sae"`. 754 755 Warning: This password will get put into a world-readable file in the Nix store! 756 Using {option}`wpaPasswordFile` or {option}`wpaPskFile` instead is recommended. 757 ''; 758 }; 759 760 wpaPasswordFile = mkOption { 761 default = null; 762 type = types.nullOr types.path; 763 description = '' 764 Sets the password for WPA-PSK. Follows the same rules as {option}`wpaPassword`, 765 but reads the password from the given file to prevent the password from being 766 put into the Nix store. 767 768 Not used when {option}`mode` is {var}`"wpa3-sae"`. 769 ''; 770 }; 771 772 wpaPskFile = mkOption { 773 default = null; 774 type = types.nullOr types.path; 775 description = '' 776 Sets the password(s) for WPA-PSK. Similar to {option}`wpaPasswordFile`, 777 but additionally allows specifying multiple passwords, and some other options. 778 779 Each line, except for empty lines and lines starting with #, must contain a 780 MAC address and either a 64-hex-digit PSK or a password separated with a space. 781 The password must follow the same rules as outlined in {option}`wpaPassword`. 782 The special MAC address `00:00:00:00:00:00` can be used to configure PSKs 783 that any client can use. 784 785 An optional key identifier can be added by prefixing the line with `keyid=<keyid_string>` 786 An optional VLAN ID can be specified by prefixing the line with `vlanid=<VLAN ID>`. 787 An optional WPS tag can be added by prefixing the line with `wps=<0/1>` (default: 0). 788 Any matching entry with that tag will be used when generating a PSK for a WPS Enrollee 789 instead of generating a new random per-Enrollee PSK. 790 791 Not used when {option}`mode` is {var}`"wpa3-sae"`. 792 ''; 793 }; 794 795 saePasswords = mkOption { 796 default = []; 797 example = literalExpression '' 798 [ 799 # Any client may use these passwords 800 { password = "Wi-Figure it out"; } 801 { password = "second password for everyone"; mac = "ff:ff:ff:ff:ff:ff"; } 802 803 # Only the client with MAC-address 11:22:33:44:55:66 can use this password 804 { password = "sekret pazzword"; mac = "11:22:33:44:55:66"; } 805 ] 806 ''; 807 description = '' 808 Sets allowed passwords for WPA3-SAE. 809 810 The last matching (based on peer MAC address and identifier) entry is used to 811 select which password to use. An empty string has the special meaning of 812 removing all previously added entries. 813 814 Warning: These entries will get put into a world-readable file in 815 the Nix store! Using {option}`saePasswordFile` instead is recommended. 816 817 Not used when {option}`mode` is {var}`"wpa2-sha1"` or {var}`"wpa2-sha256"`. 818 ''; 819 type = types.listOf (types.submodule { 820 options = { 821 password = mkOption { 822 example = "a flakey password"; 823 type = types.str; 824 description = '' 825 The password for this entry. SAE technically imposes no restrictions on 826 password length or character set. But due to limitations of {command}`hostapd`'s 827 config file format, a true newline character cannot be parsed. 828 829 Warning: This password will get put into a world-readable file in 830 the Nix store! Using {option}`wpaPasswordFile` or {option}`wpaPskFile` is recommended. 831 ''; 832 }; 833 834 mac = mkOption { 835 default = null; 836 example = "11:22:33:44:55:66"; 837 type = types.nullOr types.str; 838 description = '' 839 If this attribute is not included, or if is set to the wildcard address (`ff:ff:ff:ff:ff:ff`), 840 the entry is available for any station (client) to use. If a specific peer MAC address is included, 841 only a station with that MAC address is allowed to use the entry. 842 ''; 843 }; 844 845 vlanid = mkOption { 846 default = null; 847 example = 1; 848 type = types.nullOr types.int; 849 description = "If this attribute is given, all clients using this entry will get tagged with the given VLAN ID."; 850 }; 851 852 pk = mkOption { 853 default = null; 854 example = ""; 855 type = types.nullOr types.str; 856 description = '' 857 If this attribute is given, SAE-PK will be enabled for this connection. 858 This prevents evil-twin attacks, but a public key is required additionally to connect. 859 (Essentially adds pubkey authentication such that the client can verify identity of the AP) 860 ''; 861 }; 862 863 id = mkOption { 864 default = null; 865 example = ""; 866 type = types.nullOr types.str; 867 description = '' 868 If this attribute is given with non-zero length, it will set the password identifier 869 for this entry. It can then only be used with that identifier. 870 ''; 871 }; 872 }; 873 }); 874 }; 875 876 saePasswordsFile = mkOption { 877 default = null; 878 type = types.nullOr types.path; 879 description = '' 880 Sets the password for WPA3-SAE. Follows the same rules as {option}`saePasswords`, 881 but reads the entries from the given file to prevent them from being 882 put into the Nix store. 883 884 One entry per line, empty lines and lines beginning with # will be ignored. 885 Each line must match the following format, although the order of optional 886 parameters doesn't matter: 887 `<password>[|mac=<peer mac>][|vlanid=<VLAN ID>][|pk=<m:ECPrivateKey-base64>][|id=<identifier>]` 888 889 Not used when {option}`mode` is {var}`"wpa2-sha1"` or {var}`"wpa2-sha256"`. 890 ''; 891 }; 892 893 saeAddToMacAllow = mkOption { 894 type = types.bool; 895 default = false; 896 description = '' 897 If set, all sae password entries that have a non-wildcard MAC associated to 898 them will additionally be used to populate the MAC allow list. This is 899 additional to any entries set via {option}`macAllow` or {option}`macAllowFile`. 900 ''; 901 }; 902 }; 903 }; 904 905 config = let 906 bssCfg = bssSubmod.config; 907 pairwiseCiphers = 908 concatStringsSep " " (unique (bssCfg.authentication.pairwiseCiphers 909 ++ optionals bssCfg.authentication.enableRecommendedPairwiseCiphers ["CCMP" "CCMP-256" "GCMP" "GCMP-256"])); 910 in { 911 settings = { 912 ssid = bssCfg.ssid; 913 utf8_ssid = bssCfg.utf8Ssid; 914 915 logger_syslog = mkDefault (-1); 916 logger_syslog_level = bssCfg.logLevel; 917 logger_stdout = mkDefault (-1); 918 logger_stdout_level = bssCfg.logLevel; 919 ctrl_interface = mkDefault "/run/hostapd"; 920 ctrl_interface_group = bssCfg.group; 921 922 macaddr_acl = bssCfg.macAcl; 923 924 ignore_broadcast_ssid = bssCfg.ignoreBroadcastSsid; 925 926 # IEEE 802.11i (authentication) related configuration 927 # Encrypt management frames to protect against deauthentication and similar attacks 928 ieee80211w = mkDefault 1; 929 sae_require_mfp = mkDefault 1; 930 931 # Only allow WPA by default and disable insecure WEP 932 auth_algs = mkDefault 1; 933 # Always enable QoS, which is required for 802.11n and above 934 wmm_enabled = mkDefault true; 935 ap_isolate = bssCfg.apIsolate; 936 937 sae_password = flip map bssCfg.authentication.saePasswords ( 938 entry: 939 entry.password 940 + optionalString (entry.mac != null) "|mac=${entry.mac}" 941 + optionalString (entry.vlanid != null) "|vlanid=${toString entry.vlanid}" 942 + optionalString (entry.pk != null) "|pk=${entry.pk}" 943 + optionalString (entry.id != null) "|id=${entry.id}" 944 ); 945 } // optionalAttrs (bssCfg.bssid != null) { 946 bssid = bssCfg.bssid; 947 } // optionalAttrs (bssCfg.macAllow != [] || bssCfg.macAllowFile != null || bssCfg.authentication.saeAddToMacAllow) { 948 accept_mac_file = "/run/hostapd/${bssCfg._module.args.name}.mac.allow"; 949 } // optionalAttrs (bssCfg.macDeny != [] || bssCfg.macDenyFile != null) { 950 deny_mac_file = "/run/hostapd/${bssCfg._module.args.name}.mac.deny"; 951 } // optionalAttrs (bssCfg.authentication.mode == "none") { 952 wpa = mkDefault 0; 953 } // optionalAttrs (bssCfg.authentication.mode == "wpa3-sae") { 954 wpa = 2; 955 wpa_key_mgmt = "SAE"; 956 # Derive PWE using both hunting-and-pecking loop and hash-to-element 957 sae_pwe = 2; 958 # Prevent downgrade attacks by indicating to clients that they should 959 # disable any transition modes from now on. 960 transition_disable = "0x01"; 961 } // optionalAttrs (bssCfg.authentication.mode == "wpa3-sae-transition") { 962 wpa = 2; 963 wpa_key_mgmt = "WPA-PSK-SHA256 SAE"; 964 } // optionalAttrs (bssCfg.authentication.mode == "wpa2-sha1") { 965 wpa = 2; 966 wpa_key_mgmt = "WPA-PSK"; 967 } // optionalAttrs (bssCfg.authentication.mode == "wpa2-sha256") { 968 wpa = 2; 969 wpa_key_mgmt = "WPA-PSK-SHA256"; 970 } // optionalAttrs (bssCfg.authentication.mode != "none") { 971 wpa_pairwise = pairwiseCiphers; 972 rsn_pairwise = pairwiseCiphers; 973 } // optionalAttrs (bssCfg.authentication.wpaPassword != null) { 974 wpa_passphrase = bssCfg.authentication.wpaPassword; 975 } // optionalAttrs (bssCfg.authentication.wpaPskFile != null) { 976 wpa_psk_file = toString bssCfg.authentication.wpaPskFile; 977 }; 978 979 dynamicConfigScripts = let 980 # All MAC addresses from SAE entries that aren't the wildcard address 981 saeMacs = filter (mac: mac != null && (toLower mac) != "ff:ff:ff:ff:ff:ff") (map (x: x.mac) bssCfg.authentication.saePasswords); 982 in { 983 "20-addMacAllow" = mkIf (bssCfg.macAllow != []) (pkgs.writeShellScript "add-mac-allow" '' 984 MAC_ALLOW_FILE=$2 985 cat >> "$MAC_ALLOW_FILE" <<EOF 986 ${concatStringsSep "\n" bssCfg.macAllow} 987 EOF 988 ''); 989 "20-addMacAllowFile" = mkIf (bssCfg.macAllowFile != null) (pkgs.writeShellScript "add-mac-allow-file" '' 990 MAC_ALLOW_FILE=$2 991 grep -Eo '^([0-9A-Fa-f]{2}[:]){5}([0-9A-Fa-f]{2})' ${escapeShellArg bssCfg.macAllowFile} >> "$MAC_ALLOW_FILE" 992 ''); 993 "20-addMacAllowFromSae" = mkIf (bssCfg.authentication.saeAddToMacAllow && saeMacs != []) (pkgs.writeShellScript "add-mac-allow-from-sae" '' 994 MAC_ALLOW_FILE=$2 995 cat >> "$MAC_ALLOW_FILE" <<EOF 996 ${concatStringsSep "\n" saeMacs} 997 EOF 998 ''); 999 # Populate mac allow list from saePasswordsFile 1000 # (filter for lines with mac=; exclude commented lines; filter for real mac-addresses; strip mac=) 1001 "20-addMacAllowFromSaeFile" = mkIf (bssCfg.authentication.saeAddToMacAllow && bssCfg.authentication.saePasswordsFile != null) (pkgs.writeShellScript "add-mac-allow-from-sae-file" '' 1002 MAC_ALLOW_FILE=$2 1003 grep mac= ${escapeShellArg bssCfg.authentication.saePasswordsFile} \ 1004 | grep -v '\s*#' \ 1005 | grep -Eo 'mac=([0-9A-Fa-f]{2}[:]){5}([0-9A-Fa-f]{2})' \ 1006 | sed 's|^mac=||' >> "$MAC_ALLOW_FILE" 1007 ''); 1008 "20-addMacDeny" = mkIf (bssCfg.macDeny != []) (pkgs.writeShellScript "add-mac-deny" '' 1009 MAC_DENY_FILE=$3 1010 cat >> "$MAC_DENY_FILE" <<EOF 1011 ${concatStringsSep "\n" bssCfg.macDeny} 1012 EOF 1013 ''); 1014 "20-addMacDenyFile" = mkIf (bssCfg.macDenyFile != null) (pkgs.writeShellScript "add-mac-deny-file" '' 1015 MAC_DENY_FILE=$3 1016 grep -Eo '^([0-9A-Fa-f]{2}[:]){5}([0-9A-Fa-f]{2})' ${escapeShellArg bssCfg.macDenyFile} >> "$MAC_DENY_FILE" 1017 ''); 1018 # Add wpa_passphrase from file 1019 "20-wpaPasswordFile" = mkIf (bssCfg.authentication.wpaPasswordFile != null) (pkgs.writeShellScript "wpa-password-file" '' 1020 HOSTAPD_CONFIG_FILE=$1 1021 cat >> "$HOSTAPD_CONFIG_FILE" <<EOF 1022 wpa_passphrase=$(cat ${escapeShellArg bssCfg.authentication.wpaPasswordFile}) 1023 EOF 1024 ''); 1025 # Add sae passwords from file 1026 "20-saePasswordsFile" = mkIf (bssCfg.authentication.saePasswordsFile != null) (pkgs.writeShellScript "sae-passwords-file" '' 1027 HOSTAPD_CONFIG_FILE=$1 1028 grep -v '\s*#' ${escapeShellArg bssCfg.authentication.saePasswordsFile} \ 1029 | sed 's/^/sae_password=/' >> "$HOSTAPD_CONFIG_FILE" 1030 ''); 1031 }; 1032 }; 1033 })); 1034 }; 1035 }; 1036 1037 config.settings = let 1038 radioCfg = radioSubmod.config; 1039 in { 1040 driver = radioCfg.driver; 1041 hw_mode = { 1042 "2g" = "g"; 1043 "5g" = "a"; 1044 "6g" = "a"; 1045 "60g" = "ad"; 1046 }.${radioCfg.band}; 1047 channel = radioCfg.channel; 1048 noscan = radioCfg.noScan; 1049 } // optionalAttrs (radioCfg.countryCode != null) { 1050 country_code = radioCfg.countryCode; 1051 # IEEE 802.11d: Limit to frequencies allowed in country 1052 ieee80211d = true; 1053 # IEEE 802.11h: Enable radar detection and DFS (Dynamic Frequency Selection) 1054 ieee80211h = true; 1055 } // optionalAttrs radioCfg.wifi4.enable { 1056 # IEEE 802.11n (WiFi 4) related configuration 1057 ieee80211n = true; 1058 require_ht = radioCfg.wifi4.require; 1059 ht_capab = concatMapStrings (x: "[${x}]") radioCfg.wifi4.capabilities; 1060 } // optionalAttrs radioCfg.wifi5.enable { 1061 # IEEE 802.11ac (WiFi 5) related configuration 1062 ieee80211ac = true; 1063 require_vht = radioCfg.wifi5.require; 1064 vht_oper_chwidth = radioCfg.wifi5.operatingChannelWidth; 1065 vht_capab = concatMapStrings (x: "[${x}]") radioCfg.wifi5.capabilities; 1066 } // optionalAttrs radioCfg.wifi6.enable { 1067 # IEEE 802.11ax (WiFi 6) related configuration 1068 ieee80211ax = true; 1069 require_he = mkIf radioCfg.wifi6.require true; 1070 he_oper_chwidth = radioCfg.wifi6.operatingChannelWidth; 1071 he_su_beamformer = radioCfg.wifi6.singleUserBeamformer; 1072 he_su_beamformee = radioCfg.wifi6.singleUserBeamformee; 1073 he_mu_beamformer = radioCfg.wifi6.multiUserBeamformer; 1074 } // optionalAttrs radioCfg.wifi7.enable { 1075 # IEEE 802.11be (WiFi 7) related configuration 1076 ieee80211be = true; 1077 eht_oper_chwidth = radioCfg.wifi7.operatingChannelWidth; 1078 eht_su_beamformer = radioCfg.wifi7.singleUserBeamformer; 1079 eht_su_beamformee = radioCfg.wifi7.singleUserBeamformee; 1080 eht_mu_beamformer = radioCfg.wifi7.multiUserBeamformer; 1081 }; 1082 })); 1083 }; 1084 }; 1085 }; 1086 1087 imports = let 1088 renamedOptionMessage = message: '' 1089 ${message} 1090 Refer to the documentation of `services.hostapd.radios` for an example and more information. 1091 ''; 1092 in [ 1093 (mkRemovedOptionModule ["services" "hostapd" "interface"] 1094 (renamedOptionMessage "All other options for this interface are now set via `services.hostapd.radios.«interface».*`.")) 1095 1096 (mkRemovedOptionModule ["services" "hostapd" "driver"] 1097 (renamedOptionMessage "It has been replaced by `services.hostapd.radios.«interface».driver`.")) 1098 (mkRemovedOptionModule ["services" "hostapd" "noScan"] 1099 (renamedOptionMessage "It has been replaced by `services.hostapd.radios.«interface».noScan`.")) 1100 (mkRemovedOptionModule ["services" "hostapd" "countryCode"] 1101 (renamedOptionMessage "It has been replaced by `services.hostapd.radios.«interface».countryCode`.")) 1102 (mkRemovedOptionModule ["services" "hostapd" "hwMode"] 1103 (renamedOptionMessage "It has been replaced by `services.hostapd.radios.«interface».band`.")) 1104 (mkRemovedOptionModule ["services" "hostapd" "channel"] 1105 (renamedOptionMessage "It has been replaced by `services.hostapd.radios.«interface».channel`.")) 1106 (mkRemovedOptionModule ["services" "hostapd" "extraConfig"] 1107 (renamedOptionMessage '' 1108 It has been replaced by `services.hostapd.radios.«interface».settings` and 1109 `services.hostapd.radios.«interface».networks.«network».settings` respectively 1110 for per-radio and per-network extra configuration. The module now supports a lot more 1111 options inherently, so please re-check whether using settings is still necessary.'')) 1112 1113 (mkRemovedOptionModule ["services" "hostapd" "logLevel"] 1114 (renamedOptionMessage "It has been replaced by `services.hostapd.radios.«interface».networks.«network».logLevel`.")) 1115 (mkRemovedOptionModule ["services" "hostapd" "group"] 1116 (renamedOptionMessage "It has been replaced by `services.hostapd.radios.«interface».networks.«network».group`.")) 1117 (mkRemovedOptionModule ["services" "hostapd" "ssid"] 1118 (renamedOptionMessage "It has been replaced by `services.hostapd.radios.«interface».networks.«network».ssid`.")) 1119 1120 (mkRemovedOptionModule ["services" "hostapd" "wpa"] 1121 (renamedOptionMessage "It has been replaced by `services.hostapd.radios.«interface».networks.«network».authentication.mode`.")) 1122 (mkRemovedOptionModule ["services" "hostapd" "wpaPassphrase"] 1123 (renamedOptionMessage '' 1124 It has been replaced by `services.hostapd.radios.«interface».networks.«network».authentication.wpaPassword`. 1125 While upgrading your config, please consider using the newer SAE authentication scheme 1126 and one of the new `passwordFile`-like options to avoid putting the password into the world readable nix-store.'')) 1127 ]; 1128 1129 config = mkIf cfg.enable { 1130 assertions = 1131 [ 1132 { 1133 assertion = cfg.radios != {}; 1134 message = "At least one radio must be configured with hostapd!"; 1135 } 1136 ] 1137 # Radio warnings 1138 ++ (concatLists (mapAttrsToList ( 1139 radio: radioCfg: 1140 [ 1141 { 1142 assertion = radioCfg.networks != {}; 1143 message = "hostapd radio ${radio}: At least one network must be configured!"; 1144 } 1145 # XXX: There could be many more useful assertions about (band == xy) -> ensure other required settings. 1146 # see https://github.com/openwrt/openwrt/blob/539cb5389d9514c99ec1f87bd4465f77c7ed9b93/package/kernel/mac80211/files/lib/netifd/wireless/mac80211.sh#L158 1147 { 1148 assertion = length (filter (bss: bss == radio) (attrNames radioCfg.networks)) == 1; 1149 message = ''hostapd radio ${radio}: Exactly one network must be named like the radio, for reasons internal to hostapd.''; 1150 } 1151 { 1152 assertion = (radioCfg.wifi4.enable && builtins.elem "HT40-" radioCfg.wifi4.capabilities) -> radioCfg.channel != 0; 1153 message = ''hostapd radio ${radio}: using ACS (channel = 0) together with HT40- (wifi4.capabilities) is unsupported by hostapd''; 1154 } 1155 ] 1156 # BSS warnings 1157 ++ (concatLists (mapAttrsToList (bss: bssCfg: let 1158 auth = bssCfg.authentication; 1159 countWpaPasswordDefinitions = count (x: x != null) [ 1160 auth.wpaPassword 1161 auth.wpaPasswordFile 1162 auth.wpaPskFile 1163 ]; 1164 in [ 1165 { 1166 assertion = hasPrefix radio bss; 1167 message = "hostapd radio ${radio} bss ${bss}: The bss (network) name ${bss} is invalid. It must be prefixed by the radio name for reasons internal to hostapd. A valid name would be e.g. ${radio}, ${radio}-1, ..."; 1168 } 1169 { 1170 assertion = (length (attrNames radioCfg.networks) > 1) -> (bssCfg.bssid != null); 1171 message = ''hostapd radio ${radio} bss ${bss}: bssid must be specified manually (for now) since this radio uses multiple BSS.''; 1172 } 1173 { 1174 assertion = countWpaPasswordDefinitions <= 1; 1175 message = ''hostapd radio ${radio} bss ${bss}: must use at most one WPA password option (wpaPassword, wpaPasswordFile, wpaPskFile)''; 1176 } 1177 { 1178 assertion = auth.wpaPassword != null -> (stringLength auth.wpaPassword >= 8 && stringLength auth.wpaPassword <= 63); 1179 message = ''hostapd radio ${radio} bss ${bss}: uses a wpaPassword of invalid length (must be in [8,63]).''; 1180 } 1181 { 1182 assertion = auth.saePasswords == [] || auth.saePasswordsFile == null; 1183 message = ''hostapd radio ${radio} bss ${bss}: must use only one SAE password option (saePasswords or saePasswordsFile)''; 1184 } 1185 { 1186 assertion = auth.mode == "wpa3-sae" -> (auth.saePasswords != [] || auth.saePasswordsFile != null); 1187 message = ''hostapd radio ${radio} bss ${bss}: uses WPA3-SAE which requires defining a sae password option''; 1188 } 1189 { 1190 assertion = auth.mode == "wpa3-sae-transition" -> (auth.saePasswords != [] || auth.saePasswordsFile != null) && countWpaPasswordDefinitions == 1; 1191 message = ''hostapd radio ${radio} bss ${bss}: uses WPA3-SAE in transition mode requires defining both a wpa password option and a sae password option''; 1192 } 1193 { 1194 assertion = (auth.mode == "wpa2-sha1" || auth.mode == "wpa2-sha256") -> countWpaPasswordDefinitions == 1; 1195 message = ''hostapd radio ${radio} bss ${bss}: uses WPA2-PSK which requires defining a wpa password option''; 1196 } 1197 ]) 1198 radioCfg.networks)) 1199 ) 1200 cfg.radios)); 1201 1202 environment.systemPackages = [cfg.package]; 1203 1204 systemd.services.hostapd = { 1205 description = "IEEE 802.11 Host Access-Point Daemon"; 1206 1207 path = [cfg.package]; 1208 after = map (radio: "sys-subsystem-net-devices-${utils.escapeSystemdPath radio}.device") (attrNames cfg.radios); 1209 bindsTo = map (radio: "sys-subsystem-net-devices-${utils.escapeSystemdPath radio}.device") (attrNames cfg.radios); 1210 wantedBy = ["multi-user.target"]; 1211 1212 # Create merged configuration and acl files for each radio (and their bss's) prior to starting 1213 preStart = concatStringsSep "\n" (mapAttrsToList makeRadioRuntimeFiles cfg.radios); 1214 1215 serviceConfig = { 1216 ExecStart = "${cfg.package}/bin/hostapd ${concatStringsSep " " runtimeConfigFiles}"; 1217 Restart = "always"; 1218 ExecReload = "${pkgs.coreutils}/bin/kill -HUP $MAINPID"; 1219 RuntimeDirectory = "hostapd"; 1220 1221 # Hardening 1222 LockPersonality = true; 1223 MemoryDenyWriteExecute = true; 1224 DevicePolicy = "closed"; 1225 DeviceAllow = "/dev/rfkill rw"; 1226 NoNewPrivileges = true; 1227 PrivateUsers = false; # hostapd requires true root access. 1228 PrivateTmp = true; 1229 ProtectClock = true; 1230 ProtectControlGroups = true; 1231 ProtectHome = true; 1232 ProtectHostname = true; 1233 ProtectKernelLogs = true; 1234 ProtectKernelModules = true; 1235 ProtectKernelTunables = true; 1236 ProtectProc = "invisible"; 1237 ProcSubset = "pid"; 1238 ProtectSystem = "strict"; 1239 RestrictAddressFamilies = [ 1240 "AF_INET" 1241 "AF_INET6" 1242 "AF_NETLINK" 1243 "AF_UNIX" 1244 "AF_PACKET" 1245 ]; 1246 RestrictNamespaces = true; 1247 RestrictRealtime = true; 1248 RestrictSUIDSGID = true; 1249 SystemCallArchitectures = "native"; 1250 SystemCallFilter = [ 1251 "@system-service" 1252 "~@privileged" 1253 "@chown" 1254 ]; 1255 UMask = "0077"; 1256 }; 1257 }; 1258 }; 1259}