at master 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.ints.unsigned; 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.ints.between 0 4; 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.ints.unsigned; 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 ssid = bssCfg.ssid; 1035 utf8_ssid = bssCfg.utf8Ssid; 1036 1037 logger_syslog = mkDefault (-1); 1038 logger_syslog_level = bssCfg.logLevel; 1039 logger_stdout = mkDefault (-1); 1040 logger_stdout_level = bssCfg.logLevel; 1041 ctrl_interface = mkDefault "/run/hostapd"; 1042 ctrl_interface_group = bssCfg.group; 1043 1044 macaddr_acl = bssCfg.macAcl; 1045 1046 ignore_broadcast_ssid = bssCfg.ignoreBroadcastSsid; 1047 1048 # IEEE 802.11i (authentication) related configuration 1049 # Encrypt management frames to protect against deauthentication and similar attacks 1050 ieee80211w = mkDefault 1; 1051 sae_require_mfp = mkDefault 1; 1052 1053 # Only allow WPA by default and disable insecure WEP 1054 auth_algs = mkDefault 1; 1055 # Always enable QoS, which is required for 802.11n and above 1056 wmm_enabled = mkDefault true; 1057 ap_isolate = bssCfg.apIsolate; 1058 } 1059 // optionalAttrs (bssCfg.bssid != null) { 1060 bssid = bssCfg.bssid; 1061 } 1062 // 1063 optionalAttrs 1064 (bssCfg.macAllow != [ ] || bssCfg.macAllowFile != null || bssCfg.authentication.saeAddToMacAllow) 1065 { 1066 accept_mac_file = "/run/hostapd/${bssCfg._module.args.name}.mac.allow"; 1067 } 1068 // optionalAttrs (bssCfg.macDeny != [ ] || bssCfg.macDenyFile != null) { 1069 deny_mac_file = "/run/hostapd/${bssCfg._module.args.name}.mac.deny"; 1070 } 1071 // optionalAttrs (bssCfg.authentication.mode == "none") { 1072 wpa = mkDefault 0; 1073 } 1074 // optionalAttrs (bssCfg.authentication.mode == "wpa3-sae") { 1075 wpa = 2; 1076 wpa_key_mgmt = "SAE"; 1077 # Derive PWE using both hunting-and-pecking loop and hash-to-element 1078 sae_pwe = 2; 1079 # Prevent downgrade attacks by indicating to clients that they should 1080 # disable any transition modes from now on. 1081 transition_disable = "0x01"; 1082 } 1083 // optionalAttrs (bssCfg.authentication.mode == "wpa3-sae-transition") { 1084 wpa = 2; 1085 wpa_key_mgmt = "WPA-PSK-SHA256 SAE"; 1086 } 1087 // optionalAttrs (bssCfg.authentication.mode == "wpa2-sha1") { 1088 wpa = 2; 1089 wpa_key_mgmt = "WPA-PSK"; 1090 } 1091 // optionalAttrs (bssCfg.authentication.mode == "wpa2-sha256") { 1092 wpa = 2; 1093 wpa_key_mgmt = "WPA-PSK-SHA256"; 1094 } 1095 // optionalAttrs (bssCfg.authentication.mode != "none") { 1096 wpa_pairwise = pairwiseCiphers; 1097 rsn_pairwise = pairwiseCiphers; 1098 } 1099 // optionalAttrs (bssCfg.authentication.wpaPassword != null) { 1100 wpa_passphrase = bssCfg.authentication.wpaPassword; 1101 } 1102 // optionalAttrs (bssCfg.authentication.wpaPskFile != null) { 1103 wpa_psk_file = toString bssCfg.authentication.wpaPskFile; 1104 }; 1105 1106 dynamicConfigScripts = 1107 let 1108 # All MAC addresses from SAE entries that aren't the wildcard address 1109 saeMacs = filter (mac: mac != null && (toLower mac) != "ff:ff:ff:ff:ff:ff") ( 1110 map (x: x.mac) bssCfg.authentication.saePasswords 1111 ); 1112 in 1113 { 1114 "20-addMacAllow" = mkIf (bssCfg.macAllow != [ ]) ( 1115 pkgs.writeShellScript "add-mac-allow" '' 1116 MAC_ALLOW_FILE=$2 1117 cat >> "$MAC_ALLOW_FILE" <<EOF 1118 ${concatStringsSep "\n" bssCfg.macAllow} 1119 EOF 1120 '' 1121 ); 1122 "20-addMacAllowFile" = mkIf (bssCfg.macAllowFile != null) ( 1123 pkgs.writeShellScript "add-mac-allow-file" '' 1124 MAC_ALLOW_FILE=$2 1125 grep -Eo '^([0-9A-Fa-f]{2}[:]){5}([0-9A-Fa-f]{2})' ${escapeShellArg bssCfg.macAllowFile} >> "$MAC_ALLOW_FILE" 1126 '' 1127 ); 1128 "20-addMacAllowFromSae" = mkIf (bssCfg.authentication.saeAddToMacAllow && saeMacs != [ ]) ( 1129 pkgs.writeShellScript "add-mac-allow-from-sae" '' 1130 MAC_ALLOW_FILE=$2 1131 cat >> "$MAC_ALLOW_FILE" <<EOF 1132 ${concatStringsSep "\n" saeMacs} 1133 EOF 1134 '' 1135 ); 1136 # Populate mac allow list from saePasswordsFile 1137 # (filter for lines with mac=; exclude commented lines; filter for real mac-addresses; strip mac=) 1138 "20-addMacAllowFromSaeFile" = 1139 mkIf (bssCfg.authentication.saeAddToMacAllow && bssCfg.authentication.saePasswordsFile != null) 1140 ( 1141 pkgs.writeShellScript "add-mac-allow-from-sae-file" '' 1142 MAC_ALLOW_FILE=$2 1143 grep mac= ${escapeShellArg bssCfg.authentication.saePasswordsFile} \ 1144 | grep -v '^\s*#' \ 1145 | grep -Eo 'mac=([0-9A-Fa-f]{2}[:]){5}([0-9A-Fa-f]{2})' \ 1146 | sed 's|^mac=||' >> "$MAC_ALLOW_FILE" 1147 '' 1148 ); 1149 "20-addMacDeny" = mkIf (bssCfg.macDeny != [ ]) ( 1150 pkgs.writeShellScript "add-mac-deny" '' 1151 MAC_DENY_FILE=$3 1152 cat >> "$MAC_DENY_FILE" <<EOF 1153 ${concatStringsSep "\n" bssCfg.macDeny} 1154 EOF 1155 '' 1156 ); 1157 "20-addMacDenyFile" = mkIf (bssCfg.macDenyFile != null) ( 1158 pkgs.writeShellScript "add-mac-deny-file" '' 1159 MAC_DENY_FILE=$3 1160 grep -Eo '^([0-9A-Fa-f]{2}[:]){5}([0-9A-Fa-f]{2})' ${escapeShellArg bssCfg.macDenyFile} >> "$MAC_DENY_FILE" 1161 '' 1162 ); 1163 # Add wpa_passphrase from file 1164 "20-wpaPasswordFile" = mkIf (bssCfg.authentication.wpaPasswordFile != null) ( 1165 pkgs.writeShellScript "wpa-password-file" '' 1166 HOSTAPD_CONFIG_FILE=$1 1167 cat >> "$HOSTAPD_CONFIG_FILE" <<EOF 1168 wpa_passphrase=$(cat ${escapeShellArg bssCfg.authentication.wpaPasswordFile}) 1169 EOF 1170 '' 1171 ); 1172 # Add sae passwords from file 1173 "20-saePasswordsFile" = mkIf (bssCfg.authentication.saePasswordsFile != null) ( 1174 pkgs.writeShellScript "sae-passwords-file" '' 1175 HOSTAPD_CONFIG_FILE=$1 1176 grep -v '^\s*#' ${escapeShellArg bssCfg.authentication.saePasswordsFile} \ 1177 | sed 's/^/sae_password=/' >> "$HOSTAPD_CONFIG_FILE" 1178 '' 1179 ); 1180 # Add sae passwords from nix definitions, potentially reading secrets 1181 "20-saePasswords" = mkIf (bssCfg.authentication.saePasswords != [ ]) ( 1182 pkgs.writeShellScript "sae-passwords" ( 1183 '' 1184 HOSTAPD_CONFIG_FILE=$1 1185 '' 1186 + concatMapStrings ( 1187 entry: 1188 let 1189 lineSuffix = 1190 optionalString (entry.password != null) entry.password 1191 + optionalString (entry.mac != null) "|mac=${entry.mac}" 1192 + optionalString (entry.vlanid != null) "|vlanid=${toString entry.vlanid}" 1193 + optionalString (entry.pk != null) "|pk=${entry.pk}" 1194 + optionalString (entry.id != null) "|id=${entry.id}"; 1195 in 1196 '' 1197 ( 1198 echo -n 'sae_password=' 1199 ${optionalString (entry.passwordFile != null) ''tr -d '\n' < ${entry.passwordFile}''} 1200 echo ${escapeShellArg lineSuffix} 1201 ) >> "$HOSTAPD_CONFIG_FILE" 1202 '' 1203 ) bssCfg.authentication.saePasswords 1204 ) 1205 ); 1206 }; 1207 }; 1208 }) 1209 ); 1210 }; 1211 }; 1212 1213 config.settings = 1214 let 1215 radioCfg = radioSubmod.config; 1216 in 1217 { 1218 driver = radioCfg.driver; 1219 hw_mode = 1220 { 1221 "2g" = "g"; 1222 "5g" = "a"; 1223 "6g" = "a"; 1224 "60g" = "ad"; 1225 } 1226 .${radioCfg.band}; 1227 channel = radioCfg.channel; 1228 noscan = radioCfg.noScan; 1229 } 1230 // optionalAttrs (radioCfg.countryCode != null) { 1231 country_code = radioCfg.countryCode; 1232 # IEEE 802.11d: Limit to frequencies allowed in country 1233 ieee80211d = true; 1234 # IEEE 802.11h: Enable radar detection and DFS (Dynamic Frequency Selection) 1235 ieee80211h = true; 1236 } 1237 // optionalAttrs radioCfg.wifi4.enable { 1238 # IEEE 802.11n (WiFi 4) related configuration 1239 ieee80211n = true; 1240 require_ht = radioCfg.wifi4.require; 1241 ht_capab = concatMapStrings (x: "[${x}]") radioCfg.wifi4.capabilities; 1242 } 1243 // optionalAttrs radioCfg.wifi5.enable { 1244 # IEEE 802.11ac (WiFi 5) related configuration 1245 ieee80211ac = true; 1246 require_vht = radioCfg.wifi5.require; 1247 vht_oper_chwidth = radioCfg.wifi5.operatingChannelWidth; 1248 vht_capab = concatMapStrings (x: "[${x}]") radioCfg.wifi5.capabilities; 1249 } 1250 // optionalAttrs radioCfg.wifi6.enable { 1251 # IEEE 802.11ax (WiFi 6) related configuration 1252 ieee80211ax = true; 1253 require_he = mkIf radioCfg.wifi6.require true; 1254 he_oper_chwidth = radioCfg.wifi6.operatingChannelWidth; 1255 he_su_beamformer = radioCfg.wifi6.singleUserBeamformer; 1256 he_su_beamformee = radioCfg.wifi6.singleUserBeamformee; 1257 he_mu_beamformer = radioCfg.wifi6.multiUserBeamformer; 1258 } 1259 // optionalAttrs radioCfg.wifi7.enable { 1260 # IEEE 802.11be (WiFi 7) related configuration 1261 ieee80211be = true; 1262 eht_oper_chwidth = radioCfg.wifi7.operatingChannelWidth; 1263 eht_su_beamformer = radioCfg.wifi7.singleUserBeamformer; 1264 eht_su_beamformee = radioCfg.wifi7.singleUserBeamformee; 1265 eht_mu_beamformer = radioCfg.wifi7.multiUserBeamformer; 1266 }; 1267 }) 1268 ); 1269 }; 1270 }; 1271 }; 1272 1273 imports = 1274 let 1275 renamedOptionMessage = message: '' 1276 ${message} 1277 Refer to the documentation of `services.hostapd.radios` for an example and more information. 1278 ''; 1279 in 1280 [ 1281 (mkRemovedOptionModule [ "services" "hostapd" "interface" ] ( 1282 renamedOptionMessage "All other options for this interface are now set via `services.hostapd.radios.«interface».*`." 1283 )) 1284 1285 (mkRemovedOptionModule [ "services" "hostapd" "driver" ] ( 1286 renamedOptionMessage "It has been replaced by `services.hostapd.radios.«interface».driver`." 1287 )) 1288 (mkRemovedOptionModule [ "services" "hostapd" "noScan" ] ( 1289 renamedOptionMessage "It has been replaced by `services.hostapd.radios.«interface».noScan`." 1290 )) 1291 (mkRemovedOptionModule [ "services" "hostapd" "countryCode" ] ( 1292 renamedOptionMessage "It has been replaced by `services.hostapd.radios.«interface».countryCode`." 1293 )) 1294 (mkRemovedOptionModule [ "services" "hostapd" "hwMode" ] ( 1295 renamedOptionMessage "It has been replaced by `services.hostapd.radios.«interface».band`." 1296 )) 1297 (mkRemovedOptionModule [ "services" "hostapd" "channel" ] ( 1298 renamedOptionMessage "It has been replaced by `services.hostapd.radios.«interface».channel`." 1299 )) 1300 (mkRemovedOptionModule [ "services" "hostapd" "extraConfig" ] (renamedOptionMessage '' 1301 It has been replaced by `services.hostapd.radios.«interface».settings` and 1302 `services.hostapd.radios.«interface».networks.«network».settings` respectively 1303 for per-radio and per-network extra configuration. The module now supports a lot more 1304 options inherently, so please re-check whether using settings is still necessary.'')) 1305 1306 (mkRemovedOptionModule [ "services" "hostapd" "logLevel" ] ( 1307 renamedOptionMessage "It has been replaced by `services.hostapd.radios.«interface».networks.«network».logLevel`." 1308 )) 1309 (mkRemovedOptionModule [ "services" "hostapd" "group" ] ( 1310 renamedOptionMessage "It has been replaced by `services.hostapd.radios.«interface».networks.«network».group`." 1311 )) 1312 (mkRemovedOptionModule [ "services" "hostapd" "ssid" ] ( 1313 renamedOptionMessage "It has been replaced by `services.hostapd.radios.«interface».networks.«network».ssid`." 1314 )) 1315 1316 (mkRemovedOptionModule [ "services" "hostapd" "wpa" ] ( 1317 renamedOptionMessage "It has been replaced by `services.hostapd.radios.«interface».networks.«network».authentication.mode`." 1318 )) 1319 (mkRemovedOptionModule [ "services" "hostapd" "wpaPassphrase" ] 1320 (renamedOptionMessage '' 1321 It has been replaced by `services.hostapd.radios.«interface».networks.«network».authentication.wpaPassword`. 1322 While upgrading your config, please consider using the newer SAE authentication scheme 1323 and one of the new `passwordFile`-like options to avoid putting the password into the world readable nix-store.'') 1324 ) 1325 ]; 1326 1327 config = mkIf cfg.enable { 1328 warnings = 1329 let 1330 wirelessEnabled = config.networking.wireless.enable; 1331 wirelessInterfaces = config.networking.wireless.interfaces; 1332 hostapdInterfaces = lib.attrNames cfg.radios; 1333 hasInterfaceConflict = lib.intersectLists hostapdInterfaces wirelessInterfaces != [ ]; 1334 # we check if wirelessInterfaces is empty as that means all interfaces implicit 1335 shouldWarn = wirelessEnabled && (wirelessInterfaces == [ ] || hasInterfaceConflict); 1336 in 1337 lib.optional shouldWarn '' 1338 Some wireless interface is configured for both for client and access point mode: 1339 this is not allowed. Either specify `networking.wireless.interfaces` and exclude 1340 those from `services.hostapd.radios` or make sure to not run the `wpa_supplicant` 1341 and `hostapd` services simultaneously. 1342 '' 1343 ++ lib.optional config.networking.wireless.iwd.enable '' 1344 hostapd and iwd do conflict, 1345 use `networking.wireless.enable` in combination with `networking.wireless.interfaces` to avoid it. 1346 ''; 1347 assertions = [ 1348 { 1349 assertion = cfg.radios != { }; 1350 message = "At least one radio must be configured with hostapd!"; 1351 } 1352 ] 1353 # Radio warnings 1354 ++ (concatLists ( 1355 mapAttrsToList ( 1356 radio: radioCfg: 1357 [ 1358 { 1359 assertion = radioCfg.networks != { }; 1360 message = "hostapd radio ${radio}: At least one network must be configured!"; 1361 } 1362 # XXX: There could be many more useful assertions about (band == xy) -> ensure other required settings. 1363 # see https://github.com/openwrt/openwrt/blob/539cb5389d9514c99ec1f87bd4465f77c7ed9b93/package/kernel/mac80211/files/lib/netifd/wireless/mac80211.sh#L158 1364 { 1365 assertion = length (filter (bss: bss == radio) (attrNames radioCfg.networks)) == 1; 1366 message = ''hostapd radio ${radio}: Exactly one network must be named like the radio, for reasons internal to hostapd.''; 1367 } 1368 { 1369 assertion = 1370 (radioCfg.wifi4.enable && builtins.elem "HT40-" radioCfg.wifi4.capabilities) 1371 -> radioCfg.channel != 0; 1372 message = ''hostapd radio ${radio}: using ACS (channel = 0) together with HT40- (wifi4.capabilities) is unsupported by hostapd''; 1373 } 1374 ] 1375 # BSS warnings 1376 ++ (concatLists ( 1377 mapAttrsToList ( 1378 bss: bssCfg: 1379 let 1380 auth = bssCfg.authentication; 1381 countWpaPasswordDefinitions = count (x: x != null) [ 1382 auth.wpaPassword 1383 auth.wpaPasswordFile 1384 auth.wpaPskFile 1385 ]; 1386 in 1387 [ 1388 { 1389 assertion = hasPrefix radio bss; 1390 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, ..."; 1391 } 1392 { 1393 assertion = (length (attrNames radioCfg.networks) > 1) -> (bssCfg.bssid != null); 1394 message = ''hostapd radio ${radio} bss ${bss}: bssid must be specified manually (for now) since this radio uses multiple BSS.''; 1395 } 1396 { 1397 assertion = countWpaPasswordDefinitions <= 1; 1398 message = ''hostapd radio ${radio} bss ${bss}: must use at most one WPA password option (wpaPassword, wpaPasswordFile, wpaPskFile)''; 1399 } 1400 { 1401 assertion = 1402 auth.wpaPassword != null 1403 -> (stringLength auth.wpaPassword >= 8 && stringLength auth.wpaPassword <= 63); 1404 message = ''hostapd radio ${radio} bss ${bss}: uses a wpaPassword of invalid length (must be in [8,63]).''; 1405 } 1406 { 1407 assertion = auth.saePasswords == [ ] || auth.saePasswordsFile == null; 1408 message = ''hostapd radio ${radio} bss ${bss}: must use only one SAE password option (saePasswords or saePasswordsFile)''; 1409 } 1410 { 1411 assertion = auth.mode == "wpa3-sae" -> (auth.saePasswords != [ ] || auth.saePasswordsFile != null); 1412 message = ''hostapd radio ${radio} bss ${bss}: uses WPA3-SAE which requires defining a sae password option''; 1413 } 1414 { 1415 assertion = 1416 auth.mode == "wpa3-sae-transition" 1417 -> (auth.saePasswords != [ ] || auth.saePasswordsFile != null) && countWpaPasswordDefinitions == 1; 1418 message = ''hostapd radio ${radio} bss ${bss}: uses WPA3-SAE in transition mode requires defining both a wpa password option and a sae password option''; 1419 } 1420 { 1421 assertion = 1422 (auth.mode == "wpa2-sha1" || auth.mode == "wpa2-sha256") -> countWpaPasswordDefinitions == 1; 1423 message = ''hostapd radio ${radio} bss ${bss}: uses WPA2-PSK which requires defining a wpa password option''; 1424 } 1425 ] 1426 ++ optionals (auth.saePasswords != [ ]) ( 1427 imap1 (i: entry: { 1428 assertion = (entry.password == null) != (entry.passwordFile == null); 1429 message = ''hostapd radio ${radio} bss ${bss} saePassword entry ${i}: must set exactly one of `password` or `passwordFile`''; 1430 }) auth.saePasswords 1431 ) 1432 ) radioCfg.networks 1433 )) 1434 ) cfg.radios 1435 )); 1436 1437 environment.systemPackages = [ cfg.package ]; 1438 1439 systemd.services.hostapd = { 1440 description = "IEEE 802.11 Host Access-Point Daemon"; 1441 1442 path = [ cfg.package ]; 1443 after = map (radio: "sys-subsystem-net-devices-${utils.escapeSystemdPath radio}.device") ( 1444 attrNames cfg.radios 1445 ); 1446 bindsTo = map (radio: "sys-subsystem-net-devices-${utils.escapeSystemdPath radio}.device") ( 1447 attrNames cfg.radios 1448 ); 1449 wantedBy = [ "multi-user.target" ]; 1450 1451 # Create merged configuration and acl files for each radio (and their bss's) prior to starting 1452 preStart = concatStringsSep "\n" (mapAttrsToList makeRadioRuntimeFiles cfg.radios); 1453 1454 serviceConfig = { 1455 ExecStart = "${cfg.package}/bin/hostapd ${concatStringsSep " " runtimeConfigFiles}"; 1456 Restart = "always"; 1457 ExecReload = "${pkgs.coreutils}/bin/kill -HUP $MAINPID"; 1458 RuntimeDirectory = "hostapd"; 1459 1460 # Hardening 1461 LockPersonality = true; 1462 MemoryDenyWriteExecute = true; 1463 DevicePolicy = "closed"; 1464 DeviceAllow = "/dev/rfkill rw"; 1465 NoNewPrivileges = true; 1466 PrivateUsers = false; # hostapd requires true root access. 1467 PrivateTmp = false; # hostapd_cli opens a socket in /tmp 1468 ProtectClock = true; 1469 ProtectControlGroups = true; 1470 ProtectHome = true; 1471 ProtectHostname = true; 1472 ProtectKernelLogs = true; 1473 ProtectKernelModules = true; 1474 ProtectKernelTunables = true; 1475 ProtectProc = "invisible"; 1476 ProcSubset = "pid"; 1477 ProtectSystem = "strict"; 1478 RestrictAddressFamilies = [ 1479 "AF_INET" 1480 "AF_INET6" 1481 "AF_NETLINK" 1482 "AF_UNIX" 1483 "AF_PACKET" 1484 ]; 1485 RestrictNamespaces = true; 1486 RestrictRealtime = true; 1487 RestrictSUIDSGID = true; 1488 SystemCallArchitectures = "native"; 1489 SystemCallFilter = [ 1490 "@system-service" 1491 "~@privileged" 1492 "@chown" 1493 ]; 1494 UMask = "0077"; 1495 }; 1496 }; 1497 }; 1498}