1 { config, lib, pkgs, utils, ... }:
2 # All hope abandon ye who enter here. hostapd's configuration
3 # format is ... special, and you won't be able to infer any
4 # of their assumptions from just reading the "documentation"
5 # (i.e. the example config). Assume footguns at all points -
6 # to make informed decisions you will probably need to look
7 # at hostapd's code. You have been warned, proceed with care.
48 cfg = config.services.hostapd;
50 extraSettingsFormat = {
52 singleAtom = types.oneOf [ types.bool types.int types.str ];
53 atom = types.either singleAtom (types.listOf singleAtom) // {
54 description = "atom (bool, int or string) or a list of them for duplicate keys";
56 in types.attrsOf atom;
58 generate = name: value: pkgs.writeText name (generators.toKeyValue {
59 listsAsDuplicateKeys = true;
60 mkKeyValue = generators.mkKeyValueDefault {
62 if isInt v then toString v
63 else if isString v then v
64 else if true == v then "1"
65 else if false == v then "0"
66 else throw "unsupported type ${builtins.typeOf v}: ${(generators.toPretty {}) v}";
71 # Generates the header for a single BSS (i.e. WiFi network)
72 writeBssHeader = radio: bss: bssIdx: pkgs.writeText "hostapd-radio-${radio}-bss-${bss}.conf" ''
73 ''\n''\n# BSS ${toString bssIdx}: ${bss}
74 ################################
76 ${if bssIdx == 0 then "interface" else "bss"}=${bss}
79 makeRadioRuntimeFiles = radio: radioCfg:
80 pkgs.writeShellScript "make-hostapd-${radio}-files" (''
83 hostapd_config_file=/run/hostapd/${escapeShellArg radio}.hostapd.conf
84 rm -f "$hostapd_config_file"
85 cat > "$hostapd_config_file" <<EOF
86 # Radio base configuration: ${radio}
87 ################################
91 cat ${escapeShellArg (extraSettingsFormat.generate "hostapd-radio-${radio}-extra.conf" radioCfg.settings)} >> "$hostapd_config_file"
92 ${concatMapStrings (script: "${script} \"$hostapd_config_file\"\n") (attrValues radioCfg.dynamicConfigScripts)}
94 + concatMapStrings (x: "${x}\n") (imap0 (i: f: f i)
95 (mapAttrsToList (bss: bssCfg: bssIdx: ''
96 ''\n# BSS configuration: ${bss}
98 mac_allow_file=/run/hostapd/${escapeShellArg bss}.mac.allow
99 rm -f "$mac_allow_file"
100 touch "$mac_allow_file"
102 mac_deny_file=/run/hostapd/${escapeShellArg bss}.mac.deny
103 rm -f "$mac_deny_file"
104 touch "$mac_deny_file"
106 cat ${writeBssHeader radio bss bssIdx} >> "$hostapd_config_file"
107 cat ${escapeShellArg (extraSettingsFormat.generate "hostapd-radio-${radio}-bss-${bss}-extra.conf" bssCfg.settings)} >> "$hostapd_config_file"
108 ${concatMapStrings (script: "${script} \"$hostapd_config_file\" \"$mac_allow_file\" \"$mac_deny_file\"\n") (attrValues bssCfg.dynamicConfigScripts)}
109 '') radioCfg.networks)));
111 runtimeConfigFiles = mapAttrsToList (radio: _: "/run/hostapd/${radio}.hostapd.conf") cfg.radios;
113 meta.maintainers = with maintainers; [ oddlama ];
117 enable = mkEnableOption ''
118 hostapd, a user space daemon for access point and
119 authentication servers. It implements IEEE 802.11 access point management,
120 IEEE 802.1X/WPA/WPA2/EAP Authenticators, RADIUS client, EAP server, and RADIUS
121 authentication server
124 package = mkPackageOption pkgs "hostapd" {};
128 example = literalExpression ''
132 # countryCode = "US";
135 authentication.saePasswords = [{ password = "a flakey password"; }]; # Use saePasswordsFile if possible.
139 # WiFi 5 (5GHz) with two advertised networks
142 channel = 0; # Enable automatic channel selection (ACS). Use only if your hardware supports it.
143 # countryCode = "US";
146 authentication.saePasswords = [{ password = "a flakey password"; }]; # Use saePasswordsFile if possible.
148 networks.wlp3s0-1 = {
149 ssid = "Open AP with WiFi5";
150 authentication.mode = "none";
154 # Legacy WPA2 example
156 # countryCode = "US";
160 mode = "wpa2-sha256";
161 wpaPassword = "a flakey password"; # Use wpaPasswordFile if possible.
168 This option allows you to define APs for one or multiple physical radios.
169 At least one radio must be specified.
171 For each radio, hostapd requires a separate logical interface (like wlp3s0, wlp3s1, ...).
172 A default interface is usually be created automatically by your system, but to use
173 multiple radios of a single device, it may be required to create additional logical interfaces
174 for example by using {option}`networking.wlanInterfaces`.
176 Each physical radio can only support a single hardware-mode that is configured via
177 ({option}`services.hostapd.radios.<radio>.band`). To create a dual-band
178 or tri-band AP, you will have to use a device that has multiple physical radios
179 and supports configuring multiple APs (Refer to valid interface combinations in
182 type = types.attrsOf (types.submodule (radioSubmod: {
189 The driver {command}`hostapd` will use.
190 {var}`nl80211` is used with all Linux mac80211 drivers.
191 {var}`none` is used if building a standalone RADIUS server that does
192 not control any wireless/wired driver.
193 Most applications will probably use the default.
201 Disables scan for overlapping BSSs in HT40+/- mode.
202 Caution: turning this on will likely violate regulatory requirements!
206 countryCode = mkOption {
209 type = types.nullOr types.str;
211 Country code (ISO/IEC 3166-1). Used to set regulatory domain.
212 Set as needed to indicate country in which device is operating.
213 This can limit available channels and transmit power.
214 These two octets are used as the first two octets of the Country String
215 (dot11CountryString).
217 Setting this will force you to also enable IEEE 802.11d and IEEE 802.11h.
219 IEEE 802.11d: This advertises the countryCode and the set of allowed channels
220 and transmit power levels based on the regulatory limits.
222 IEEE802.11h: This enables radar detection and DFS (Dynamic Frequency Selection)
223 support if available. DFS support is required on outdoor 5 GHz channels in most
224 countries of the world.
230 type = types.enum ["2g" "5g" "6g" "60g"];
232 Specifies the frequency band to use, possible values are 2g for 2.4 GHz,
233 5g for 5 GHz, 6g for 6 GHz and 60g for 60 GHz.
242 The channel to operate on. Use 0 to enable ACS (Automatic Channel Selection).
243 Beware that not every device supports ACS in which case {command}`hostapd`
248 settings = mkOption {
250 example = { acs_exclude_dfs = true; };
251 type = types.submodule {
252 freeformType = extraSettingsFormat.type;
255 Extra configuration options to put at the end of global initialization, before defining BSSs.
256 To find out which options are global and which are per-bss you have to read hostapd's source code,
257 which is non-trivial and not documented otherwise.
259 Lists will be converted to multiple definitions of the same key, and booleans to 0/1.
260 Otherwise, the inputs are not modified or checked for correctness.
264 dynamicConfigScripts = mkOption {
266 type = types.attrsOf types.path;
267 example = literalExpression ''
269 exampleDynamicConfig = pkgs.writeShellScript "dynamic-config" '''
272 cat >> "$HOSTAPD_CONFIG" << EOF
273 # Add some dynamically generated statements here,
274 # for example based on the physical adapter in use
280 All of these scripts will be executed in lexicographical order before hostapd
281 is started, right after the global segment was generated and may dynamically
282 append global options the generated configuration file.
284 The first argument will point to the configuration file that you may append to.
288 #### IEEE 802.11n (WiFi 4) related configuration
295 Enables support for IEEE 802.11n (WiFi 4, HT).
296 This is enabled by default, since the vase majority of devices
297 are expected to support this.
301 capabilities = mkOption {
302 type = types.listOf types.str;
303 default = ["HT40" "HT40-" "SHORT-GI-20" "SHORT-GI-40"];
304 example = ["LDPC" "HT40+" "HT40-" "GF" "SHORT-GI-20" "SHORT-GI-40" "TX-STBC" "RX-STBC1"];
306 HT (High Throughput) capabilities given as a list of flags.
307 Please refer to the hostapd documentation for allowed values and
308 only set values supported by your physical adapter.
310 The default contains common values supported by most adapters.
317 description = "Require stations (clients) to support WiFi 4 (HT) and disassociate them if they don't.";
321 #### IEEE 802.11ac (WiFi 5) related configuration
327 description = "Enables support for IEEE 802.11ac (WiFi 5, VHT)";
330 capabilities = mkOption {
331 type = types.listOf types.str;
333 example = ["SHORT-GI-80" "TX-STBC-2BY1" "RX-STBC-1" "RX-ANTENNA-PATTERN" "TX-ANTENNA-PATTERN"];
335 VHT (Very High Throughput) capabilities given as a list of flags.
336 Please refer to the hostapd documentation for allowed values and
337 only set values supported by your physical adapter.
344 description = "Require stations (clients) to support WiFi 5 (VHT) and disassociate them if they don't.";
347 operatingChannelWidth = mkOption {
349 type = types.enum ["20or40" "80" "160" "80+80"];
358 Determines the operating channel width for VHT.
360 - {var}`"20or40"`: 20 or 40 MHz operating channel width
361 - {var}`"80"`: 80 MHz channel width
362 - {var}`"160"`: 160 MHz channel width
363 - {var}`"80+80"`: 80+80 MHz channel width
368 #### IEEE 802.11ax (WiFi 6) related configuration
374 description = "Enables support for IEEE 802.11ax (WiFi 6, HE)";
380 description = "Require stations (clients) to support WiFi 6 (HE) and disassociate them if they don't.";
383 singleUserBeamformer = mkOption {
386 description = "HE single user beamformer support";
389 singleUserBeamformee = mkOption {
392 description = "HE single user beamformee support";
395 multiUserBeamformer = mkOption {
398 description = "HE multi user beamformee support";
401 operatingChannelWidth = mkOption {
403 type = types.enum ["20or40" "80" "160" "80+80"];
412 Determines the operating channel width for HE.
414 - {var}`"20or40"`: 20 or 40 MHz operating channel width
415 - {var}`"80"`: 80 MHz channel width
416 - {var}`"160"`: 160 MHz channel width
417 - {var}`"80+80"`: 80+80 MHz channel width
422 #### IEEE 802.11be (WiFi 7) related configuration
429 Enables support for IEEE 802.11be (WiFi 7, EHT). This is currently experimental
430 and requires you to manually enable CONFIG_IEEE80211BE when building hostapd.
434 singleUserBeamformer = mkOption {
437 description = "EHT single user beamformer support";
440 singleUserBeamformee = mkOption {
443 description = "EHT single user beamformee support";
446 multiUserBeamformer = mkOption {
449 description = "EHT multi user beamformee support";
452 operatingChannelWidth = mkOption {
454 type = types.enum ["20or40" "80" "160" "80+80"];
463 Determines the operating channel width for EHT.
465 - {var}`"20or40"`: 20 or 40 MHz operating channel width
466 - {var}`"80"`: 80 MHz channel width
467 - {var}`"160"`: 160 MHz channel width
468 - {var}`"80+80"`: 80+80 MHz channel width
475 networks = mkOption {
477 example = literalExpression ''
480 ssid = "Primary advertised network";
481 authentication.saePasswords = [{ password = "a flakey password"; }]; # Use saePasswordsFile if possible.
484 ssid = "Secondary advertised network (Open)";
485 authentication.mode = "none";
490 This defines a BSS, colloquially known as a WiFi network.
491 You have to specify at least one.
493 type = types.attrsOf (types.submodule (bssSubmod: {
495 logLevel = mkOption {
499 Levels (minimum value for logged events):
500 0 = verbose debugging
502 2 = informational messages
513 Members of this group can access the control socket for this interface.
517 utf8Ssid = mkOption {
520 description = "Whether the SSID is to be interpreted using UTF-8 encoding.";
524 example = "❄️ cool ❄️";
526 description = "SSID to be used in IEEE 802.11 management frames.";
530 type = types.nullOr types.str;
532 example = "11:22:33:44:55:66";
534 Specifies the BSSID for this BSS. Usually determined automatically,
535 but for now you have to manually specify them when using multiple BSS.
536 Try assigning related addresses from the locally administered MAC address ranges,
537 by reusing the hardware address but replacing the second nibble with 2, 6, A or E.
538 (e.g. if real address is `XX:XX:XX:XX:XX`, try `X2:XX:XX:XX:XX:XX`, `X6:XX:XX:XX:XX:XX`, ...
539 for the second, third, ... BSS)
545 type = types.enum ["deny" "allow" "radius"];
553 Station MAC address -based authentication. The following modes are available:
555 - {var}`"deny"`: Allow unless listed in {option}`macDeny` (default)
556 - {var}`"allow"`: Deny unless listed in {option}`macAllow`
557 - {var}`"radius"`: Use external radius server, but check both {option}`macAllow` and {option}`macDeny` first
559 Please note that this kind of access control requires a driver that uses
560 hostapd to take care of management frame processing and as such, this can be
561 used with driver=hostap or driver=nl80211, but not with driver=atheros.
565 macAllow = mkOption {
566 type = types.listOf types.str;
568 example = ["11:22:33:44:55:66"];
570 Specifies the MAC addresses to allow if {option}`macAcl` is set to {var}`"allow"` or {var}`"radius"`.
571 These values will be world-readable in the Nix store. Values will automatically be merged with
572 {option}`macAllowFile` if necessary.
576 macAllowFile = mkOption {
577 type = types.nullOr types.path;
580 Specifies a file containing the MAC addresses to allow if {option}`macAcl` is set to {var}`"allow"` or {var}`"radius"`.
581 The file should contain exactly one MAC address per line. Comments and empty lines are ignored,
582 only lines starting with a valid MAC address will be considered (e.g. `11:22:33:44:55:66`) and
583 any content after the MAC address is ignored.
588 type = types.listOf types.str;
590 example = ["11:22:33:44:55:66"];
592 Specifies the MAC addresses to deny if {option}`macAcl` is set to {var}`"deny"` or {var}`"radius"`.
593 These values will be world-readable in the Nix store. Values will automatically be merged with
594 {option}`macDenyFile` if necessary.
598 macDenyFile = mkOption {
599 type = types.nullOr types.path;
602 Specifies a file containing the MAC addresses to deny if {option}`macAcl` is set to {var}`"deny"` or {var}`"radius"`.
603 The file should contain exactly one MAC address per line. Comments and empty lines are ignored,
604 only lines starting with a valid MAC address will be considered (e.g. `11:22:33:44:55:66`) and
605 any content after the MAC address is ignored.
609 ignoreBroadcastSsid = mkOption {
610 default = "disabled";
611 type = types.enum ["disabled" "empty" "clear"];
619 Send empty SSID in beacons and ignore probe request frames that do not
620 specify full SSID, i.e., require stations to know SSID. Note that this does
621 not increase security, since your clients will then broadcast the SSID instead,
622 which can increase congestion.
624 - {var}`"disabled"`: Advertise ssid normally.
625 - {var}`"empty"`: send empty (length=0) SSID in beacon and ignore probe request for broadcast SSID
626 - {var}`"clear"`: clear SSID (ASCII 0), but keep the original length (this may be required with some
627 legacy clients that do not support empty SSID) and ignore probe requests for broadcast SSID. Only
628 use this if empty does not work with your clients.
632 apIsolate = mkOption {
636 Isolate traffic between stations (clients) and prevent them from
637 communicating with each other.
641 settings = mkOption {
643 example = { multi_ap = true; };
644 type = types.submodule {
645 freeformType = extraSettingsFormat.type;
648 Extra configuration options to put at the end of this BSS's defintion in the
649 hostapd.conf for the associated interface. To find out which options are global
650 and which are per-bss you have to read hostapd's source code, which is non-trivial
651 and not documented otherwise.
653 Lists will be converted to multiple definitions of the same key, and booleans to 0/1.
654 Otherwise, the inputs are not modified or checked for correctness.
658 dynamicConfigScripts = mkOption {
660 type = types.attrsOf types.path;
661 example = literalExpression ''
663 exampleDynamicConfig = pkgs.writeShellScript "dynamic-config" '''
665 # These always exist, but may or may not be used depending on the actual configuration
669 cat >> "$HOSTAPD_CONFIG" << EOF
670 # Add some dynamically generated statements here
676 All of these scripts will be executed in lexicographical order before hostapd
677 is started, right after the bss segment was generated and may dynamically
678 append bss options to the generated configuration file.
680 The first argument will point to the configuration file that you may append to.
681 The second and third argument will point to this BSS's MAC allow and MAC deny file respectively.
685 #### IEEE 802.11i (WPA) configuration
689 default = "wpa3-sae";
690 type = types.enum ["none" "wpa2-sha1" "wpa2-sha256" "wpa3-sae-transition" "wpa3-sae"];
692 Selects the authentication mode for this AP.
694 - {var}`"none"`: Don't configure any authentication. This will disable wpa alltogether
695 and create an open AP. Use {option}`settings` together with this option if you
696 want to configure the authentication manually. Any password options will still be
698 - {var}`"wpa2-sha1"`: Not recommended. WPA2-Personal using HMAC-SHA1. Passwords are set
699 using {option}`wpaPassword` or preferably by {option}`wpaPasswordFile` or {option}`wpaPskFile`.
700 - {var}`"wpa2-sha256"`: WPA2-Personal using HMAC-SHA256 (IEEE 802.11i/RSN). Passwords are set
701 using {option}`wpaPassword` or preferably by {option}`wpaPasswordFile` or {option}`wpaPskFile`.
702 - {var}`"wpa3-sae-transition"`: Use WPA3-Personal (SAE) if possible, otherwise fallback
703 to WPA2-SHA256. Only use if necessary and switch to the newer WPA3-SAE when possible.
704 You will have to specify both {option}`wpaPassword` and {option}`saePasswords` (or one of their alternatives).
705 - {var}`"wpa3-sae"`: Use WPA3-Personal (SAE). This is currently the recommended way to
706 setup a secured WiFi AP (as of March 2023) and therefore the default. Passwords are set
707 using either {option}`saePasswords` or preferably {option}`saePasswordsFile`.
711 pairwiseCiphers = mkOption {
713 example = ["CCMP-256" "GCMP-256"];
714 type = types.listOf types.str;
716 Set of accepted cipher suites (encryption algorithms) for pairwise keys (unicast packets).
717 By default this allows just CCMP, which is the only commonly supported secure option.
718 Use {option}`enableRecommendedPairwiseCiphers` to also enable newer recommended ciphers.
720 Please refer to the hostapd documentation for allowed values. Generally, only
721 CCMP or GCMP modes should be considered safe options. Most devices support CCMP while
722 GCMP is often only available with devices supporting WiFi 5 (IEEE 802.11ac) or higher.
726 enableRecommendedPairwiseCiphers = mkOption {
731 Additionally enable the recommended set of pairwise ciphers.
732 This enables newer secure ciphers, additionally to those defined in {option}`pairwiseCiphers`.
733 You will have to test whether your hardware supports these by trial-and-error, because
734 even if `iw list` indicates hardware support, your driver might not expose it.
736 Beware {command}`hostapd` will most likely not return a useful error message in case
737 this is enabled despite the driver or hardware not supporting the newer ciphers.
738 Look out for messages like `Failed to set beacon parameters`.
742 wpaPassword = mkOption {
744 example = "a flakey password";
745 type = types.nullOr types.str;
747 Sets the password for WPA-PSK that will be converted to the pre-shared key.
748 The password length must be in the range [8, 63] characters. While some devices
749 may allow arbitrary characters (such as UTF-8) to be used, but the standard specifies
750 that each character in the passphrase must be an ASCII character in the range [0x20, 0x7e]
751 (IEEE Std. 802.11i-2004, Annex H.4.1). Use emojis at your own risk.
753 Not used when {option}`mode` is {var}`"wpa3-sae"`.
755 Warning: This password will get put into a world-readable file in the Nix store!
756 Using {option}`wpaPasswordFile` or {option}`wpaPskFile` instead is recommended.
760 wpaPasswordFile = mkOption {
762 type = types.nullOr types.path;
764 Sets the password for WPA-PSK. Follows the same rules as {option}`wpaPassword`,
765 but reads the password from the given file to prevent the password from being
766 put into the Nix store.
768 Not used when {option}`mode` is {var}`"wpa3-sae"`.
772 wpaPskFile = mkOption {
774 type = types.nullOr types.path;
776 Sets the password(s) for WPA-PSK. Similar to {option}`wpaPasswordFile`,
777 but additionally allows specifying multiple passwords, and some other options.
779 Each line, except for empty lines and lines starting with #, must contain a
780 MAC address and either a 64-hex-digit PSK or a password separated with a space.
781 The password must follow the same rules as outlined in {option}`wpaPassword`.
782 The special MAC address `00:00:00:00:00:00` can be used to configure PSKs
783 that any client can use.
785 An optional key identifier can be added by prefixing the line with `keyid=<keyid_string>`
786 An optional VLAN ID can be specified by prefixing the line with `vlanid=<VLAN ID>`.
787 An optional WPS tag can be added by prefixing the line with `wps=<0/1>` (default: 0).
788 Any matching entry with that tag will be used when generating a PSK for a WPS Enrollee
789 instead of generating a new random per-Enrollee PSK.
791 Not used when {option}`mode` is {var}`"wpa3-sae"`.
795 saePasswords = mkOption {
797 example = literalExpression ''
799 # Any client may use these passwords
800 { password = "Wi-Figure it out"; }
801 { password = "second password for everyone"; mac = "ff:ff:ff:ff:ff:ff"; }
803 # Only the client with MAC-address 11:22:33:44:55:66 can use this password
804 { password = "sekret pazzword"; mac = "11:22:33:44:55:66"; }
808 Sets allowed passwords for WPA3-SAE.
810 The last matching (based on peer MAC address and identifier) entry is used to
811 select which password to use. An empty string has the special meaning of
812 removing all previously added entries.
814 Warning: These entries will get put into a world-readable file in
815 the Nix store! Using {option}`saePasswordFile` instead is recommended.
817 Not used when {option}`mode` is {var}`"wpa2-sha1"` or {var}`"wpa2-sha256"`.
819 type = types.listOf (types.submodule {
821 password = mkOption {
822 example = "a flakey password";
825 The password for this entry. SAE technically imposes no restrictions on
826 password length or character set. But due to limitations of {command}`hostapd`'s
827 config file format, a true newline character cannot be parsed.
829 Warning: This password will get put into a world-readable file in
830 the Nix store! Using {option}`wpaPasswordFile` or {option}`wpaPskFile` is recommended.
836 example = "11:22:33:44:55:66";
837 type = types.nullOr types.str;
839 If this attribute is not included, or if is set to the wildcard address (`ff:ff:ff:ff:ff:ff`),
840 the entry is available for any station (client) to use. If a specific peer MAC address is included,
841 only a station with that MAC address is allowed to use the entry.
848 type = types.nullOr types.int;
849 description = "If this attribute is given, all clients using this entry will get tagged with the given VLAN ID.";
855 type = types.nullOr types.str;
857 If this attribute is given, SAE-PK will be enabled for this connection.
858 This prevents evil-twin attacks, but a public key is required additionally to connect.
859 (Essentially adds pubkey authentication such that the client can verify identity of the AP)
866 type = types.nullOr types.str;
868 If this attribute is given with non-zero length, it will set the password identifier
869 for this entry. It can then only be used with that identifier.
876 saePasswordsFile = mkOption {
878 type = types.nullOr types.path;
880 Sets the password for WPA3-SAE. Follows the same rules as {option}`saePasswords`,
881 but reads the entries from the given file to prevent them from being
882 put into the Nix store.
884 One entry per line, empty lines and lines beginning with # will be ignored.
885 Each line must match the following format, although the order of optional
886 parameters doesn't matter:
887 `<password>[|mac=<peer mac>][|vlanid=<VLAN ID>][|pk=<m:ECPrivateKey-base64>][|id=<identifier>]`
889 Not used when {option}`mode` is {var}`"wpa2-sha1"` or {var}`"wpa2-sha256"`.
893 saeAddToMacAllow = mkOption {
897 If set, all sae password entries that have a non-wildcard MAC associated to
898 them will additionally be used to populate the MAC allow list. This is
899 additional to any entries set via {option}`macAllow` or {option}`macAllowFile`.
906 bssCfg = bssSubmod.config;
908 concatStringsSep " " (unique (bssCfg.authentication.pairwiseCiphers
909 ++ optionals bssCfg.authentication.enableRecommendedPairwiseCiphers ["CCMP" "CCMP-256" "GCMP" "GCMP-256"]));
913 utf8_ssid = bssCfg.utf8Ssid;
915 logger_syslog = mkDefault (-1);
916 logger_syslog_level = bssCfg.logLevel;
917 logger_stdout = mkDefault (-1);
918 logger_stdout_level = bssCfg.logLevel;
919 ctrl_interface = mkDefault "/run/hostapd";
920 ctrl_interface_group = bssCfg.group;
922 macaddr_acl = bssCfg.macAcl;
924 ignore_broadcast_ssid = bssCfg.ignoreBroadcastSsid;
926 # IEEE 802.11i (authentication) related configuration
927 # Encrypt management frames to protect against deauthentication and similar attacks
928 ieee80211w = mkDefault 1;
929 sae_require_mfp = mkDefault 1;
931 # Only allow WPA by default and disable insecure WEP
932 auth_algs = mkDefault 1;
933 # Always enable QoS, which is required for 802.11n and above
934 wmm_enabled = mkDefault true;
935 ap_isolate = bssCfg.apIsolate;
937 sae_password = flip map bssCfg.authentication.saePasswords (
940 + optionalString (entry.mac != null) "|mac=${entry.mac}"
941 + optionalString (entry.vlanid != null) "|vlanid=${toString entry.vlanid}"
942 + optionalString (entry.pk != null) "|pk=${entry.pk}"
943 + optionalString (entry.id != null) "|id=${entry.id}"
945 } // optionalAttrs (bssCfg.bssid != null) {
946 bssid = bssCfg.bssid;
947 } // optionalAttrs (bssCfg.macAllow != [] || bssCfg.macAllowFile != null || bssCfg.authentication.saeAddToMacAllow) {
948 accept_mac_file = "/run/hostapd/${bssCfg._module.args.name}.mac.allow";
949 } // optionalAttrs (bssCfg.macDeny != [] || bssCfg.macDenyFile != null) {
950 deny_mac_file = "/run/hostapd/${bssCfg._module.args.name}.mac.deny";
951 } // optionalAttrs (bssCfg.authentication.mode == "none") {
953 } // optionalAttrs (bssCfg.authentication.mode == "wpa3-sae") {
955 wpa_key_mgmt = "SAE";
956 # Derive PWE using both hunting-and-pecking loop and hash-to-element
958 # Prevent downgrade attacks by indicating to clients that they should
959 # disable any transition modes from now on.
960 transition_disable = "0x01";
961 } // optionalAttrs (bssCfg.authentication.mode == "wpa3-sae-transition") {
963 wpa_key_mgmt = "WPA-PSK-SHA256 SAE";
964 } // optionalAttrs (bssCfg.authentication.mode == "wpa2-sha1") {
966 wpa_key_mgmt = "WPA-PSK";
967 } // optionalAttrs (bssCfg.authentication.mode == "wpa2-sha256") {
969 wpa_key_mgmt = "WPA-PSK-SHA256";
970 } // optionalAttrs (bssCfg.authentication.mode != "none") {
971 wpa_pairwise = pairwiseCiphers;
972 rsn_pairwise = pairwiseCiphers;
973 } // optionalAttrs (bssCfg.authentication.wpaPassword != null) {
974 wpa_passphrase = bssCfg.authentication.wpaPassword;
975 } // optionalAttrs (bssCfg.authentication.wpaPskFile != null) {
976 wpa_psk_file = toString bssCfg.authentication.wpaPskFile;
979 dynamicConfigScripts = let
980 # All MAC addresses from SAE entries that aren't the wildcard address
981 saeMacs = filter (mac: mac != null && (toLower mac) != "ff:ff:ff:ff:ff:ff") (map (x: x.mac) bssCfg.authentication.saePasswords);
983 "20-addMacAllow" = mkIf (bssCfg.macAllow != []) (pkgs.writeShellScript "add-mac-allow" ''
985 cat >> "$MAC_ALLOW_FILE" <<EOF
986 ${concatStringsSep "\n" bssCfg.macAllow}
989 "20-addMacAllowFile" = mkIf (bssCfg.macAllowFile != null) (pkgs.writeShellScript "add-mac-allow-file" ''
991 grep -Eo '^([0-9A-Fa-f]{2}[:]){5}([0-9A-Fa-f]{2})' ${escapeShellArg bssCfg.macAllowFile} >> "$MAC_ALLOW_FILE"
993 "20-addMacAllowFromSae" = mkIf (bssCfg.authentication.saeAddToMacAllow && saeMacs != []) (pkgs.writeShellScript "add-mac-allow-from-sae" ''
995 cat >> "$MAC_ALLOW_FILE" <<EOF
996 ${concatStringsSep "\n" saeMacs}
999 # Populate mac allow list from saePasswordsFile
1000 # (filter for lines with mac=; exclude commented lines; filter for real mac-addresses; strip mac=)
1001 "20-addMacAllowFromSaeFile" = mkIf (bssCfg.authentication.saeAddToMacAllow && bssCfg.authentication.saePasswordsFile != null) (pkgs.writeShellScript "add-mac-allow-from-sae-file" ''
1003 grep mac= ${escapeShellArg bssCfg.authentication.saePasswordsFile} \
1005 | grep -Eo 'mac=([0-9A-Fa-f]{2}[:]){5}([0-9A-Fa-f]{2})' \
1006 | sed 's|^mac=||' >> "$MAC_ALLOW_FILE"
1008 "20-addMacDeny" = mkIf (bssCfg.macDeny != []) (pkgs.writeShellScript "add-mac-deny" ''
1010 cat >> "$MAC_DENY_FILE" <<EOF
1011 ${concatStringsSep "\n" bssCfg.macDeny}
1014 "20-addMacDenyFile" = mkIf (bssCfg.macDenyFile != null) (pkgs.writeShellScript "add-mac-deny-file" ''
1016 grep -Eo '^([0-9A-Fa-f]{2}[:]){5}([0-9A-Fa-f]{2})' ${escapeShellArg bssCfg.macDenyFile} >> "$MAC_DENY_FILE"
1018 # Add wpa_passphrase from file
1019 "20-wpaPasswordFile" = mkIf (bssCfg.authentication.wpaPasswordFile != null) (pkgs.writeShellScript "wpa-password-file" ''
1020 HOSTAPD_CONFIG_FILE=$1
1021 cat >> "$HOSTAPD_CONFIG_FILE" <<EOF
1022 wpa_passphrase=$(cat ${escapeShellArg bssCfg.authentication.wpaPasswordFile})
1025 # Add sae passwords from file
1026 "20-saePasswordsFile" = mkIf (bssCfg.authentication.saePasswordsFile != null) (pkgs.writeShellScript "sae-passwords-file" ''
1027 HOSTAPD_CONFIG_FILE=$1
1028 grep -v '\s*#' ${escapeShellArg bssCfg.authentication.saePasswordsFile} \
1029 | sed 's/^/sae_password=/' >> "$HOSTAPD_CONFIG_FILE"
1037 config.settings = let
1038 radioCfg = radioSubmod.config;
1040 driver = radioCfg.driver;
1047 channel = radioCfg.channel;
1048 noscan = radioCfg.noScan;
1049 } // optionalAttrs (radioCfg.countryCode != null) {
1050 country_code = radioCfg.countryCode;
1051 # IEEE 802.11d: Limit to frequencies allowed in country
1053 # IEEE 802.11h: Enable radar detection and DFS (Dynamic Frequency Selection)
1055 } // optionalAttrs radioCfg.wifi4.enable {
1056 # IEEE 802.11n (WiFi 4) related configuration
1058 require_ht = radioCfg.wifi4.require;
1059 ht_capab = concatMapStrings (x: "[${x}]") radioCfg.wifi4.capabilities;
1060 } // optionalAttrs radioCfg.wifi5.enable {
1061 # IEEE 802.11ac (WiFi 5) related configuration
1063 require_vht = radioCfg.wifi5.require;
1064 vht_oper_chwidth = radioCfg.wifi5.operatingChannelWidth;
1065 vht_capab = concatMapStrings (x: "[${x}]") radioCfg.wifi5.capabilities;
1066 } // optionalAttrs radioCfg.wifi6.enable {
1067 # IEEE 802.11ax (WiFi 6) related configuration
1069 require_he = mkIf radioCfg.wifi6.require true;
1070 he_oper_chwidth = radioCfg.wifi6.operatingChannelWidth;
1071 he_su_beamformer = radioCfg.wifi6.singleUserBeamformer;
1072 he_su_beamformee = radioCfg.wifi6.singleUserBeamformee;
1073 he_mu_beamformer = radioCfg.wifi6.multiUserBeamformer;
1074 } // optionalAttrs radioCfg.wifi7.enable {
1075 # IEEE 802.11be (WiFi 7) related configuration
1077 eht_oper_chwidth = radioCfg.wifi7.operatingChannelWidth;
1078 eht_su_beamformer = radioCfg.wifi7.singleUserBeamformer;
1079 eht_su_beamformee = radioCfg.wifi7.singleUserBeamformee;
1080 eht_mu_beamformer = radioCfg.wifi7.multiUserBeamformer;
1088 renamedOptionMessage = message: ''
1090 Refer to the documentation of `services.hostapd.radios` for an example and more information.
1093 (mkRemovedOptionModule ["services" "hostapd" "interface"]
1094 (renamedOptionMessage "All other options for this interface are now set via `services.hostapd.radios.«interface».*`."))
1096 (mkRemovedOptionModule ["services" "hostapd" "driver"]
1097 (renamedOptionMessage "It has been replaced by `services.hostapd.radios.«interface».driver`."))
1098 (mkRemovedOptionModule ["services" "hostapd" "noScan"]
1099 (renamedOptionMessage "It has been replaced by `services.hostapd.radios.«interface».noScan`."))
1100 (mkRemovedOptionModule ["services" "hostapd" "countryCode"]
1101 (renamedOptionMessage "It has been replaced by `services.hostapd.radios.«interface».countryCode`."))
1102 (mkRemovedOptionModule ["services" "hostapd" "hwMode"]
1103 (renamedOptionMessage "It has been replaced by `services.hostapd.radios.«interface».band`."))
1104 (mkRemovedOptionModule ["services" "hostapd" "channel"]
1105 (renamedOptionMessage "It has been replaced by `services.hostapd.radios.«interface».channel`."))
1106 (mkRemovedOptionModule ["services" "hostapd" "extraConfig"]
1107 (renamedOptionMessage ''
1108 It has been replaced by `services.hostapd.radios.«interface».settings` and
1109 `services.hostapd.radios.«interface».networks.«network».settings` respectively
1110 for per-radio and per-network extra configuration. The module now supports a lot more
1111 options inherently, so please re-check whether using settings is still necessary.''))
1113 (mkRemovedOptionModule ["services" "hostapd" "logLevel"]
1114 (renamedOptionMessage "It has been replaced by `services.hostapd.radios.«interface».networks.«network».logLevel`."))
1115 (mkRemovedOptionModule ["services" "hostapd" "group"]
1116 (renamedOptionMessage "It has been replaced by `services.hostapd.radios.«interface».networks.«network».group`."))
1117 (mkRemovedOptionModule ["services" "hostapd" "ssid"]
1118 (renamedOptionMessage "It has been replaced by `services.hostapd.radios.«interface».networks.«network».ssid`."))
1120 (mkRemovedOptionModule ["services" "hostapd" "wpa"]
1121 (renamedOptionMessage "It has been replaced by `services.hostapd.radios.«interface».networks.«network».authentication.mode`."))
1122 (mkRemovedOptionModule ["services" "hostapd" "wpaPassphrase"]
1123 (renamedOptionMessage ''
1124 It has been replaced by `services.hostapd.radios.«interface».networks.«network».authentication.wpaPassword`.
1125 While upgrading your config, please consider using the newer SAE authentication scheme
1126 and one of the new `passwordFile`-like options to avoid putting the password into the world readable nix-store.''))
1129 config = mkIf cfg.enable {
1133 assertion = cfg.radios != {};
1134 message = "At least one radio must be configured with hostapd!";
1138 ++ (concatLists (mapAttrsToList (
1142 assertion = radioCfg.networks != {};
1143 message = "hostapd radio ${radio}: At least one network must be configured!";
1145 # XXX: There could be many more useful assertions about (band == xy) -> ensure other required settings.
1146 # see https://github.com/openwrt/openwrt/blob/539cb5389d9514c99ec1f87bd4465f77c7ed9b93/package/kernel/mac80211/files/lib/netifd/wireless/mac80211.sh#L158
1148 assertion = length (filter (bss: bss == radio) (attrNames radioCfg.networks)) == 1;
1149 message = ''hostapd radio ${radio}: Exactly one network must be named like the radio, for reasons internal to hostapd.'';
1152 assertion = (radioCfg.wifi4.enable && builtins.elem "HT40-" radioCfg.wifi4.capabilities) -> radioCfg.channel != 0;
1153 message = ''hostapd radio ${radio}: using ACS (channel = 0) together with HT40- (wifi4.capabilities) is unsupported by hostapd'';
1157 ++ (concatLists (mapAttrsToList (bss: bssCfg: let
1158 auth = bssCfg.authentication;
1159 countWpaPasswordDefinitions = count (x: x != null) [
1161 auth.wpaPasswordFile
1166 assertion = hasPrefix radio bss;
1167 message = "hostapd radio ${radio} bss ${bss}: The bss (network) name ${bss} is invalid. It must be prefixed by the radio name for reasons internal to hostapd. A valid name would be e.g. ${radio}, ${radio}-1, ...";
1170 assertion = (length (attrNames radioCfg.networks) > 1) -> (bssCfg.bssid != null);
1171 message = ''hostapd radio ${radio} bss ${bss}: bssid must be specified manually (for now) since this radio uses multiple BSS.'';
1174 assertion = countWpaPasswordDefinitions <= 1;
1175 message = ''hostapd radio ${radio} bss ${bss}: must use at most one WPA password option (wpaPassword, wpaPasswordFile, wpaPskFile)'';
1178 assertion = auth.wpaPassword != null -> (stringLength auth.wpaPassword >= 8 && stringLength auth.wpaPassword <= 63);
1179 message = ''hostapd radio ${radio} bss ${bss}: uses a wpaPassword of invalid length (must be in [8,63]).'';
1182 assertion = auth.saePasswords == [] || auth.saePasswordsFile == null;
1183 message = ''hostapd radio ${radio} bss ${bss}: must use only one SAE password option (saePasswords or saePasswordsFile)'';
1186 assertion = auth.mode == "wpa3-sae" -> (auth.saePasswords != [] || auth.saePasswordsFile != null);
1187 message = ''hostapd radio ${radio} bss ${bss}: uses WPA3-SAE which requires defining a sae password option'';
1190 assertion = auth.mode == "wpa3-sae-transition" -> (auth.saePasswords != [] || auth.saePasswordsFile != null) && countWpaPasswordDefinitions == 1;
1191 message = ''hostapd radio ${radio} bss ${bss}: uses WPA3-SAE in transition mode requires defining both a wpa password option and a sae password option'';
1194 assertion = (auth.mode == "wpa2-sha1" || auth.mode == "wpa2-sha256") -> countWpaPasswordDefinitions == 1;
1195 message = ''hostapd radio ${radio} bss ${bss}: uses WPA2-PSK which requires defining a wpa password option'';
1202 environment.systemPackages = [cfg.package];
1204 systemd.services.hostapd = {
1205 description = "IEEE 802.11 Host Access-Point Daemon";
1207 path = [cfg.package];
1208 after = map (radio: "sys-subsystem-net-devices-${utils.escapeSystemdPath radio}.device") (attrNames cfg.radios);
1209 bindsTo = map (radio: "sys-subsystem-net-devices-${utils.escapeSystemdPath radio}.device") (attrNames cfg.radios);
1210 wantedBy = ["multi-user.target"];
1212 # Create merged configuration and acl files for each radio (and their bss's) prior to starting
1213 preStart = concatStringsSep "\n" (mapAttrsToList makeRadioRuntimeFiles cfg.radios);
1216 ExecStart = "${cfg.package}/bin/hostapd ${concatStringsSep " " runtimeConfigFiles}";
1218 ExecReload = "${pkgs.coreutils}/bin/kill -HUP $MAINPID";
1219 RuntimeDirectory = "hostapd";
1222 LockPersonality = true;
1223 MemoryDenyWriteExecute = true;
1224 DevicePolicy = "closed";
1225 DeviceAllow = "/dev/rfkill rw";
1226 NoNewPrivileges = true;
1227 PrivateUsers = false; # hostapd requires true root access.
1229 ProtectClock = true;
1230 ProtectControlGroups = true;
1232 ProtectHostname = true;
1233 ProtectKernelLogs = true;
1234 ProtectKernelModules = true;
1235 ProtectKernelTunables = true;
1236 ProtectProc = "invisible";
1238 ProtectSystem = "strict";
1239 RestrictAddressFamilies = [
1246 RestrictNamespaces = true;
1247 RestrictRealtime = true;
1248 RestrictSUIDSGID = true;
1249 SystemCallArchitectures = "native";
1250 SystemCallFilter = [