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 with lib; let
# TODO: add multi AP support (aka EasyMesh(TM))
# TODO DFS as separate setting ?
cfg = config.services.hostapd; cfg = config.services.hostapd;
# Maps the specified acl mode to values understood by hostapd # Maps the specified acl mode to values understood by hostapd
@ -39,8 +37,11 @@ with lib; let
"required" = "2"; "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 configFileForInterface = interface: ifcfg: let
escapedInterface = utils.escapeSystemdPath interface;
hasMacAllowList = length ifcfg.macAllow > 0 || ifcfg.macAllowFile != null; hasMacAllowList = length ifcfg.macAllow > 0 || ifcfg.macAllowFile != null;
hasMacDenyList = length ifcfg.macDeny > 0 || ifcfg.macDenyFile != null; hasMacDenyList = length ifcfg.macDeny > 0 || ifcfg.macDenyFile != null;
bool01 = b: bool01 = b:
@ -48,7 +49,7 @@ with lib; let
then "1" then "1"
else "0"; else "0";
in in
pkgs.writeText "hostapd-${escapedInterface}.conf" '' pkgs.writeText "hostapd-${interface}.conf" ''
logger_syslog=-1 logger_syslog=-1
logger_syslog_level=${toString ifcfg.logLevel} logger_syslog_level=${toString ifcfg.logLevel}
logger_stdout=-1 logger_stdout=-1
@ -76,10 +77,10 @@ with lib; let
# Set the MAC-address access control mode # Set the MAC-address access control mode
macaddr_acl=${macaddrAclModes.${ifcfg.macAcl}} macaddr_acl=${macaddrAclModes.${ifcfg.macAcl}}
${optionalString hasMacAllowList '' ${optionalString hasMacAllowList ''
accept_mac_file=/run/hostapd/mac-${escapedInterface}.allow accept_mac_file=/run/hostapd/${interface}.mac.allow
''} ''}
${optionalString hasMacDenyList '' ${optionalString hasMacDenyList ''
deny_mac_file=/run/hostapd/mac-${escapedInterface}.deny deny_mac_file=/run/hostapd/${interface}.mac.deny
''} ''}
# Only allow WPA, disable WEP (insecure) # Only allow WPA, disable WEP (insecure)
auth_algs=1 auth_algs=1
@ -151,21 +152,64 @@ with lib; let
wpa_key_mgmt=WPA-PSK-SHA256 wpa_key_mgmt=WPA-PSK-SHA256
''} ''}
${optionalString (ifcfg.authentication.mode != "none") '' ${optionalString (ifcfg.authentication.mode != "none") ''
wpa_pairwise=CCMP CCMP-256 wpa_pairwise=${concatStringsSep " " ifcfg.authentication.pairwiseCiphers}
rsn_pairwise=CCMP CCMP-256 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 # Encrypt management frames to protect against deauthentication and similar attacks
ieee80211w=${managementFrameProtection.${ifcfg.managementFrameProtection}} ieee80211w=${managementFrameProtection.${ifcfg.managementFrameProtection}}
# SAE passwords can be set via wpa_passphrase but not via wpa_psk_file. This sucks ##### User-provided extra configuration ##########################################
# and means we have to add the passwords in pre-start to prevent them being visible here
{{SAE_PASSWORDS}}
${ifcfg.extraConfig} ${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 { in {
options = { options = {
services.hostapd = { services.hostapd = {
@ -350,8 +394,8 @@ in {
description = mdDoc '' description = mdDoc ''
Specifies a file containing the MAC addresses to allow if {option}`macAcl` is set to {var}`"deny"` or {var}`"radius"`. 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, 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. 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. any content after the MAC address is ignored.
''; '';
}; };
@ -372,8 +416,8 @@ in {
description = mdDoc '' description = mdDoc ''
Specifies a file containing the MAC addresses to allow if {option}`macAcl` is set to {var}`"deny"` or {var}`"radius"`. 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, 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. 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. 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 { wpaPassword = mkOption {
default = null; default = null;
example = "a flakey password"; example = "a flakey password";
@ -722,8 +778,6 @@ in {
}; };
}; };
###### implementation
config = mkIf cfg.enable { config = mkIf cfg.enable {
assertions = assertions =
[ [
@ -732,24 +786,42 @@ in {
message = "At least one interface must be configured with hostapd!"; 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"; 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"''; message = ''hostapd interface ${interface} has enabled WiFi 5 or above, which requires hwMode="a"'';
} }
{ {
assertion = ifcfg.authentication.mode == "wpa3-sae" -> ifcfg.managementFrameProtection == "required"; 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"; 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)); cfg.interfaces));
@ -758,7 +830,7 @@ in {
services.udev.packages = optionals (any (i: i.countryCode != null) (attrValues cfg.interfaces)) [pkgs.crda]; services.udev.packages = optionals (any (i: i.countryCode != null) (attrValues cfg.interfaces)) [pkgs.crda];
systemd.services.hostapd = { systemd.services.hostapd = {
description = "hostapd wireless AP"; description = "Hostapd IEEE 802.11 AP";
path = [pkgs.hostapd]; path = [pkgs.hostapd];
after = mapAttrsToList (interface: _: "sys-subsystem-net-devices-${utils.escapeSystemdPath interface}.device") cfg.interfaces; 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; requiredBy = mapAttrsToList (interface: _: "network-link-${interface}.service") cfg.interfaces;
wantedBy = ["multi-user.target"]; wantedBy = ["multi-user.target"];
preStart = mkBefore '' # Create merged configuration and acl files for each interface prior to starting
grep -o '^..:..:..:..:..:..' ${config.rekey.secrets.wifi-clients.path} > /run/hostapd/client-macs preStart = concatStringsSep "\n" (mapAttrsToList makeInterfaceRuntimeFiles cfg.interfaces);
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
'';
serviceConfig = { serviceConfig = {
ExecStart = "${pkgs.hostapd}/bin/hostapd ${concatStringsSep " " configFiles}"; ExecStart = "${pkgs.hostapd}/bin/hostapd ${concatStringsSep " " runtimeConfigFiles}";
Restart = "always"; Restart = "always";
ExecReload = "/bin/kill -HUP $MAINPID"; ExecReload = "/bin/kill -HUP $MAINPID";
RuntimeDirectory = "hostapd"; RuntimeDirectory = "hostapd";