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