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