chore: wip: add assertions and most of runtime file generation

This commit is contained in:
oddlama 2023-03-20 02:27:10 +01:00
parent 41e60b81f7
commit 1383eb20df
No known key found for this signature in database
GPG key ID: 14EFE510775FE39A

View file

@ -6,8 +6,6 @@
...
}:
with lib; let
# TODO: add multi AP support (aka EasyMesh(TM))
# TODO DFS as separate setting ?
cfg = config.services.hostapd;
# Maps the specified acl mode to values understood by hostapd
@ -39,8 +37,11 @@ with lib; let
"required" = "2";
};
# A marker that will be replaced in service.hostapd.preStart to possibly add the
# AP password from a store-external location.
runtimePasswordDefinitionMarker = "##RUNTIME_PASSWORDS_DEFINITION##";
configFileForInterface = interface: ifcfg: let
escapedInterface = utils.escapeSystemdPath interface;
hasMacAllowList = length ifcfg.macAllow > 0 || ifcfg.macAllowFile != null;
hasMacDenyList = length ifcfg.macDeny > 0 || ifcfg.macDenyFile != null;
bool01 = b:
@ -48,7 +49,7 @@ with lib; let
then "1"
else "0";
in
pkgs.writeText "hostapd-${escapedInterface}.conf" ''
pkgs.writeText "hostapd-${interface}.conf" ''
logger_syslog=-1
logger_syslog_level=${toString ifcfg.logLevel}
logger_stdout=-1
@ -76,10 +77,10 @@ with lib; let
# Set the MAC-address access control mode
macaddr_acl=${macaddrAclModes.${ifcfg.macAcl}}
${optionalString hasMacAllowList ''
accept_mac_file=/run/hostapd/mac-${escapedInterface}.allow
accept_mac_file=/run/hostapd/${interface}.mac.allow
''}
${optionalString hasMacDenyList ''
deny_mac_file=/run/hostapd/mac-${escapedInterface}.deny
deny_mac_file=/run/hostapd/${interface}.mac.deny
''}
# Only allow WPA, disable WEP (insecure)
auth_algs=1
@ -151,21 +152,64 @@ with lib; let
wpa_key_mgmt=WPA-PSK-SHA256
''}
${optionalString (ifcfg.authentication.mode != "none") ''
wpa_pairwise=CCMP CCMP-256
rsn_pairwise=CCMP CCMP-256
wpa_pairwise=${concatStringsSep " " ifcfg.authentication.pairwiseCiphers}
rsn_pairwise=${concatStringsSep " " ifcfg.authentication.pairwiseCiphers}
''}
${optionalString (ifcfg.authentication.wpaPassword != null) ''
wpa_passphrase=${ifcfg.authentication.wpaPassword}
''}
${optionalString (ifcfg.authentication.wpaPskFile != null) ''
wpa_passphrase=${ifcfg.authentication.wpaPskFile}
''}
${optionalString (length ifcfg.authentication.saePasswords > 0) (concatMapStrings (pw: "sae_password=${pw}\n") ifcfg.authentication.saePasswords)}
${runtimePasswordDefinitionMarker}
# Encrypt management frames to protect against deauthentication and similar attacks
ieee80211w=${managementFrameProtection.${ifcfg.managementFrameProtection}}
# SAE passwords can be set via wpa_passphrase but not via wpa_psk_file. This sucks
# and means we have to add the passwords in pre-start to prevent them being visible here
{{SAE_PASSWORDS}}
##### User-provided extra configuration ##########################################
${ifcfg.extraConfig}
'';
configFiles = mapAttrsToList configFileForInterface cfg.interfaces;
runtimeConfigFiles = mapAttrsToList (i: _: "/run/hostapd/${i}.hostapd.conf") cfg.interfaces;
makeInterfaceRuntimeFiles = interface: ifcfg:
pkgs.writeShellScript ("make-hostapd-${interface}-files" ''
set -euo pipefail
rm -f /run/hostapd/${interface}.mac.allow
touch /run/hostapd/${interface}.mac.allow
rm -f /run/hostapd/${interface}.mac.deny
touch /run/hostapd/${interface}.mac.deny
rm -f /run/hostapd/${interface}.hostapd.conf
cp ${configFileForInterface interface ifcfg} /run/hostapd/${interface}.hostapd.conf
''
++ concatStringsSep "\n" (
optional (length ifcfg.macAllow > 0) ''
cat >> /run/hostapd/${interface}.mac.allow <<EOF
${concatStringsSep "\n" ifcfg.macAllow}
EOF
''
++ optional (ifcfg.macAllowFile != null) ''
grep -Eo '^([0-9A-Fa-f]{2}[:]){5}([0-9A-Fa-f]{2})' ${escapeShellArg ifcfg.macAllowFile} >> /run/hostapd/${interface}.mac.allow
''
# Create combined mac.deny list from macDeny and macDenyFile
++ optional (length ifcfg.macDeny > 0) ''
cat >> /run/hostapd/${interface}.mac.deny <<EOF
${concatStringsSep "\n" ifcfg.macDeny}
EOF
''
++ optional (ifcfg.macDenyFile != null) ''
grep -Eo '^([0-9A-Fa-f]{2}[:]){5}([0-9A-Fa-f]{2})' ${escapeShellArg ifcfg.macDenyFile} >> /run/hostapd/${interface}.mac.deny
''
++ optional (ifcfg.wpaPasswordFile != null) ''
awk -v marker=${escapeShellArg (escapeRegex runtimePasswordDefinitionMarker)} -v content="$replacement" '{gsub(marker,content)}' /run/hostapd/${interface}.hostapd.conf
''
# TODO replace marker if it's left.
));
in {
options = {
services.hostapd = {
@ -350,8 +394,8 @@ in {
description = mdDoc ''
Specifies a file containing the MAC addresses to allow if {option}`macAcl` is set to {var}`"deny"` or {var}`"radius"`.
The file should contain exactly one MAC address per line. Comments and empty lines are ignored,
only lines matching the regex `^..:..:..:..:..:..\b` will be considered.
Any content after the MAC address is ignored.
only lines starting with a valid MAC address will be considered (e.g. `11:22:33:44:55:66`) and
any content after the MAC address is ignored.
'';
};
@ -372,8 +416,8 @@ in {
description = mdDoc ''
Specifies a file containing the MAC addresses to allow if {option}`macAcl` is set to {var}`"deny"` or {var}`"radius"`.
The file should contain exactly one MAC address per line. Comments and empty lines are ignored,
only lines matching the regex `^..:..:..:..:..:..\b` will be considered.
Any content after the MAC address is ignored.
only lines starting with a valid MAC address will be considered (e.g. `11:22:33:44:55:66`) and
any content after the MAC address is ignored.
'';
};
@ -591,6 +635,18 @@ in {
'';
};
pairwiseCiphers = mkOption {
default = ["CCMP" "CCMP-256" "GCMP" "GCMP-256"];
example = ["CCMP-256" "GCMP-256"];
type = types.listOf types.str;
description = mdDoc ''
Set of accepted cipher suites (encryption algorithms) for pairwise keys (unicast packets).
Please refer to the hostapd documentation for allowed values. Generally, only
CCMP or GCMP modes should be considered safe options. Most devices support CCMP while
GCMP is often only available when using devices supporting WiFi 5 (IEEE 802.11ac) or higher.
'';
};
wpaPassword = mkOption {
default = null;
example = "a flakey password";
@ -722,8 +778,6 @@ in {
};
};
###### implementation
config = mkIf cfg.enable {
assertions =
[
@ -732,24 +786,42 @@ in {
message = "At least one interface must be configured with hostapd!";
}
]
++ (concatLists (mapAttrsToList (interface: ifcfg: [
# Interface warnings
++ (concatLists (mapAttrsToList (interface: ifcfg: let
countWpaPasswordDefinitions = count (x: x != null) [ifcfg.wpaPassword ifcfg.wpaPasswordFile ifcfg.wpaPskFile];
in [
{
assertion = (ifcfg.wifi5.enable || ifcfg.wifi6.enable || ifcfg.wifi7.enable) -> ifcfg.hwMode == "a";
message = ''hostapd interface ${interface} has enabled WiFi 5 or above, which requires hwMode="a"'';
}
{
assertion = ifcfg.authentication.mode == "wpa3-sae" -> ifcfg.managementFrameProtection == "required";
message = ''hostapd interface ${interface} uses WPA3-SAE which requires managementFrameProtection="required".'';
message = ''hostapd interface ${interface} uses WPA3-SAE which requires managementFrameProtection="required"'';
}
{
assertion = ifcfg.authentication.mode == "wpa3-sae-transition" -> ifcfg.managementFrameProtection != "disabled";
message = ''hostapd interface ${interface} uses WPA3-SAE in transition mode with WPA2-SHA256, which requires managementFrameProtection="optional" or ="required".'';
message = ''hostapd interface ${interface} uses WPA3-SAE in transition mode with WPA2-SHA256, which requires managementFrameProtection="optional" or ="required"'';
}
{
assertion = countWpaPasswordDefinitions <= 1;
message = ''hostapd interface ${interface} must use at most one WPA password option (wpaPassword, wpaPasswordFile, wpaPskFile)'';
}
{
assertion = length ifcfg.saePasswords == 0 || ifcfg.saePasswordsFile == null;
message = ''hostapd interface ${interface} must use only one SAE password option (saePasswords or saePasswordsFile)'';
}
{
assertion = ifcfg.authentication.mode == "wpa3-sae" -> (length ifcfg.saePasswords > 0 || ifcfg.saePasswordsFile != null);
message = ''hostapd interface ${interface} uses WPA3-SAE which requires defining a sae password option'';
}
{
assertion = ifcfg.authentication.mode == "wpa3-sae-transition" -> (length ifcfg.saePasswords > 0 || ifcfg.saePasswordsFile != null) && countWpaPasswordDefinitions == 1;
message = ''hostapd interface ${interface} uses WPA3-SAE in transition mode requires defining both a wpa password option and a sae password option'';
}
{
assertion = ifcfg.authentication.mode == "wpa2-sha256" -> countWpaPasswordDefinitions == 1;
message = ''hostapd interface ${interface} uses WPA2-SHA256 which requires defining a wpa password option'';
}
# TODO one of wpaPassword, wpaPasswordFile, wpaPskFile
# TODO one of saePasswords, saePasswordsFile
# TODO if wpa3-sae then no wpaPassword anmd one of saepass
# TODO if wpa3-sae-transition then both wpa and sae passes
# TODO if wpa2-sha256 then only wpa and not sae passes
])
cfg.interfaces));
@ -758,7 +830,7 @@ in {
services.udev.packages = optionals (any (i: i.countryCode != null) (attrValues cfg.interfaces)) [pkgs.crda];
systemd.services.hostapd = {
description = "hostapd wireless AP";
description = "Hostapd IEEE 802.11 AP";
path = [pkgs.hostapd];
after = mapAttrsToList (interface: _: "sys-subsystem-net-devices-${utils.escapeSystemdPath interface}.device") cfg.interfaces;
@ -766,16 +838,11 @@ in {
requiredBy = mapAttrsToList (interface: _: "network-link-${interface}.service") cfg.interfaces;
wantedBy = ["multi-user.target"];
preStart = mkBefore ''
grep -o '^..:..:..:..:..:..' ${config.rekey.secrets.wifi-clients.path} > /run/hostapd/client-macs
hostapd_conf=$(cat ''${systemd.services.hostapd.serviceConfig.ExecStart})
sae_passwords=$(echo -e "sae_password=aa|mac=13:13:13:13:13:13\nsae_password=aa|mac=12:12:12:12:12:12")
hostapd_conf=''${hostapd_conf//"{{SAE_PASSWORDS}}"/$sae_passwords}
echo "$hostapd_conf" > /run/hostapd/config/$interface
'';
# Create merged configuration and acl files for each interface prior to starting
preStart = concatStringsSep "\n" (mapAttrsToList makeInterfaceRuntimeFiles cfg.interfaces);
serviceConfig = {
ExecStart = "${pkgs.hostapd}/bin/hostapd ${concatStringsSep " " configFiles}";
ExecStart = "${pkgs.hostapd}/bin/hostapd ${concatStringsSep " " runtimeConfigFiles}";
Restart = "always";
ExecReload = "/bin/kill -HUP $MAINPID";
RuntimeDirectory = "hostapd";