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}