forked from mirrors_public/oddlama_nix-config
chore: wip: add assertions and most of runtime file generation
This commit is contained in:
parent
41e60b81f7
commit
1383eb20df
1 changed files with 102 additions and 35 deletions
|
@ -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";
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue