···4455- FoundationDB now defaults to major version 7.
6677+- Support for WiFi6 (IEEE 802.11ax) and WPA3-SAE-PK was enabled in the `hostapd` package, along with a significant rework of the hostapd module.
88+79## New Services {#sec-release-23.11-new-services}
810911- [MCHPRS](https://github.com/MCHPR/MCHPRS), a multithreaded Minecraft server built for redstone. Available as [services.mchprs](#opt-services.mchprs.enable).
···3133- `writeTextFile` now requires `executable` to be boolean, values like `null` or `""` will now fail to evaluate.
32343335- The latest version of `clonehero` now stores custom content in `~/.clonehero`. See the [migration instructions](https://clonehero.net/2022/11/29/v23-to-v1-migration-instructions.html). Typically, these content files would exist along side the binary, but the previous build used a wrapper script that would store them in `~/.config/unity3d/srylain Inc_/Clone Hero`.
3636+3737+- The `services.hostapd` module was rewritten to support `passwordFile` like options, WPA3-SAE, and management of multiple interfaces. This breaks compatibility with older configurations.
3838+ - `hostapd` is now started with additional systemd sandbox/hardening options for better security.
3939+ - `services.hostapd.interface` was replaced with a per-radio and per-bss configuration scheme using [services.hostapd.radios](#opt-services.hostapd.radios).
4040+ - `services.hostapd.wpa` has been replaced by [services.hostapd.radios.<name>.networks.<name>.authentication.wpaPassword](#opt-services.hostapd.radios._name_.networks._name_.authentication.wpaPassword) and [services.hostapd.radios.<name>.networks.<name>.authentication.saePasswords](#opt-services.hostapd.radios._name_.networks._name_.authentication.saePasswords) which configure WPA2-PSK and WP3-SAE respectively.
4141+ - The default authentication has been changed to WPA3-SAE. Options for other (legacy) schemes are still available.
34423543- `python3.pkgs.fetchPypi` (and `python3Packages.fetchPypi`) has been deprecated in favor of top-level `fetchPypi`.
3644
+1239-176
nixos/modules/services/networking/hostapd.nix
···11{ config, lib, pkgs, utils, ... }:
22+# All hope abandon ye who enter here. hostapd's configuration
33+# format is ... special, and you won't be able to infer any
44+# of their assumptions from just reading the "documentation"
55+# (i.e. the example config). Assume footguns at all points -
66+# to make informed decisions you will probably need to look
77+# at hostapd's code. You have been warned, proceed with care.
88+let
99+ inherit
1010+ (lib)
1111+ attrNames
1212+ attrValues
1313+ concatLists
1414+ concatMap
1515+ concatMapStrings
1616+ concatStringsSep
1717+ count
1818+ escapeShellArg
1919+ filter
2020+ flip
2121+ generators
2222+ getAttr
2323+ hasPrefix
2424+ imap0
2525+ isInt
2626+ isString
2727+ length
2828+ literalExpression
2929+ maintainers
3030+ mapAttrsToList
3131+ mdDoc
3232+ mkDefault
3333+ mkEnableOption
3434+ mkIf
3535+ mkOption
3636+ mkPackageOption
3737+ mkRemovedOptionModule
3838+ optional
3939+ optionalAttrs
4040+ optionalString
4141+ optionals
4242+ singleton
4343+ stringLength
4444+ toLower
4545+ types
4646+ unique
4747+ ;
24833-# TODO:
44-#
55-# asserts
66-# ensure that the nl80211 module is loaded/compiled in the kernel
77-# wpa_supplicant and hostapd on the same wireless interface doesn't make any sense
4949+ cfg = config.services.hostapd;
85099-with lib;
5151+ extraSettingsFormat = {
5252+ type = let
5353+ singleAtom = types.oneOf [ types.bool types.int types.str ];
5454+ atom = types.either singleAtom (types.listOf singleAtom) // {
5555+ description = "atom (bool, int or string) or a list of them for duplicate keys";
5656+ };
5757+ in types.attrsOf atom;
10581111-let
5959+ generate = name: value: pkgs.writeText name (generators.toKeyValue {
6060+ listsAsDuplicateKeys = true;
6161+ mkKeyValue = generators.mkKeyValueDefault {
6262+ mkValueString = v:
6363+ if isInt v then toString v
6464+ else if isString v then v
6565+ else if true == v then "1"
6666+ else if false == v then "0"
6767+ else throw "unsupported type ${builtins.typeOf v}: ${(generators.toPretty {}) v}";
6868+ } "=";
6969+ } value);
7070+ };
12711313- cfg = config.services.hostapd;
7272+ # Generates the header for a single BSS (i.e. WiFi network)
7373+ writeBssHeader = radio: bss: bssIdx: pkgs.writeText "hostapd-radio-${radio}-bss-${bss}.conf" ''
7474+ ''\n''\n# BSS ${toString bssIdx}: ${bss}
7575+ ################################
14761515- escapedInterface = utils.escapeSystemdPath cfg.interface;
7777+ ${if bssIdx == 0 then "interface" else "bss"}=${bss}
7878+ '';
7979+8080+ makeRadioRuntimeFiles = radio: radioCfg:
8181+ pkgs.writeShellScript "make-hostapd-${radio}-files" (''
8282+ set -euo pipefail
16831717- configFile = pkgs.writeText "hostapd.conf" ''
1818- interface=${cfg.interface}
1919- driver=${cfg.driver}
2020- ssid=${cfg.ssid}
2121- hw_mode=${cfg.hwMode}
2222- channel=${toString cfg.channel}
2323- ieee80211n=1
2424- ieee80211ac=1
2525- ${optionalString (cfg.countryCode != null) "country_code=${cfg.countryCode}"}
2626- ${optionalString (cfg.countryCode != null) "ieee80211d=1"}
8484+ hostapd_config_file=/run/hostapd/${escapeShellArg radio}.hostapd.conf
8585+ rm -f "$hostapd_config_file"
8686+ cat > "$hostapd_config_file" <<EOF
8787+ # Radio base configuration: ${radio}
8888+ ################################
27892828- # logging (debug level)
2929- logger_syslog=-1
3030- logger_syslog_level=${toString cfg.logLevel}
3131- logger_stdout=-1
3232- logger_stdout_level=${toString cfg.logLevel}
9090+ EOF
33913434- ctrl_interface=/run/hostapd
3535- ctrl_interface_group=${cfg.group}
9292+ cat ${escapeShellArg (extraSettingsFormat.generate "hostapd-radio-${radio}-extra.conf" radioCfg.settings)} >> "$hostapd_config_file"
9393+ ${concatMapStrings (script: "${script} \"$hostapd_config_file\"\n") (attrValues radioCfg.dynamicConfigScripts)}
9494+ ''
9595+ + concatMapStrings (x: "${x}\n") (imap0 (i: f: f i)
9696+ (mapAttrsToList (bss: bssCfg: bssIdx: ''
9797+ ''\n# BSS configuration: ${bss}
36983737- ${optionalString cfg.wpa ''
3838- wpa=2
3939- wpa_pairwise=CCMP
4040- wpa_passphrase=${cfg.wpaPassphrase}
4141- ''}
4242- ${optionalString cfg.noScan "noscan=1"}
9999+ mac_allow_file=/run/hostapd/${escapeShellArg bss}.mac.allow
100100+ rm -f "$mac_allow_file"
101101+ touch "$mac_allow_file"
431024444- ${cfg.extraConfig}
4545- '' ;
103103+ mac_deny_file=/run/hostapd/${escapeShellArg bss}.mac.deny
104104+ rm -f "$mac_deny_file"
105105+ touch "$mac_deny_file"
461064747-in
107107+ cat ${writeBssHeader radio bss bssIdx} >> "$hostapd_config_file"
108108+ cat ${escapeShellArg (extraSettingsFormat.generate "hostapd-radio-${radio}-bss-${bss}-extra.conf" bssCfg.settings)} >> "$hostapd_config_file"
109109+ ${concatMapStrings (script: "${script} \"$hostapd_config_file\" \"$mac_allow_file\" \"$mac_deny_file\"\n") (attrValues bssCfg.dynamicConfigScripts)}
110110+ '') radioCfg.networks)));
481114949-{
5050- ###### interface
112112+ runtimeConfigFiles = mapAttrsToList (radio: _: "/run/hostapd/${radio}.hostapd.conf") cfg.radios;
113113+in {
114114+ meta.maintainers = with maintainers; [ oddlama ];
5111552116 options = {
5353-54117 services.hostapd = {
118118+ enable = mkEnableOption (mdDoc ''
119119+ Whether to enable hostapd. hostapd is a user space daemon for access point and
120120+ authentication servers. It implements IEEE 802.11 access point management,
121121+ IEEE 802.1X/WPA/WPA2/EAP Authenticators, RADIUS client, EAP server, and RADIUS
122122+ authentication server.
123123+ '');
551245656- enable = mkOption {
5757- type = types.bool;
5858- default = false;
5959- description = lib.mdDoc ''
6060- Enable putting a wireless interface into infrastructure mode,
6161- allowing other wireless devices to associate with the wireless
6262- interface and do wireless networking. A simple access point will
6363- {option}`enable hostapd.wpa`,
6464- {option}`hostapd.wpaPassphrase`, and
6565- {option}`hostapd.ssid`, as well as DHCP on the wireless
6666- interface to provide IP addresses to the associated stations, and
6767- NAT (from the wireless interface to an upstream interface).
125125+ package = mkPackageOption pkgs "hostapd" {};
126126+127127+ radios = mkOption {
128128+ default = {};
129129+ example = literalExpression ''
130130+ {
131131+ # Simple 2.4GHz AP
132132+ wlp2s0 = {
133133+ # countryCode = "US";
134134+ networks.wlp2s0 = {
135135+ ssid = "AP 1";
136136+ authentication.saePasswords = [{ password = "a flakey password"; }]; # Use saePasswordsFile if possible.
137137+ };
138138+ };
139139+140140+ # WiFi 5 (5GHz) with two advertised networks
141141+ wlp3s0 = {
142142+ band = "5g";
143143+ channel = 0; # Enable automatic channel selection (ACS). Use only if your hardware supports it.
144144+ # countryCode = "US";
145145+ networks.wlp3s0 = {
146146+ ssid = "My AP";
147147+ authentication.saePasswords = [{ password = "a flakey password"; }]; # Use saePasswordsFile if possible.
148148+ };
149149+ networks.wlp3s0-1 = {
150150+ ssid = "Open AP with WiFi5";
151151+ authentication.mode = "none";
152152+ };
153153+ };
154154+155155+ # Legacy WPA2 example
156156+ wlp4s0 = {
157157+ # countryCode = "US";
158158+ networks.wlp4s0 = {
159159+ ssid = "AP 2";
160160+ authentication = {
161161+ mode = "wpa2-sha256";
162162+ wpaPassword = "a flakey password"; # Use wpaPasswordFile if possible.
163163+ };
164164+ };
165165+ };
166166+ }
68167 '';
6969- };
168168+ description = mdDoc ''
169169+ This option allows you to define APs for one or multiple physical radios.
170170+ At least one radio must be specified.
701717171- interface = mkOption {
7272- example = "wlp2s0";
7373- type = types.str;
7474- description = lib.mdDoc ''
7575- The interfaces {command}`hostapd` will use.
172172+ For each radio, hostapd requires a separate logical interface (like wlp3s0, wlp3s1, ...).
173173+ A default interface is usually be created automatically by your system, but to use
174174+ multiple radios of a single device, it may be required to create additional logical interfaces
175175+ for example by using {option}`networking.wlanInterfaces`.
176176+177177+ Each physical radio can only support a single hardware-mode that is configured via
178178+ ({option}`services.hostapd.radios.<radio>.band`). To create a dual-band
179179+ or tri-band AP, you will have to use a device that has multiple physical radios
180180+ and supports configuring multiple APs (Refer to valid interface combinations in
181181+ {command}`iw list`).
76182 '';
7777- };
183183+ type = types.attrsOf (types.submodule (radioSubmod: {
184184+ options = {
185185+ driver = mkOption {
186186+ default = "nl80211";
187187+ example = "none";
188188+ type = types.str;
189189+ description = mdDoc ''
190190+ The driver {command}`hostapd` will use.
191191+ {var}`nl80211` is used with all Linux mac80211 drivers.
192192+ {var}`none` is used if building a standalone RADIUS server that does
193193+ not control any wireless/wired driver.
194194+ Most applications will probably use the default.
195195+ '';
196196+ };
197197+198198+ noScan = mkOption {
199199+ type = types.bool;
200200+ default = false;
201201+ description = mdDoc ''
202202+ Disables scan for overlapping BSSs in HT40+/- mode.
203203+ Caution: turning this on will likely violate regulatory requirements!
204204+ '';
205205+ };
206206+207207+ countryCode = mkOption {
208208+ default = null;
209209+ example = "US";
210210+ type = types.nullOr types.str;
211211+ description = mdDoc ''
212212+ Country code (ISO/IEC 3166-1). Used to set regulatory domain.
213213+ Set as needed to indicate country in which device is operating.
214214+ This can limit available channels and transmit power.
215215+ These two octets are used as the first two octets of the Country String
216216+ (dot11CountryString).
217217+218218+ Setting this will force you to also enable IEEE 802.11d and IEEE 802.11h.
219219+220220+ IEEE 802.11d: This advertises the countryCode and the set of allowed channels
221221+ and transmit power levels based on the regulatory limits.
222222+223223+ IEEE802.11h: This enables radar detection and DFS (Dynamic Frequency Selection)
224224+ support if available. DFS support is required on outdoor 5 GHz channels in most
225225+ countries of the world.
226226+ '';
227227+ };
228228+229229+ band = mkOption {
230230+ default = "2g";
231231+ type = types.enum ["2g" "5g" "6g" "60g"];
232232+ description = mdDoc ''
233233+ Specifies the frequency band to use, possible values are 2g for 2.4 GHz,
234234+ 5g for 5 GHz, 6g for 6 GHz and 60g for 60 GHz.
235235+ '';
236236+ };
237237+238238+ channel = mkOption {
239239+ default = 7;
240240+ example = 11;
241241+ type = types.int;
242242+ description = mdDoc ''
243243+ The channel to operate on. Use 0 to enable ACS (Automatic Channel Selection).
244244+ Beware that not every device supports ACS in which case {command}`hostapd`
245245+ will fail to start.
246246+ '';
247247+ };
248248+249249+ settings = mkOption {
250250+ default = {};
251251+ example = { acs_exclude_dfs = true; };
252252+ type = types.submodule {
253253+ freeformType = extraSettingsFormat.type;
254254+ };
255255+ description = mdDoc ''
256256+ Extra configuration options to put at the end of global initialization, before defining BSSs.
257257+ To find out which options are global and which are per-bss you have to read hostapd's source code,
258258+ which is non-trivial and not documented otherwise.
259259+260260+ Lists will be converted to multiple definitions of the same key, and booleans to 0/1.
261261+ Otherwise, the inputs are not modified or checked for correctness.
262262+ '';
263263+ };
264264+265265+ dynamicConfigScripts = mkOption {
266266+ default = {};
267267+ type = types.attrsOf types.path;
268268+ example = literalExpression ''
269269+ {
270270+ exampleDynamicConfig = pkgs.writeShellScript "dynamic-config" '''
271271+ HOSTAPD_CONFIG=$1
272272+273273+ cat >> "$HOSTAPD_CONFIG" << EOF
274274+ # Add some dynamically generated statements here,
275275+ # for example based on the physical adapter in use
276276+ EOF
277277+ ''';
278278+ }
279279+ '';
280280+ description = mdDoc ''
281281+ All of these scripts will be executed in lexicographical order before hostapd
282282+ is started, right after the global segment was generated and may dynamically
283283+ append global options the generated configuration file.
284284+285285+ The first argument will point to the configuration file that you may append to.
286286+ '';
287287+ };
288288+289289+ #### IEEE 802.11n (WiFi 4) related configuration
290290+291291+ wifi4 = {
292292+ enable = mkOption {
293293+ default = true;
294294+ type = types.bool;
295295+ description = mdDoc ''
296296+ Enables support for IEEE 802.11n (WiFi 4, HT).
297297+ This is enabled by default, since the vase majority of devices
298298+ are expected to support this.
299299+ '';
300300+ };
301301+302302+ capabilities = mkOption {
303303+ type = types.listOf types.str;
304304+ default = ["HT40" "HT40-" "SHORT-GI-20" "SHORT-GI-40"];
305305+ example = ["LDPC" "HT40+" "HT40-" "GF" "SHORT-GI-20" "SHORT-GI-40" "TX-STBC" "RX-STBC1"];
306306+ description = mdDoc ''
307307+ HT (High Throughput) capabilities given as a list of flags.
308308+ Please refer to the hostapd documentation for allowed values and
309309+ only set values supported by your physical adapter.
310310+311311+ The default contains common values supported by most adapters.
312312+ '';
313313+ };
314314+315315+ require = mkOption {
316316+ default = false;
317317+ type = types.bool;
318318+ description = mdDoc "Require stations (clients) to support WiFi 4 (HT) and disassociate them if they don't.";
319319+ };
320320+ };
321321+322322+ #### IEEE 802.11ac (WiFi 5) related configuration
323323+324324+ wifi5 = {
325325+ enable = mkOption {
326326+ default = true;
327327+ type = types.bool;
328328+ description = mdDoc "Enables support for IEEE 802.11ac (WiFi 5, VHT)";
329329+ };
330330+331331+ capabilities = mkOption {
332332+ type = types.listOf types.str;
333333+ default = [];
334334+ example = ["SHORT-GI-80" "TX-STBC-2BY1" "RX-STBC-1" "RX-ANTENNA-PATTERN" "TX-ANTENNA-PATTERN"];
335335+ description = mdDoc ''
336336+ VHT (Very High Throughput) capabilities given as a list of flags.
337337+ Please refer to the hostapd documentation for allowed values and
338338+ only set values supported by your physical adapter.
339339+ '';
340340+ };
341341+342342+ require = mkOption {
343343+ default = false;
344344+ type = types.bool;
345345+ description = mdDoc "Require stations (clients) to support WiFi 5 (VHT) and disassociate them if they don't.";
346346+ };
347347+348348+ operatingChannelWidth = mkOption {
349349+ default = "20or40";
350350+ type = types.enum ["20or40" "80" "160" "80+80"];
351351+ apply = x:
352352+ getAttr x {
353353+ "20or40" = 0;
354354+ "80" = 1;
355355+ "160" = 2;
356356+ "80+80" = 3;
357357+ };
358358+ description = mdDoc ''
359359+ Determines the operating channel width for VHT.
360360+361361+ - {var}`"20or40"`: 20 or 40 MHz operating channel width
362362+ - {var}`"80"`: 80 MHz channel width
363363+ - {var}`"160"`: 160 MHz channel width
364364+ - {var}`"80+80"`: 80+80 MHz channel width
365365+ '';
366366+ };
367367+ };
368368+369369+ #### IEEE 802.11ax (WiFi 6) related configuration
370370+371371+ wifi6 = {
372372+ enable = mkOption {
373373+ default = false;
374374+ type = types.bool;
375375+ description = mdDoc "Enables support for IEEE 802.11ax (WiFi 6, HE)";
376376+ };
377377+378378+ require = mkOption {
379379+ default = false;
380380+ type = types.bool;
381381+ description = mdDoc "Require stations (clients) to support WiFi 6 (HE) and disassociate them if they don't.";
382382+ };
383383+384384+ singleUserBeamformer = mkOption {
385385+ default = false;
386386+ type = types.bool;
387387+ description = mdDoc "HE single user beamformer support";
388388+ };
389389+390390+ singleUserBeamformee = mkOption {
391391+ default = false;
392392+ type = types.bool;
393393+ description = mdDoc "HE single user beamformee support";
394394+ };
395395+396396+ multiUserBeamformer = mkOption {
397397+ default = false;
398398+ type = types.bool;
399399+ description = mdDoc "HE multi user beamformee support";
400400+ };
401401+402402+ operatingChannelWidth = mkOption {
403403+ default = "20or40";
404404+ type = types.enum ["20or40" "80" "160" "80+80"];
405405+ apply = x:
406406+ getAttr x {
407407+ "20or40" = 0;
408408+ "80" = 1;
409409+ "160" = 2;
410410+ "80+80" = 3;
411411+ };
412412+ description = mdDoc ''
413413+ Determines the operating channel width for HE.
414414+415415+ - {var}`"20or40"`: 20 or 40 MHz operating channel width
416416+ - {var}`"80"`: 80 MHz channel width
417417+ - {var}`"160"`: 160 MHz channel width
418418+ - {var}`"80+80"`: 80+80 MHz channel width
419419+ '';
420420+ };
421421+ };
422422+423423+ #### IEEE 802.11be (WiFi 7) related configuration
424424+425425+ wifi7 = {
426426+ enable = mkOption {
427427+ default = false;
428428+ type = types.bool;
429429+ description = mdDoc ''
430430+ Enables support for IEEE 802.11be (WiFi 7, EHT). This is currently experimental
431431+ and requires you to manually enable CONFIG_IEEE80211BE when building hostapd.
432432+ '';
433433+ };
434434+435435+ singleUserBeamformer = mkOption {
436436+ default = false;
437437+ type = types.bool;
438438+ description = mdDoc "EHT single user beamformer support";
439439+ };
440440+441441+ singleUserBeamformee = mkOption {
442442+ default = false;
443443+ type = types.bool;
444444+ description = mdDoc "EHT single user beamformee support";
445445+ };
446446+447447+ multiUserBeamformer = mkOption {
448448+ default = false;
449449+ type = types.bool;
450450+ description = mdDoc "EHT multi user beamformee support";
451451+ };
452452+453453+ operatingChannelWidth = mkOption {
454454+ default = "20or40";
455455+ type = types.enum ["20or40" "80" "160" "80+80"];
456456+ apply = x:
457457+ getAttr x {
458458+ "20or40" = 0;
459459+ "80" = 1;
460460+ "160" = 2;
461461+ "80+80" = 3;
462462+ };
463463+ description = mdDoc ''
464464+ Determines the operating channel width for EHT.
465465+466466+ - {var}`"20or40"`: 20 or 40 MHz operating channel width
467467+ - {var}`"80"`: 80 MHz channel width
468468+ - {var}`"160"`: 160 MHz channel width
469469+ - {var}`"80+80"`: 80+80 MHz channel width
470470+ '';
471471+ };
472472+ };
473473+474474+ #### BSS definitions
475475+476476+ networks = mkOption {
477477+ default = {};
478478+ example = literalExpression ''
479479+ {
480480+ wlp2s0 = {
481481+ ssid = "Primary advertised network";
482482+ authentication.saePasswords = [{ password = "a flakey password"; }]; # Use saePasswordsFile if possible.
483483+ };
484484+ wlp2s0-1 = {
485485+ ssid = "Secondary advertised network (Open)";
486486+ authentication.mode = "none";
487487+ };
488488+ }
489489+ '';
490490+ description = mdDoc ''
491491+ This defines a BSS, colloquially known as a WiFi network.
492492+ You have to specify at least one.
493493+ '';
494494+ type = types.attrsOf (types.submodule (bssSubmod: {
495495+ options = {
496496+ logLevel = mkOption {
497497+ default = 2;
498498+ type = types.int;
499499+ description = mdDoc ''
500500+ Levels (minimum value for logged events):
501501+ 0 = verbose debugging
502502+ 1 = debugging
503503+ 2 = informational messages
504504+ 3 = notification
505505+ 4 = warning
506506+ '';
507507+ };
508508+509509+ group = mkOption {
510510+ default = "wheel";
511511+ example = "network";
512512+ type = types.str;
513513+ description = mdDoc ''
514514+ Members of this group can access the control socket for this interface.
515515+ '';
516516+ };
517517+518518+ utf8Ssid = mkOption {
519519+ default = true;
520520+ type = types.bool;
521521+ description = mdDoc "Whether the SSID is to be interpreted using UTF-8 encoding.";
522522+ };
523523+524524+ ssid = mkOption {
525525+ example = "❄️ cool ❄️";
526526+ type = types.str;
527527+ description = mdDoc "SSID to be used in IEEE 802.11 management frames.";
528528+ };
529529+530530+ bssid = mkOption {
531531+ type = types.nullOr types.str;
532532+ default = null;
533533+ example = "11:22:33:44:55:66";
534534+ description = mdDoc ''
535535+ Specifies the BSSID for this BSS. Usually determined automatically,
536536+ but for now you have to manually specify them when using multiple BSS.
537537+ Try assigning related addresses from the locally administered MAC address ranges,
538538+ by reusing the hardware address but replacing the second nibble with 2, 6, A or E.
539539+ (e.g. if real address is `XX:XX:XX:XX:XX`, try `X2:XX:XX:XX:XX:XX`, `X6:XX:XX:XX:XX:XX`, ...
540540+ for the second, third, ... BSS)
541541+ '';
542542+ };
543543+544544+ macAcl = mkOption {
545545+ default = "deny";
546546+ type = types.enum ["deny" "allow" "radius"];
547547+ apply = x:
548548+ getAttr x {
549549+ "deny" = 0;
550550+ "allow" = 1;
551551+ "radius" = 2;
552552+ };
553553+ description = mdDoc ''
554554+ Station MAC address -based authentication. The following modes are available:
555555+556556+ - {var}`"deny"`: Allow unless listed in {option}`macDeny` (default)
557557+ - {var}`"allow"`: Deny unless listed in {option}`macAllow`
558558+ - {var}`"radius"`: Use external radius server, but check both {option}`macAllow` and {option}`macDeny` first
559559+560560+ Please note that this kind of access control requires a driver that uses
561561+ hostapd to take care of management frame processing and as such, this can be
562562+ used with driver=hostap or driver=nl80211, but not with driver=atheros.
563563+ '';
564564+ };
565565+566566+ macAllow = mkOption {
567567+ type = types.listOf types.str;
568568+ default = [];
569569+ example = ["11:22:33:44:55:66"];
570570+ description = mdDoc ''
571571+ Specifies the MAC addresses to allow if {option}`macAcl` is set to {var}`"allow"` or {var}`"radius"`.
572572+ These values will be world-readable in the Nix store. Values will automatically be merged with
573573+ {option}`macAllowFile` if necessary.
574574+ '';
575575+ };
576576+577577+ macAllowFile = mkOption {
578578+ type = types.nullOr types.path;
579579+ default = null;
580580+ description = mdDoc ''
581581+ Specifies a file containing the MAC addresses to allow if {option}`macAcl` is set to {var}`"allow"` or {var}`"radius"`.
582582+ The file should contain exactly one MAC address per line. Comments and empty lines are ignored,
583583+ only lines starting with a valid MAC address will be considered (e.g. `11:22:33:44:55:66`) and
584584+ any content after the MAC address is ignored.
585585+ '';
586586+ };
587587+588588+ macDeny = mkOption {
589589+ type = types.listOf types.str;
590590+ default = [];
591591+ example = ["11:22:33:44:55:66"];
592592+ description = mdDoc ''
593593+ Specifies the MAC addresses to deny if {option}`macAcl` is set to {var}`"deny"` or {var}`"radius"`.
594594+ These values will be world-readable in the Nix store. Values will automatically be merged with
595595+ {option}`macDenyFile` if necessary.
596596+ '';
597597+ };
598598+599599+ macDenyFile = mkOption {
600600+ type = types.nullOr types.path;
601601+ default = null;
602602+ description = mdDoc ''
603603+ Specifies a file containing the MAC addresses to deny if {option}`macAcl` is set to {var}`"deny"` or {var}`"radius"`.
604604+ The file should contain exactly one MAC address per line. Comments and empty lines are ignored,
605605+ only lines starting with a valid MAC address will be considered (e.g. `11:22:33:44:55:66`) and
606606+ any content after the MAC address is ignored.
607607+ '';
608608+ };
609609+610610+ ignoreBroadcastSsid = mkOption {
611611+ default = "disabled";
612612+ type = types.enum ["disabled" "empty" "clear"];
613613+ apply = x:
614614+ getAttr x {
615615+ "disabled" = 0;
616616+ "empty" = 1;
617617+ "clear" = 2;
618618+ };
619619+ description = mdDoc ''
620620+ Send empty SSID in beacons and ignore probe request frames that do not
621621+ specify full SSID, i.e., require stations to know SSID. Note that this does
622622+ not increase security, since your clients will then broadcast the SSID instead,
623623+ which can increase congestion.
624624+625625+ - {var}`"disabled"`: Advertise ssid normally.
626626+ - {var}`"empty"`: send empty (length=0) SSID in beacon and ignore probe request for broadcast SSID
627627+ - {var}`"clear"`: clear SSID (ASCII 0), but keep the original length (this may be required with some
628628+ legacy clients that do not support empty SSID) and ignore probe requests for broadcast SSID. Only
629629+ use this if empty does not work with your clients.
630630+ '';
631631+ };
632632+633633+ apIsolate = mkOption {
634634+ default = false;
635635+ type = types.bool;
636636+ description = mdDoc ''
637637+ Isolate traffic between stations (clients) and prevent them from
638638+ communicating with each other.
639639+ '';
640640+ };
641641+642642+ settings = mkOption {
643643+ default = {};
644644+ example = { multi_ap = true; };
645645+ type = types.submodule {
646646+ freeformType = extraSettingsFormat.type;
647647+ };
648648+ description = mdDoc ''
649649+ Extra configuration options to put at the end of this BSS's defintion in the
650650+ hostapd.conf for the associated interface. To find out which options are global
651651+ and which are per-bss you have to read hostapd's source code, which is non-trivial
652652+ and not documented otherwise.
653653+654654+ Lists will be converted to multiple definitions of the same key, and booleans to 0/1.
655655+ Otherwise, the inputs are not modified or checked for correctness.
656656+ '';
657657+ };
658658+659659+ dynamicConfigScripts = mkOption {
660660+ default = {};
661661+ type = types.attrsOf types.path;
662662+ example = literalExpression ''
663663+ {
664664+ exampleDynamicConfig = pkgs.writeShellScript "dynamic-config" '''
665665+ HOSTAPD_CONFIG=$1
666666+ # These always exist, but may or may not be used depending on the actual configuration
667667+ MAC_ALLOW_FILE=$2
668668+ MAC_DENY_FILE=$3
669669+670670+ cat >> "$HOSTAPD_CONFIG" << EOF
671671+ # Add some dynamically generated statements here
672672+ EOF
673673+ ''';
674674+ }
675675+ '';
676676+ description = mdDoc ''
677677+ All of these scripts will be executed in lexicographical order before hostapd
678678+ is started, right after the bss segment was generated and may dynamically
679679+ append bss options to the generated configuration file.
680680+681681+ The first argument will point to the configuration file that you may append to.
682682+ The second and third argument will point to this BSS's MAC allow and MAC deny file respectively.
683683+ '';
684684+ };
685685+686686+ #### IEEE 802.11i (WPA) configuration
687687+688688+ authentication = {
689689+ mode = mkOption {
690690+ default = "wpa3-sae";
691691+ type = types.enum ["none" "wpa2-sha256" "wpa3-sae-transition" "wpa3-sae"];
692692+ description = mdDoc ''
693693+ Selects the authentication mode for this AP.
694694+695695+ - {var}`"none"`: Don't configure any authentication. This will disable wpa alltogether
696696+ and create an open AP. Use {option}`settings` together with this option if you
697697+ want to configure the authentication manually. Any password options will still be
698698+ effective, if set.
699699+ - {var}`"wpa2-sha256"`: WPA2-Personal using SHA256 (IEEE 802.11i/RSN). Passwords are set
700700+ using {option}`wpaPassword` or preferably by {option}`wpaPasswordFile` or {option}`wpaPskFile`.
701701+ - {var}`"wpa3-sae-transition"`: Use WPA3-Personal (SAE) if possible, otherwise fallback
702702+ to WPA2-SHA256. Only use if necessary and switch to the newer WPA3-SAE when possible.
703703+ You will have to specify both {option}`wpaPassword` and {option}`saePasswords` (or one of their alternatives).
704704+ - {var}`"wpa3-sae"`: Use WPA3-Personal (SAE). This is currently the recommended way to
705705+ setup a secured WiFi AP (as of March 2023) and therefore the default. Passwords are set
706706+ using either {option}`saePasswords` or preferably {option}`saePasswordsFile`.
707707+ '';
708708+ };
709709+710710+ pairwiseCiphers = mkOption {
711711+ default = ["CCMP"];
712712+ example = ["CCMP-256" "GCMP-256"];
713713+ type = types.listOf types.str;
714714+ description = mdDoc ''
715715+ Set of accepted cipher suites (encryption algorithms) for pairwise keys (unicast packets).
716716+ By default this allows just CCMP, which is the only commonly supported secure option.
717717+ Use {option}`enableRecommendedPairwiseCiphers` to also enable newer recommended ciphers.
787187979- noScan = mkOption {
8080- type = types.bool;
8181- default = false;
8282- description = lib.mdDoc ''
8383- Do not scan for overlapping BSSs in HT40+/- mode.
8484- Caution: turning this on will violate regulatory requirements!
8585- '';
8686- };
719719+ Please refer to the hostapd documentation for allowed values. Generally, only
720720+ CCMP or GCMP modes should be considered safe options. Most devices support CCMP while
721721+ GCMP is often only available with devices supporting WiFi 5 (IEEE 802.11ac) or higher.
722722+ '';
723723+ };
877248888- driver = mkOption {
8989- default = "nl80211";
9090- example = "hostapd";
9191- type = types.str;
9292- description = lib.mdDoc ''
9393- Which driver {command}`hostapd` will use.
9494- Most applications will probably use the default.
9595- '';
9696- };
725725+ enableRecommendedPairwiseCiphers = mkOption {
726726+ default = false;
727727+ example = true;
728728+ type = types.bool;
729729+ description = mdDoc ''
730730+ Additionally enable the recommended set of pairwise ciphers.
731731+ This enables newer secure ciphers, additionally to those defined in {option}`pairwiseCiphers`.
732732+ You will have to test whether your hardware supports these by trial-and-error, because
733733+ even if `iw list` indicates hardware support, your driver might not expose it.
734734+735735+ Beware {command}`hostapd` will most likely not return a useful error message in case
736736+ this is enabled despite the driver or hardware not supporting the newer ciphers.
737737+ Look out for messages like `Failed to set beacon parameters`.
738738+ '';
739739+ };
740740+741741+ wpaPassword = mkOption {
742742+ default = null;
743743+ example = "a flakey password";
744744+ type = types.nullOr types.str;
745745+ description = mdDoc ''
746746+ Sets the password for WPA-PSK that will be converted to the pre-shared key.
747747+ The password length must be in the range [8, 63] characters. While some devices
748748+ may allow arbitrary characters (such as UTF-8) to be used, but the standard specifies
749749+ that each character in the passphrase must be an ASCII character in the range [0x20, 0x7e]
750750+ (IEEE Std. 802.11i-2004, Annex H.4.1). Use emojis at your own risk.
977519898- ssid = mkOption {
9999- default = config.system.nixos.distroId;
100100- defaultText = literalExpression "config.system.nixos.distroId";
101101- example = "mySpecialSSID";
102102- type = types.str;
103103- description = lib.mdDoc "SSID to be used in IEEE 802.11 management frames.";
104104- };
752752+ Not used when {option}`mode` is {var}`"wpa3-sae"`.
105753106106- hwMode = mkOption {
107107- default = "g";
108108- type = types.enum [ "a" "b" "g" ];
109109- description = lib.mdDoc ''
110110- Operation mode.
111111- (a = IEEE 802.11a, b = IEEE 802.11b, g = IEEE 802.11g).
112112- '';
113113- };
754754+ Warning: This password will get put into a world-readable file in the Nix store!
755755+ Using {option}`wpaPasswordFile` or {option}`wpaPskFile` instead is recommended.
756756+ '';
757757+ };
114758115115- channel = mkOption {
116116- default = 7;
117117- example = 11;
118118- type = types.int;
119119- description = lib.mdDoc ''
120120- Channel number (IEEE 802.11)
121121- Please note that some drivers do not use this value from
122122- {command}`hostapd` and the channel will need to be configured
123123- separately with {command}`iwconfig`.
124124- '';
125125- };
759759+ wpaPasswordFile = mkOption {
760760+ default = null;
761761+ type = types.nullOr types.path;
762762+ description = mdDoc ''
763763+ Sets the password for WPA-PSK. Follows the same rules as {option}`wpaPassword`,
764764+ but reads the password from the given file to prevent the password from being
765765+ put into the Nix store.
126766127127- group = mkOption {
128128- default = "wheel";
129129- example = "network";
130130- type = types.str;
131131- description = lib.mdDoc ''
132132- Members of this group can control {command}`hostapd`.
133133- '';
134134- };
767767+ Not used when {option}`mode` is {var}`"wpa3-sae"`.
768768+ '';
769769+ };
135770136136- wpa = mkOption {
137137- type = types.bool;
138138- default = true;
139139- description = lib.mdDoc ''
140140- Enable WPA (IEEE 802.11i/D3.0) to authenticate with the access point.
141141- '';
142142- };
771771+ wpaPskFile = mkOption {
772772+ default = null;
773773+ type = types.nullOr types.path;
774774+ description = mdDoc ''
775775+ Sets the password(s) for WPA-PSK. Similar to {option}`wpaPasswordFile`,
776776+ but additionally allows specifying multiple passwords, and some other options.
143777144144- wpaPassphrase = mkOption {
145145- default = "my_sekret";
146146- example = "any_64_char_string";
147147- type = types.str;
148148- description = lib.mdDoc ''
149149- WPA-PSK (pre-shared-key) passphrase. Clients will need this
150150- passphrase to associate with this access point.
151151- Warning: This passphrase will get put into a world-readable file in
152152- the Nix store!
153153- '';
154154- };
778778+ Each line, except for empty lines and lines starting with #, must contain a
779779+ MAC address and either a 64-hex-digit PSK or a password separated with a space.
780780+ The password must follow the same rules as outlined in {option}`wpaPassword`.
781781+ The special MAC address `00:00:00:00:00:00` can be used to configure PSKs
782782+ that any client can use.
155783156156- logLevel = mkOption {
157157- default = 2;
158158- type = types.int;
159159- description = lib.mdDoc ''
160160- Levels (minimum value for logged events):
161161- 0 = verbose debugging
162162- 1 = debugging
163163- 2 = informational messages
164164- 3 = notification
165165- 4 = warning
166166- '';
167167- };
784784+ An optional key identifier can be added by prefixing the line with `keyid=<keyid_string>`
785785+ An optional VLAN ID can be specified by prefixing the line with `vlanid=<VLAN ID>`.
786786+ An optional WPS tag can be added by prefixing the line with `wps=<0/1>` (default: 0).
787787+ Any matching entry with that tag will be used when generating a PSK for a WPS Enrollee
788788+ instead of generating a new random per-Enrollee PSK.
789789+790790+ Not used when {option}`mode` is {var}`"wpa3-sae"`.
791791+ '';
792792+ };
793793+794794+ saePasswords = mkOption {
795795+ default = [];
796796+ example = literalExpression ''
797797+ [
798798+ # Any client may use these passwords
799799+ { password = "Wi-Figure it out"; }
800800+ { password = "second password for everyone"; mac = "ff:ff:ff:ff:ff:ff"; }
801801+802802+ # Only the client with MAC-address 11:22:33:44:55:66 can use this password
803803+ { password = "sekret pazzword"; mac = "11:22:33:44:55:66"; }
804804+ ]
805805+ '';
806806+ description = mdDoc ''
807807+ Sets allowed passwords for WPA3-SAE.
808808+809809+ The last matching (based on peer MAC address and identifier) entry is used to
810810+ select which password to use. An empty string has the special meaning of
811811+ removing all previously added entries.
812812+813813+ Warning: These entries will get put into a world-readable file in
814814+ the Nix store! Using {option}`saePasswordFile` instead is recommended.
815815+816816+ Not used when {option}`mode` is {var}`"wpa2-sha256"`.
817817+ '';
818818+ type = types.listOf (types.submodule {
819819+ options = {
820820+ password = mkOption {
821821+ example = "a flakey password";
822822+ type = types.str;
823823+ description = mdDoc ''
824824+ The password for this entry. SAE technically imposes no restrictions on
825825+ password length or character set. But due to limitations of {command}`hostapd`'s
826826+ config file format, a true newline character cannot be parsed.
827827+828828+ Warning: This password will get put into a world-readable file in
829829+ the Nix store! Using {option}`wpaPasswordFile` or {option}`wpaPskFile` is recommended.
830830+ '';
831831+ };
832832+833833+ mac = mkOption {
834834+ default = null;
835835+ example = "11:22:33:44:55:66";
836836+ type = types.nullOr types.str;
837837+ description = mdDoc ''
838838+ If this attribute is not included, or if is set to the wildcard address (`ff:ff:ff:ff:ff:ff`),
839839+ the entry is available for any station (client) to use. If a specific peer MAC address is included,
840840+ only a station with that MAC address is allowed to use the entry.
841841+ '';
842842+ };
843843+844844+ vlanid = mkOption {
845845+ default = null;
846846+ example = 1;
847847+ type = types.nullOr types.int;
848848+ description = mdDoc "If this attribute is given, all clients using this entry will get tagged with the given VLAN ID.";
849849+ };
850850+851851+ pk = mkOption {
852852+ default = null;
853853+ example = "";
854854+ type = types.nullOr types.str;
855855+ description = mdDoc ''
856856+ If this attribute is given, SAE-PK will be enabled for this connection.
857857+ This prevents evil-twin attacks, but a public key is required additionally to connect.
858858+ (Essentially adds pubkey authentication such that the client can verify identity of the AP)
859859+ '';
860860+ };
861861+862862+ id = mkOption {
863863+ default = null;
864864+ example = "";
865865+ type = types.nullOr types.str;
866866+ description = mdDoc ''
867867+ If this attribute is given with non-zero length, it will set the password identifier
868868+ for this entry. It can then only be used with that identifier.
869869+ '';
870870+ };
871871+ };
872872+ });
873873+ };
874874+875875+ saePasswordsFile = mkOption {
876876+ default = null;
877877+ type = types.nullOr types.path;
878878+ description = mdDoc ''
879879+ Sets the password for WPA3-SAE. Follows the same rules as {option}`saePasswords`,
880880+ but reads the entries from the given file to prevent them from being
881881+ put into the Nix store.
882882+883883+ One entry per line, empty lines and lines beginning with # will be ignored.
884884+ Each line must match the following format, although the order of optional
885885+ parameters doesn't matter:
886886+ `<password>[|mac=<peer mac>][|vlanid=<VLAN ID>][|pk=<m:ECPrivateKey-base64>][|id=<identifier>]`
887887+888888+ Not used when {option}`mode` is {var}`"wpa2-sha256"`.
889889+ '';
890890+ };
891891+892892+ saeAddToMacAllow = mkOption {
893893+ type = types.bool;
894894+ default = false;
895895+ description = mdDoc ''
896896+ If set, all sae password entries that have a non-wildcard MAC associated to
897897+ them will additionally be used to populate the MAC allow list. This is
898898+ additional to any entries set via {option}`macAllow` or {option}`macAllowFile`.
899899+ '';
900900+ };
901901+ };
902902+903903+ managementFrameProtection = mkOption {
904904+ default = "required";
905905+ type = types.enum ["disabled" "optional" "required"];
906906+ apply = x:
907907+ getAttr x {
908908+ "disabled" = 0;
909909+ "optional" = 1;
910910+ "required" = 2;
911911+ };
912912+ description = mdDoc ''
913913+ Management frame protection (MFP) authenticates management frames
914914+ to prevent deauthentication (or related) attacks.
915915+916916+ - {var}`"disabled"`: No management frame protection
917917+ - {var}`"optional"`: Use MFP if a connection allows it
918918+ - {var}`"required"`: Force MFP for all clients
919919+ '';
920920+ };
921921+ };
922922+923923+ config = let
924924+ bss = bssSubmod.name;
925925+ bssCfg = bssSubmod.config;
926926+927927+ pairwiseCiphers =
928928+ concatStringsSep " " (unique (bssCfg.authentication.pairwiseCiphers
929929+ ++ optionals bssCfg.authentication.enableRecommendedPairwiseCiphers ["CCMP" "CCMP-256" "GCMP" "GCMP-256"]));
930930+ in {
931931+ settings = {
932932+ ssid = bssCfg.ssid;
933933+ utf8_ssid = bssCfg.ssid;
934934+935935+ logger_syslog = mkDefault (-1);
936936+ logger_syslog_level = bssCfg.logLevel;
937937+ logger_stdout = mkDefault (-1);
938938+ logger_stdout_level = bssCfg.logLevel;
939939+ ctrl_interface = mkDefault "/run/hostapd";
940940+ ctrl_interface_group = bssCfg.group;
941941+942942+ macaddr_acl = bssCfg.macAcl;
943943+944944+ ignore_broadcast_ssid = bssCfg.ignoreBroadcastSsid;
945945+946946+ # IEEE 802.11i (authentication) related configuration
947947+ # Encrypt management frames to protect against deauthentication and similar attacks
948948+ ieee80211w = bssCfg.managementFrameProtection;
949949+950950+ # Only allow WPA by default and disable insecure WEP
951951+ auth_algs = mkDefault 1;
952952+ # Always enable QoS, which is required for 802.11n and above
953953+ wmm_enabled = mkDefault true;
954954+ ap_isolate = bssCfg.apIsolate;
955955+956956+ sae_password = flip map bssCfg.authentication.saePasswords (
957957+ entry:
958958+ entry.password
959959+ + optionalString (entry.mac != null) "|mac=${entry.mac}"
960960+ + optionalString (entry.vlanid != null) "|vlanid=${toString entry.vlanid}"
961961+ + optionalString (entry.pk != null) "|pk=${entry.pk}"
962962+ + optionalString (entry.id != null) "|id=${entry.id}"
963963+ );
964964+ } // optionalAttrs (bssCfg.bssid != null) {
965965+ bssid = bssCfg.bssid;
966966+ } // optionalAttrs (bssCfg.macAllow != [] || bssCfg.macAllowFile != null || bssCfg.authentication.saeAddToMacAllow) {
967967+ accept_mac_file = "/run/hostapd/${bss}.mac.allow";
968968+ } // optionalAttrs (bssCfg.macDeny != [] || bssCfg.macDenyFile != null) {
969969+ deny_mac_file = "/run/hostapd/${bss}.mac.deny";
970970+ } // optionalAttrs (bssCfg.authentication.mode == "none") {
971971+ wpa = mkDefault 0;
972972+ } // optionalAttrs (bssCfg.authentication.mode == "wpa3-sae") {
973973+ wpa = 2;
974974+ wpa_key_mgmt = "SAE";
975975+ # Derive PWE using both hunting-and-pecking loop and hash-to-element
976976+ sae_pwe = 2;
977977+ # Prevent downgrade attacks by indicating to clients that they should
978978+ # disable any transition modes from now on.
979979+ transition_disable = "0x01";
980980+ } // optionalAttrs (bssCfg.authentication.mode == "wpa3-sae-transition") {
981981+ wpa = 2;
982982+ wpa_key_mgmt = "WPA-PSK-SHA256 SAE";
983983+ } // optionalAttrs (bssCfg.authentication.mode == "wpa2-sha256") {
984984+ wpa = 2;
985985+ wpa_key_mgmt = "WPA-PSK-SHA256";
986986+ } // optionalAttrs (bssCfg.authentication.mode != "none") {
987987+ wpa_pairwise = pairwiseCiphers;
988988+ rsn_pairwise = pairwiseCiphers;
989989+ } // optionalAttrs (bssCfg.authentication.wpaPassword != null) {
990990+ wpa_passphrase = bssCfg.authentication.wpaPassword;
991991+ } // optionalAttrs (bssCfg.authentication.wpaPskFile != null) {
992992+ wpa_psk_file = bssCfg.authentication.wpaPskFile;
993993+ };
168994169169- countryCode = mkOption {
170170- default = null;
171171- example = "US";
172172- type = with types; nullOr str;
173173- description = lib.mdDoc ''
174174- Country code (ISO/IEC 3166-1). Used to set regulatory domain.
175175- Set as needed to indicate country in which device is operating.
176176- This can limit available channels and transmit power.
177177- These two octets are used as the first two octets of the Country String
178178- (dot11CountryString).
179179- If set this enables IEEE 802.11d. This advertises the countryCode and
180180- the set of allowed channels and transmit power levels based on the
181181- regulatory limits.
182182- '';
183183- };
995995+ dynamicConfigScripts = let
996996+ # All MAC addresses from SAE entries that aren't the wildcard address
997997+ saeMacs = filter (mac: mac != null && (toLower mac) != "ff:ff:ff:ff:ff:ff") (map (x: x.mac) bssCfg.authentication.saePasswords);
998998+ in {
999999+ "20-addMacAllow" = mkIf (bssCfg.macAllow != []) (pkgs.writeShellScript "add-mac-allow" ''
10001000+ MAC_ALLOW_FILE=$2
10011001+ cat >> "$MAC_ALLOW_FILE" <<EOF
10021002+ ${concatStringsSep "\n" bssCfg.macAllow}
10031003+ EOF
10041004+ '');
10051005+ "20-addMacAllowFile" = mkIf (bssCfg.macAllowFile != null) (pkgs.writeShellScript "add-mac-allow-file" ''
10061006+ MAC_ALLOW_FILE=$2
10071007+ grep -Eo '^([0-9A-Fa-f]{2}[:]){5}([0-9A-Fa-f]{2})' ${escapeShellArg bssCfg.macAllowFile} >> "$MAC_ALLOW_FILE"
10081008+ '');
10091009+ "20-addMacAllowFromSae" = mkIf (bssCfg.authentication.saeAddToMacAllow && saeMacs != []) (pkgs.writeShellScript "add-mac-allow-from-sae" ''
10101010+ MAC_ALLOW_FILE=$2
10111011+ cat >> "$MAC_ALLOW_FILE" <<EOF
10121012+ ${concatStringsSep "\n" saeMacs}
10131013+ EOF
10141014+ '');
10151015+ # Populate mac allow list from saePasswordsFile
10161016+ # (filter for lines with mac=; exclude commented lines; filter for real mac-addresses; strip mac=)
10171017+ "20-addMacAllowFromSaeFile" = mkIf (bssCfg.authentication.saeAddToMacAllow && bssCfg.authentication.saePasswordsFile != null) (pkgs.writeShellScript "add-mac-allow-from-sae-file" ''
10181018+ MAC_ALLOW_FILE=$2
10191019+ grep mac= ${escapeShellArg bssCfg.authentication.saePasswordsFile} \
10201020+ | grep -v '\s*#' \
10211021+ | grep -Eo 'mac=([0-9A-Fa-f]{2}[:]){5}([0-9A-Fa-f]{2})' \
10221022+ | sed 's|^mac=||' >> "$MAC_ALLOW_FILE"
10231023+ '');
10241024+ "20-addMacDeny" = mkIf (bssCfg.macDeny != []) (pkgs.writeShellScript "add-mac-deny" ''
10251025+ MAC_DENY_FILE=$3
10261026+ cat >> "$MAC_DENY_FILE" <<EOF
10271027+ ${concatStringsSep "\n" bssCfg.macDeny}
10281028+ EOF
10291029+ '');
10301030+ "20-addMacDenyFile" = mkIf (bssCfg.macDenyFile != null) (pkgs.writeShellScript "add-mac-deny-file" ''
10311031+ MAC_DENY_FILE=$3
10321032+ grep -Eo '^([0-9A-Fa-f]{2}[:]){5}([0-9A-Fa-f]{2})' ${escapeShellArg bssCfg.macDenyFile} >> "$MAC_DENY_FILE"
10331033+ '');
10341034+ # Add wpa_passphrase from file
10351035+ "20-wpaPasswordFile" = mkIf (bssCfg.authentication.wpaPasswordFile != null) (pkgs.writeShellScript "wpa-password-file" ''
10361036+ HOSTAPD_CONFIG_FILE=$1
10371037+ cat >> "$HOSTAPD_CONFIG_FILE" <<EOF
10381038+ wpa_passphrase=$(cat ${escapeShellArg bssCfg.authentication.wpaPasswordFile})
10391039+ EOF
10401040+ '');
10411041+ # Add sae passwords from file
10421042+ "20-saePasswordsFile" = mkIf (bssCfg.authentication.saePasswordsFile != null) (pkgs.writeShellScript "sae-passwords-file" ''
10431043+ HOSTAPD_CONFIG_FILE=$1
10441044+ grep -v '\s*#' ${escapeShellArg bssCfg.authentication.saePasswordsFile} \
10451045+ | sed 's/^/sae_password=/' >> "$HOSTAPD_CONFIG_FILE"
10461046+ '');
10471047+ };
10481048+ };
10491049+ }));
10501050+ };
10511051+ };
1841052185185- extraConfig = mkOption {
186186- default = "";
187187- example = ''
188188- auth_algo=0
189189- ieee80211n=1
190190- ht_capab=[HT40-][SHORT-GI-40][DSSS_CCK-40]
191191- '';
192192- type = types.lines;
193193- description = lib.mdDoc "Extra configuration options to put in hostapd.conf.";
10531053+ config.settings = let
10541054+ radio = radioSubmod.name;
10551055+ radioCfg = radioSubmod.config;
10561056+ in {
10571057+ driver = radioCfg.driver;
10581058+ hw_mode = {
10591059+ "2g" = "g";
10601060+ "5g" = "a";
10611061+ "6g" = "a";
10621062+ "60g" = "ad";
10631063+ }.${radioCfg.band};
10641064+ channel = radioCfg.channel;
10651065+ noscan = radioCfg.noScan;
10661066+ } // optionalAttrs (radioCfg.countryCode != null) {
10671067+ country_code = radioCfg.countryCode;
10681068+ # IEEE 802.11d: Limit to frequencies allowed in country
10691069+ ieee80211d = true;
10701070+ # IEEE 802.11h: Enable radar detection and DFS (Dynamic Frequency Selection)
10711071+ ieee80211h = true;
10721072+ } // optionalAttrs radioCfg.wifi4.enable {
10731073+ # IEEE 802.11n (WiFi 4) related configuration
10741074+ ieee80211n = true;
10751075+ require_ht = radioCfg.wifi4.require;
10761076+ ht_capab = concatMapStrings (x: "[${x}]") radioCfg.wifi4.capabilities;
10771077+ } // optionalAttrs radioCfg.wifi5.enable {
10781078+ # IEEE 802.11ac (WiFi 5) related configuration
10791079+ ieee80211ac = true;
10801080+ require_vht = radioCfg.wifi5.require;
10811081+ vht_oper_chwidth = radioCfg.wifi5.operatingChannelWidth;
10821082+ vht_capab = concatMapStrings (x: "[${x}]") radioCfg.wifi5.capabilities;
10831083+ } // optionalAttrs radioCfg.wifi6.enable {
10841084+ # IEEE 802.11ax (WiFi 6) related configuration
10851085+ ieee80211ax = true;
10861086+ require_he = mkIf radioCfg.wifi6.require true;
10871087+ he_oper_chwidth = radioCfg.wifi6.operatingChannelWidth;
10881088+ he_su_beamformer = radioCfg.wifi6.singleUserBeamformer;
10891089+ he_su_beamformee = radioCfg.wifi6.singleUserBeamformee;
10901090+ he_mu_beamformer = radioCfg.wifi6.multiUserBeamformer;
10911091+ } // optionalAttrs radioCfg.wifi7.enable {
10921092+ # IEEE 802.11be (WiFi 7) related configuration
10931093+ ieee80211be = true;
10941094+ eht_oper_chwidth = radioCfg.wifi7.operatingChannelWidth;
10951095+ eht_su_beamformer = radioCfg.wifi7.singleUserBeamformer;
10961096+ eht_su_beamformee = radioCfg.wifi7.singleUserBeamformee;
10971097+ eht_mu_beamformer = radioCfg.wifi7.multiUserBeamformer;
10981098+ };
10991099+ }));
1941100 };
1951101 };
1961102 };
197110311041104+ imports = let
11051105+ renamedOptionMessage = message: ''
11061106+ ${message}
11071107+ Refer to the documentation of `services.hostapd.radios` for an example and more information.
11081108+ '';
11091109+ in [
11101110+ (mkRemovedOptionModule ["services" "hostapd" "interface"]
11111111+ (renamedOptionMessage "All other options for this interface are now set via `services.hostapd.radios.«interface».*`."))
1981112199199- ###### implementation
11131113+ (mkRemovedOptionModule ["services" "hostapd" "driver"]
11141114+ (renamedOptionMessage "It has been replaced by `services.hostapd.radios.«interface».driver`."))
11151115+ (mkRemovedOptionModule ["services" "hostapd" "noScan"]
11161116+ (renamedOptionMessage "It has been replaced by `services.hostapd.radios.«interface».noScan`."))
11171117+ (mkRemovedOptionModule ["services" "hostapd" "countryCode"]
11181118+ (renamedOptionMessage "It has been replaced by `services.hostapd.radios.«interface».countryCode`."))
11191119+ (mkRemovedOptionModule ["services" "hostapd" "hwMode"]
11201120+ (renamedOptionMessage "It has been replaced by `services.hostapd.radios.«interface».band`."))
11211121+ (mkRemovedOptionModule ["services" "hostapd" "channel"]
11221122+ (renamedOptionMessage "It has been replaced by `services.hostapd.radios.«interface».channel`."))
11231123+ (mkRemovedOptionModule ["services" "hostapd" "extraConfig"]
11241124+ (renamedOptionMessage ''
11251125+ It has been replaced by `services.hostapd.radios.«interface».settings` and
11261126+ `services.hostapd.radios.«interface».networks.«network».settings` respectively
11271127+ for per-radio and per-network extra configuration. The module now supports a lot more
11281128+ options inherently, so please re-check whether using settings is still necessary.''))
11291129+11301130+ (mkRemovedOptionModule ["services" "hostapd" "logLevel"]
11311131+ (renamedOptionMessage "It has been replaced by `services.hostapd.radios.«interface».networks.«network».logLevel`."))
11321132+ (mkRemovedOptionModule ["services" "hostapd" "group"]
11331133+ (renamedOptionMessage "It has been replaced by `services.hostapd.radios.«interface».networks.«network».group`."))
11341134+ (mkRemovedOptionModule ["services" "hostapd" "ssid"]
11351135+ (renamedOptionMessage "It has been replaced by `services.hostapd.radios.«interface».networks.«network».ssid`."))
11361136+11371137+ (mkRemovedOptionModule ["services" "hostapd" "wpa"]
11381138+ (renamedOptionMessage "It has been replaced by `services.hostapd.radios.«interface».networks.«network».authentication.mode`."))
11391139+ (mkRemovedOptionModule ["services" "hostapd" "wpaPassphrase"]
11401140+ (renamedOptionMessage ''
11411141+ It has been replaced by `services.hostapd.radios.«interface».networks.«network».authentication.wpaPassword`.
11421142+ While upgrading your config, please consider using the newer SAE authentication scheme
11431143+ and one of the new `passwordFile`-like options to avoid putting the password into the world readable nix-store.''))
11441144+ ];
20011452011146 config = mkIf cfg.enable {
11471147+ assertions =
11481148+ [
11491149+ {
11501150+ assertion = cfg.radios != {};
11511151+ message = "At least one radio must be configured with hostapd!";
11521152+ }
11531153+ ]
11541154+ # Radio warnings
11551155+ ++ (concatLists (mapAttrsToList (
11561156+ radio: radioCfg:
11571157+ [
11581158+ {
11591159+ assertion = radioCfg.networks != {};
11601160+ message = "hostapd radio ${radio}: At least one network must be configured!";
11611161+ }
11621162+ # XXX: There could be many more useful assertions about (band == xy) -> ensure other required settings.
11631163+ # see https://github.com/openwrt/openwrt/blob/539cb5389d9514c99ec1f87bd4465f77c7ed9b93/package/kernel/mac80211/files/lib/netifd/wireless/mac80211.sh#L158
11641164+ {
11651165+ assertion = length (filter (bss: bss == radio) (attrNames radioCfg.networks)) == 1;
11661166+ message = ''hostapd radio ${radio}: Exactly one network must be named like the radio, for reasons internal to hostapd.'';
11671167+ }
11681168+ {
11691169+ assertion = (radioCfg.wifi4.enable && builtins.elem "HT40-" radioCfg.wifi4.capabilities) -> radioCfg.channel != 0;
11701170+ message = ''hostapd radio ${radio}: using ACS (channel = 0) together with HT40- (wifi4.capabilities) is unsupported by hostapd'';
11711171+ }
11721172+ ]
11731173+ # BSS warnings
11741174+ ++ (concatLists (mapAttrsToList (bss: bssCfg: let
11751175+ auth = bssCfg.authentication;
11761176+ countWpaPasswordDefinitions = count (x: x != null) [
11771177+ auth.wpaPassword
11781178+ auth.wpaPasswordFile
11791179+ auth.wpaPskFile
11801180+ ];
11811181+ in [
11821182+ {
11831183+ assertion = hasPrefix radio bss;
11841184+ 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, ...";
11851185+ }
11861186+ {
11871187+ assertion = (length (attrNames radioCfg.networks) > 1) -> (bssCfg.bssid != null);
11881188+ message = ''hostapd radio ${radio} bss ${bss}: bssid must be specified manually (for now) since this radio uses multiple BSS.'';
11891189+ }
11901190+ {
11911191+ assertion = auth.mode == "wpa3-sae" -> bssCfg.managementFrameProtection == 2;
11921192+ message = ''hostapd radio ${radio} bss ${bss}: uses WPA3-SAE which requires managementFrameProtection="required"'';
11931193+ }
11941194+ {
11951195+ assertion = auth.mode == "wpa3-sae-transition" -> bssCfg.managementFrameProtection != 0;
11961196+ message = ''hostapd radio ${radio} bss ${bss}: uses WPA3-SAE in transition mode with WPA2-SHA256, which requires managementFrameProtection="optional" or ="required"'';
11971197+ }
11981198+ {
11991199+ assertion = countWpaPasswordDefinitions <= 1;
12001200+ message = ''hostapd radio ${radio} bss ${bss}: must use at most one WPA password option (wpaPassword, wpaPasswordFile, wpaPskFile)'';
12011201+ }
12021202+ {
12031203+ assertion = auth.wpaPassword != null -> (stringLength auth.wpaPassword >= 8 && stringLength auth.wpaPassword <= 63);
12041204+ message = ''hostapd radio ${radio} bss ${bss}: uses a wpaPassword of invalid length (must be in [8,63]).'';
12051205+ }
12061206+ {
12071207+ assertion = auth.saePasswords == [] || auth.saePasswordsFile == null;
12081208+ message = ''hostapd radio ${radio} bss ${bss}: must use only one SAE password option (saePasswords or saePasswordsFile)'';
12091209+ }
12101210+ {
12111211+ assertion = auth.mode == "wpa3-sae" -> (auth.saePasswords != [] || auth.saePasswordsFile != null);
12121212+ message = ''hostapd radio ${radio} bss ${bss}: uses WPA3-SAE which requires defining a sae password option'';
12131213+ }
12141214+ {
12151215+ assertion = auth.mode == "wpa3-sae-transition" -> (auth.saePasswords != [] || auth.saePasswordsFile != null) && countWpaPasswordDefinitions == 1;
12161216+ message = ''hostapd radio ${radio} bss ${bss}: uses WPA3-SAE in transition mode requires defining both a wpa password option and a sae password option'';
12171217+ }
12181218+ {
12191219+ assertion = auth.mode == "wpa2-sha256" -> countWpaPasswordDefinitions == 1;
12201220+ message = ''hostapd radio ${radio} bss ${bss}: uses WPA2-SHA256 which requires defining a wpa password option'';
12211221+ }
12221222+ ])
12231223+ radioCfg.networks))
12241224+ )
12251225+ cfg.radios));
2021226203203- environment.systemPackages = [ pkgs.hostapd ];
12271227+ environment.systemPackages = [cfg.package];
2041228205205- services.udev.packages = optionals (cfg.countryCode != null) [ pkgs.crda ];
12291229+ services.udev.packages = with pkgs; [crda];
2061230207207- systemd.services.hostapd =
208208- { description = "hostapd wireless AP";
12311231+ systemd.services.hostapd = {
12321232+ description = "IEEE 802.11 Host Access-Point Daemon";
2091233210210- path = [ pkgs.hostapd ];
211211- after = [ "sys-subsystem-net-devices-${escapedInterface}.device" ];
212212- bindsTo = [ "sys-subsystem-net-devices-${escapedInterface}.device" ];
213213- requiredBy = [ "network-link-${cfg.interface}.service" ];
214214- wantedBy = [ "multi-user.target" ];
12341234+ path = [cfg.package];
12351235+ after = map (radio: "sys-subsystem-net-devices-${utils.escapeSystemdPath radio}.device") (attrNames cfg.radios);
12361236+ bindsTo = map (radio: "sys-subsystem-net-devices-${utils.escapeSystemdPath radio}.device") (attrNames cfg.radios);
12371237+ wantedBy = ["multi-user.target"];
2151238216216- serviceConfig =
217217- { ExecStart = "${pkgs.hostapd}/bin/hostapd ${configFile}";
218218- Restart = "always";
219219- };
12391239+ # Create merged configuration and acl files for each radio (and their bss's) prior to starting
12401240+ preStart = concatStringsSep "\n" (mapAttrsToList makeRadioRuntimeFiles cfg.radios);
12411241+12421242+ serviceConfig = {
12431243+ ExecStart = "${cfg.package}/bin/hostapd ${concatStringsSep " " runtimeConfigFiles}";
12441244+ Restart = "always";
12451245+ ExecReload = "${pkgs.coreutils}/bin/kill -HUP $MAINPID";
12461246+ RuntimeDirectory = "hostapd";
12471247+12481248+ # Hardening
12491249+ LockPersonality = true;
12501250+ MemoryDenyWriteExecute = true;
12511251+ DevicePolicy = "closed";
12521252+ DeviceAllow = "/dev/rfkill rw";
12531253+ NoNewPrivileges = true;
12541254+ PrivateUsers = false; # hostapd requires true root access.
12551255+ PrivateTmp = true;
12561256+ ProtectClock = true;
12571257+ ProtectControlGroups = true;
12581258+ ProtectHome = true;
12591259+ ProtectHostname = true;
12601260+ ProtectKernelLogs = true;
12611261+ ProtectKernelModules = true;
12621262+ ProtectKernelTunables = true;
12631263+ ProtectProc = "invisible";
12641264+ ProcSubset = "pid";
12651265+ ProtectSystem = "strict";
12661266+ RestrictAddressFamilies = [
12671267+ "AF_INET"
12681268+ "AF_INET6"
12691269+ "AF_NETLINK"
12701270+ "AF_UNIX"
12711271+ ];
12721272+ RestrictNamespaces = true;
12731273+ RestrictRealtime = true;
12741274+ RestrictSUIDSGID = true;
12751275+ SystemCallArchitectures = "native";
12761276+ SystemCallFilter = [
12771277+ "@system-service"
12781278+ "~@privileged"
12791279+ "@chown"
12801280+ ];
12811281+ UMask = "0077";
2201282 };
12831283+ };
2211284 };
2221285}
+168-55
nixos/tests/wpa_supplicant.nix
···22{
33 name = "wpa_supplicant";
44 meta = with lib.maintainers; {
55- maintainers = [ rnhmjoj ];
55+ maintainers = [ oddlama rnhmjoj ];
66 };
7788- nodes.machine = { ... }: {
99- imports = [ ../modules/profiles/minimal.nix ];
88+ nodes = let
99+ machineWithHostapd = extraConfigModule: { ... }: {
1010+ imports = [
1111+ ../modules/profiles/minimal.nix
1212+ extraConfigModule
1313+ ];
10141111- # add a virtual wlan interface
1212- boot.kernelModules = [ "mac80211_hwsim" ];
1515+ # add a virtual wlan interface
1616+ boot.kernelModules = [ "mac80211_hwsim" ];
13171414- # wireless access point
1515- services.hostapd = {
1616- enable = true;
1717- wpa = true;
1818- interface = "wlan0";
1919- ssid = "nixos-test";
2020- wpaPassphrase = "reproducibility";
1818+ # wireless access point
1919+ services.hostapd = {
2020+ enable = true;
2121+ radios.wlan0 = {
2222+ band = "2g";
2323+ countryCode = "US";
2424+ networks = {
2525+ wlan0 = {
2626+ ssid = "nixos-test-sae";
2727+ authentication = {
2828+ mode = "wpa3-sae";
2929+ saePasswords = [ { password = "reproducibility"; } ];
3030+ };
3131+ bssid = "02:00:00:00:00:00";
3232+ };
3333+ wlan0-1 = {
3434+ ssid = "nixos-test-mixed";
3535+ authentication = {
3636+ mode = "wpa3-sae-transition";
3737+ saePasswordsFile = pkgs.writeText "password" "reproducibility";
3838+ wpaPasswordFile = pkgs.writeText "password" "reproducibility";
3939+ };
4040+ bssid = "02:00:00:00:00:01";
4141+ };
4242+ wlan0-2 = {
4343+ ssid = "nixos-test-wpa2";
4444+ authentication = {
4545+ mode = "wpa2-sha256";
4646+ wpaPassword = "reproducibility";
4747+ };
4848+ bssid = "02:00:00:00:00:02";
4949+ };
5050+ };
5151+ };
5252+ };
5353+5454+ # wireless client
5555+ networking.wireless = {
5656+ # the override is needed because the wifi is
5757+ # disabled with mkVMOverride in qemu-vm.nix.
5858+ enable = lib.mkOverride 0 true;
5959+ userControlled.enable = true;
6060+ interfaces = [ "wlan1" ];
6161+ fallbackToWPA2 = lib.mkDefault true;
6262+6363+ # networks will be added on-demand below for the specific
6464+ # network that should be tested
6565+6666+ # secrets
6767+ environmentFile = pkgs.writeText "wpa-secrets" ''
6868+ PSK_NIXOS_TEST="reproducibility"
6969+ '';
7070+ };
2171 };
7272+ in {
7373+ basic = { ... }: {
7474+ imports = [ ../modules/profiles/minimal.nix ];
22752323- # wireless client
2424- networking.wireless = {
2525- # the override is needed because the wifi is
2626- # disabled with mkVMOverride in qemu-vm.nix.
2727- enable = lib.mkOverride 0 true;
2828- userControlled.enable = true;
2929- interfaces = [ "wlan1" ];
3030- fallbackToWPA2 = true;
7676+ # add a virtual wlan interface
7777+ boot.kernelModules = [ "mac80211_hwsim" ];
31783232- networks = {
3333- # test WPA2 fallback
3434- mixed-wpa = {
3535- psk = "password";
3636- authProtocols = [ "WPA-PSK" "SAE" ];
7979+ # wireless client
8080+ networking.wireless = {
8181+ # the override is needed because the wifi is
8282+ # disabled with mkVMOverride in qemu-vm.nix.
8383+ enable = lib.mkOverride 0 true;
8484+ userControlled.enable = true;
8585+ interfaces = [ "wlan1" ];
8686+ fallbackToWPA2 = true;
8787+8888+ networks = {
8989+ # test WPA2 fallback
9090+ mixed-wpa = {
9191+ psk = "password";
9292+ authProtocols = [ "WPA-PSK" "SAE" ];
9393+ };
9494+ sae-only = {
9595+ psk = "password";
9696+ authProtocols = [ "SAE" ];
9797+ };
9898+9999+ # secrets substitution test cases
100100+ test1.psk = "@PSK_VALID@"; # should be replaced
101101+ test2.psk = "@PSK_SPECIAL@"; # should be replaced
102102+ test3.psk = "@PSK_MISSING@"; # should not be replaced
103103+ test4.psk = "P@ssowrdWithSome@tSymbol"; # should not be replaced
37104 };
3838- sae-only = {
3939- psk = "password";
105105+106106+ # secrets
107107+ environmentFile = pkgs.writeText "wpa-secrets" ''
108108+ PSK_VALID="S0m3BadP4ssw0rd";
109109+ # taken from https://github.com/minimaxir/big-list-of-naughty-strings
110110+ PSK_SPECIAL=",./;'[]\-= <>?:\"{}|_+ !@#$%^\&*()`~";
111111+ '';
112112+ };
113113+ };
114114+115115+ # Test connecting to the SAE-only hotspot using SAE
116116+ machineSae = machineWithHostapd {
117117+ networking.wireless = {
118118+ fallbackToWPA2 = false;
119119+ networks.nixos-test-sae = {
120120+ psk = "@PSK_NIXOS_TEST@";
40121 authProtocols = [ "SAE" ];
41122 };
123123+ };
124124+ };
421254343- # test network
4444- nixos-test.psk = "@PSK_NIXOS_TEST@";
126126+ # Test connecting to the SAE and WPA2 mixed hotspot using SAE
127127+ machineMixedUsingSae = machineWithHostapd {
128128+ networking.wireless = {
129129+ fallbackToWPA2 = false;
130130+ networks.nixos-test-mixed = {
131131+ psk = "@PSK_NIXOS_TEST@";
132132+ authProtocols = [ "SAE" ];
133133+ };
134134+ };
135135+ };
451364646- # secrets substitution test cases
4747- test1.psk = "@PSK_VALID@"; # should be replaced
4848- test2.psk = "@PSK_SPECIAL@"; # should be replaced
4949- test3.psk = "@PSK_MISSING@"; # should not be replaced
5050- test4.psk = "P@ssowrdWithSome@tSymbol"; # should not be replaced
137137+ # Test connecting to the SAE and WPA2 mixed hotspot using WPA2
138138+ machineMixedUsingWpa2 = machineWithHostapd {
139139+ networking.wireless = {
140140+ fallbackToWPA2 = true;
141141+ networks.nixos-test-mixed = {
142142+ psk = "@PSK_NIXOS_TEST@";
143143+ authProtocols = [ "WPA-PSK-SHA256" ];
144144+ };
51145 };
146146+ };
521475353- # secrets
5454- environmentFile = pkgs.writeText "wpa-secrets" ''
5555- PSK_NIXOS_TEST="reproducibility"
5656- PSK_VALID="S0m3BadP4ssw0rd";
5757- # taken from https://github.com/minimaxir/big-list-of-naughty-strings
5858- PSK_SPECIAL=",./;'[]\-= <>?:\"{}|_+ !@#$%^\&*()`~";
5959- '';
148148+ # Test connecting to the WPA2 legacy hotspot using WPA2
149149+ machineWpa2 = machineWithHostapd {
150150+ networking.wireless = {
151151+ fallbackToWPA2 = true;
152152+ networks.nixos-test-wpa2 = {
153153+ psk = "@PSK_NIXOS_TEST@";
154154+ authProtocols = [ "WPA-PSK-SHA256" ];
155155+ };
156156+ };
60157 };
6161-62158 };
6315964160 testScript =
···66162 config_file = "/run/wpa_supplicant/wpa_supplicant.conf"
6716368164 with subtest("Configuration file is inaccessible to other users"):
6969- machine.wait_for_file(config_file)
7070- machine.fail(f"sudo -u nobody ls {config_file}")
165165+ basic.wait_for_file(config_file)
166166+ basic.fail(f"sudo -u nobody ls {config_file}")
7116772168 with subtest("Secrets variables have been substituted"):
7373- machine.fail(f"grep -q @PSK_VALID@ {config_file}")
7474- machine.fail(f"grep -q @PSK_SPECIAL@ {config_file}")
7575- machine.succeed(f"grep -q @PSK_MISSING@ {config_file}")
7676- machine.succeed(f"grep -q P@ssowrdWithSome@tSymbol {config_file}")
169169+ basic.fail(f"grep -q @PSK_VALID@ {config_file}")
170170+ basic.fail(f"grep -q @PSK_SPECIAL@ {config_file}")
171171+ basic.succeed(f"grep -q @PSK_MISSING@ {config_file}")
172172+ basic.succeed(f"grep -q P@ssowrdWithSome@tSymbol {config_file}")
7717378174 with subtest("WPA2 fallbacks have been generated"):
7979- assert int(machine.succeed(f"grep -c sae-only {config_file}")) == 1
8080- assert int(machine.succeed(f"grep -c mixed-wpa {config_file}")) == 2
175175+ assert int(basic.succeed(f"grep -c sae-only {config_file}")) == 1
176176+ assert int(basic.succeed(f"grep -c mixed-wpa {config_file}")) == 2
8117782178 # save file for manual inspection
8383- machine.copy_from_vm(config_file)
179179+ basic.copy_from_vm(config_file)
8418085181 with subtest("Daemon is running and accepting connections"):
8686- machine.wait_for_unit("wpa_supplicant-wlan1.service")
8787- status = machine.succeed("wpa_cli -i wlan1 status")
182182+ basic.wait_for_unit("wpa_supplicant-wlan1.service")
183183+ status = basic.succeed("wpa_cli -i wlan1 status")
88184 assert "Failed to connect" not in status, \
89185 "Failed to connect to the daemon"
901869191- with subtest("Daemon can connect to the access point"):
9292- machine.wait_until_succeeds(
187187+ machineSae.wait_for_unit("hostapd.service")
188188+ machineSae.copy_from_vm("/run/hostapd/wlan0.hostapd.conf")
189189+ with subtest("Daemon can connect to the SAE access point using SAE"):
190190+ machineSae.wait_until_succeeds(
191191+ "wpa_cli -i wlan1 status | grep -q wpa_state=COMPLETED"
192192+ )
193193+194194+ with subtest("Daemon can connect to the SAE and WPA2 mixed access point using SAE"):
195195+ machineMixedUsingSae.wait_until_succeeds(
196196+ "wpa_cli -i wlan1 status | grep -q wpa_state=COMPLETED"
197197+ )
198198+199199+ with subtest("Daemon can connect to the SAE and WPA2 mixed access point using WPA2"):
200200+ machineMixedUsingWpa2.wait_until_succeeds(
201201+ "wpa_cli -i wlan1 status | grep -q wpa_state=COMPLETED"
202202+ )
203203+204204+ with subtest("Daemon can connect to the WPA2 access point using WPA2"):
205205+ machineWpa2.wait_until_succeeds(
93206 "wpa_cli -i wlan1 status | grep -q wpa_state=COMPLETED"
94207 )
95208 '';