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}