From 5d095392cf749d588242d42bf539b9c0d931095a Mon Sep 17 00:00:00 2001 From: oddlama Date: Sun, 2 Apr 2023 17:33:04 +0200 Subject: [PATCH] feat: per-bss settings in hostapd module, prepare vaultwarden for later --- flake.lock | 30 +- hosts/ward/default.nix | 67 +- hosts/ward/vaultwarden.nix | 81 ++ hosts/zackbiene/default.nix | 2 +- hosts/zackbiene/hostapd.nix | 20 +- hosts/zackbiene/secrets/secrets.nix.age | Bin 721 -> 719 bytes hosts/zackbiene/secrets/wifi-clients.age | Bin 432 -> 531 bytes modules/hostapd.nix | 1311 ++++++++++++---------- 8 files changed, 900 insertions(+), 611 deletions(-) create mode 100644 hosts/ward/vaultwarden.nix diff --git a/flake.lock b/flake.lock index a931192..fba3dd8 100644 --- a/flake.lock +++ b/flake.lock @@ -8,11 +8,11 @@ ] }, "locked": { - "lastModified": 1677969766, - "narHash": "sha256-AIp/ZYZMNLDZR/H7iiAlaGpu4lcXsVt9JQpBlf43HRY=", + "lastModified": 1680281360, + "narHash": "sha256-XdLTgAzjJNDhAG2V+++0bHpSzfvArvr2pW6omiFfEJk=", "owner": "ryantm", "repo": "agenix", - "rev": "03b51fe8e459a946c4b88dcfb6446e45efb2c24e", + "rev": "e64961977f60388dd0b49572bb0fc453b871f896", "type": "github" }, "original": { @@ -188,11 +188,11 @@ ] }, "locked": { - "lastModified": 1680000368, - "narHash": "sha256-TlgC4IJ7aotynUdkGRtaAVxquaiddO38Ws89nB7VGY8=", + "lastModified": 1680389554, + "narHash": "sha256-+8FUmS4GbDMynQErZGXKg+wU76rq6mI5fprxFXFWKSM=", "owner": "nix-community", "repo": "home-manager", - "rev": "765e4007b6f9f111469a25d1df6540e8e0ca73a6", + "rev": "ddd8866c0306c48f465e7f48432e6f1ecd1da7f8", "type": "github" }, "original": { @@ -227,11 +227,11 @@ ] }, "locked": { - "lastModified": 1679533405, - "narHash": "sha256-LQbHTnEn/jAME1AsJtjif5oVeNWUGdL/RMUZCb2Ts5I=", + "lastModified": 1680291155, + "narHash": "sha256-s1YCdBGhKl3kqlhTICKgfrfHyIbiUczqiUM/TBzCyf4=", "owner": "astro", "repo": "microvm.nix", - "rev": "31d3c1a05fba175e5d96f16256296ad4088ca9f5", + "rev": "2528d10d30524522027878c871b680532b5172da", "type": "github" }, "original": { @@ -257,11 +257,11 @@ }, "nixpkgs": { "locked": { - "lastModified": 1679944645, - "narHash": "sha256-e5Qyoe11UZjVfgRfwNoSU57ZeKuEmjYb77B9IVW7L/M=", + "lastModified": 1680213900, + "narHash": "sha256-cIDr5WZIj3EkKyCgj/6j3HBH4Jj1W296z7HTcWj1aMA=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "4bb072f0a8b267613c127684e099a70e1f6ff106", + "rev": "e3652e0735fbec227f342712f180f4f21f0594f2", "type": "github" }, "original": { @@ -300,11 +300,11 @@ "nixpkgs-stable": "nixpkgs-stable" }, "locked": { - "lastModified": 1678976941, - "narHash": "sha256-skNr08frCwN9NO+7I77MjOHHAw+L410/37JknNld+W4=", + "lastModified": 1680170909, + "narHash": "sha256-FtKU/edv1jFRr/KwUxWTYWXEyj9g8GBrHntC2o8oFI8=", "owner": "cachix", "repo": "pre-commit-hooks.nix", - "rev": "32b1dbedfd77892a6e375737ef04d8efba634e9e", + "rev": "29dbe1efaa91c3a415d8b45d62d48325a4748816", "type": "github" }, "original": { diff --git a/hosts/ward/default.nix b/hosts/ward/default.nix index 9e7d81a..85fca2b 100644 --- a/hosts/ward/default.nix +++ b/hosts/ward/default.nix @@ -22,9 +22,66 @@ boot.initrd.availableKernelModules = ["xhci_pci" "ahci" "nvme" "usbhid" "usb_storage" "sd_mod" "sdhci_pci"]; - microvm.vms.agag = { - flake = self; - updateFlake = microvm; - }; - autostart = ["guest"]; + #services.authelia.instances.main = { + # enable = true; + # settings = { + # theme = "dark"; + # log = { + # level = "info"; + # format = "text"; + # }; + # server = { + # host = "127.0.0.1"; + # port = 9091; + # }; + # session = { + # name = "session"; + # domain = "pas.sh"; + # }; + # authentication_backend.ldap = { + # implementation = "custom"; + # url = "ldap://127.0.0.1:3890"; + # base_dn = "dc=pas,dc=sh"; + # username_attribute = "uid"; + # additional_users_dn = "ou=people"; + # users_filter = "(&({username_attribute}={input})(objectclass=person))"; + # additional_groups_dn = "ou=groups"; + # groups_filter = "(member={dn})"; + # group_name_attribute = "cn"; + # mail_attribute = "mail"; + # display_name_attribute = "uid"; + # user = "uid=authelia,ou=people,dc=pas,dc=sh"; + # }; + # storage.local = { + # path = "/var/lib/authelia-${cfg.name}/db.sqlite3"; + # }; + # access_control = { + # default_policy = "deny"; + # }; + # notifier.smtp = rec { + # host = "smtp.fastmail.com"; + # port = 587; + # username = "a@example.com"; + # sender = "noreply@example.com"; + # startup_check_address = sender; + # disable_html_emails = true; + # }; + # identity_providers.oidc = { + # cors.allowed_origins_from_client_redirect_uris = true; + # cors.endpoints = [ + # "authorization" + # "introspection" + # "revocation" + # "token" + # "userinfo" + # ]; + # }; + # }; + #}; + + #microvm.vms.agag = { + # flake = self; + # updateFlake = microvm; + #}; + #microvm.autostart = ["guest"]; } diff --git a/hosts/ward/vaultwarden.nix b/hosts/ward/vaultwarden.nix new file mode 100644 index 0000000..e5f261d --- /dev/null +++ b/hosts/ward/vaultwarden.nix @@ -0,0 +1,81 @@ +{ + config, + nodeSecrets, + ... +}: { + services.vaultwarden = { + enable = true; + dbBackend = "sqlite"; + settings = { + DATA_FOLDER = "/var/lib/vaultwarden"; + EXTENDED_LOGGING = true; + USE_SYSLOG = true; + WEB_VAULT_ENABLED = true; + + WEBSOCKET_ENABLED = true; + WEBSOCKET_ADDRESS = "127.0.0.1"; + WEBSOCKET_PORT = 3012; + ROCKET_ADDRESS = "127.0.0.1"; + ROCKET_PORT = 8012; + + SIGNUPS_ALLOWED = false; + PASSWORD_ITERATIONS = 1000000; + INVITATIONS_ALLOWED = true; + INVITATION_ORG_NAME = "Vaultwarden"; + DOMAIN = nodeSecrets.vaultwarden.domain; + + SMTP_EMBED_IMAGES = true; + }; + #backupDir = "/data/backup"; + #YUBICO_CLIENT_ID=; + #YUBICO_SECRET_KEY=; + #ADMIN_TOKEN="$argon2id:TODO"; + #SMTP_HOST={{ vaultwarden_smtp_host }}; + #SMTP_FROM={{ vaultwarden_smtp_from }}; + #SMTP_FROM_NAME={{ vaultwarden_smtp_from_name }}; + #SMTP_PORT = 465; + #SMTP_SECURITY = "force_tls"; + #SMTP_USERNAME={{ vaultwarden_smtp_username }}; + #SMTP_PASSWORD={{ vaultwarden_smtp_password }}; + #environmentFile = config.rekey.secrets.vaultwarden-env.path; + }; + + # Replace uses of old name + systemd.services.vaultwarden.seviceConfig.StateDirectory = "vaultwarden"; + systemd.services.backup-vaultwarden.environment.DATA_FOLDER = "/var/lib/vaultwarden"; + + services.nginx = { + upstreams."vaultwarden" = { + servers = {"localhost:8012" = {};}; + extraConfig = '' + zone vaultwarden 64k; + keepalive 2; + ''; + }; + upstreams."vaultwarden-websocket" = { + servers = {"localhost:3012" = {};}; + extraConfig = '' + zone vaultwarden-websocket 64k; + keepalive 2; + ''; + }; + virtualHosts."${nodeSecrets.vaultwarden.domain}" = { + forceSSL = true; + #enableACME = true; + sslCertificate = config.rekey.secrets."selfcert.crt".path; + sslCertificateKey = config.rekey.secrets."selfcert.key".path; + locations."/" = { + proxyPass = "http://vaultwarden"; + proxyWebsockets = true; + }; + locations."/notifications/hub" = { + proxyPass = "http://vaultwarden-websocket"; + proxyWebsockets = true; + }; + locations."/notifications/hub/negotiate" = { + proxyPass = "http://vaultwarden"; + proxyWebsockets = true; + }; + }; + }; +} diff --git a/hosts/zackbiene/default.nix b/hosts/zackbiene/default.nix index cd024ce..f68b579 100644 --- a/hosts/zackbiene/default.nix +++ b/hosts/zackbiene/default.nix @@ -16,7 +16,7 @@ ./fs.nix ./net.nix - ./dnsmasq.nix + #./dnsmasq.nix ./esphome.nix ./home-assistant.nix ./hostapd.nix diff --git a/hosts/zackbiene/hostapd.nix b/hosts/zackbiene/hostapd.nix index 21b4856..d44fac3 100644 --- a/hosts/zackbiene/hostapd.nix +++ b/hosts/zackbiene/hostapd.nix @@ -2,6 +2,7 @@ lib, config, pkgs, + nodeSecrets, ... }: { imports = [../../modules/hostapd.nix]; @@ -12,19 +13,24 @@ services.hostapd = { enable = true; - interfaces = { - "wlan1" = { - ssid = "🍯🐝💨"; - hwMode = "g"; - countryCode = "DE"; - channel = 13; # Automatic Channel Selection (ACS) is unfortunately not implemented for mt7612u. + radios.wlan1 = { + hwMode = "g"; + countryCode = "DE"; + channel = 13; # Automatic Channel Selection (ACS) is unfortunately not implemented for mt7612u. + wifi4.capabilities = ["LDPC" "HT40+" "HT40-" "GF" "SHORT-GI-20" "SHORT-GI-40" "TX-STBC" "RX-STBC1"]; + networks.wlan1 = { + inherit (nodeSecrets.hostapd) ssid; macAcl = "deny"; apIsolate = true; authentication = { saePasswordsFile = config.rekey.secrets.wifi-clients.path; saeAddToMacAllow = true; + enableRecommendedPairwiseCiphers = true; }; - wifi4.capabilities = ["LDPC" "HT40+" "HT40-" "GF" "SHORT-GI-20" "SHORT-GI-40" "TX-STBC" "RX-STBC1"]; + }; + networks.wlan1-1 = { + ssid = "Open"; + authentication.mode = "none"; }; }; }; diff --git a/hosts/zackbiene/secrets/secrets.nix.age b/hosts/zackbiene/secrets/secrets.nix.age index 01af9cec22534f62b3221f5bac2eb06bcd27dfa9..b82df1a8150360baec3d0b50f5f99bb205abfff7 100644 GIT binary patch delta 699 zcmV;s0!01M1rAFiTT$aBXQTZC6xSHg88vLQr*WXEJkhb!IqAa!yE7 zY*;f^Zwhu(MnXA5Lv(aDQZ!n4D@IQ+OF~mhY&mCUSyf>&YEEt|RW(;eYHKhxQ3@?S zAaH4REpRe5HXwL$Q)M_&AVF|%IcRECVQXP&X;pJ-G;vTjQGah@bxSvLc0*%wZDB}D zVNEhOWH&iPN=gbdb7o3aQ&VDfcX(MiO=5O3MPpPeIZSm>aAj~vPh)vlSVdYmY)m*= zW_JoLJ|J31EoX9NVRL05M>1FnMoc(GP)b2fXH{cPT6cC~Xj51}Yhy}A zOlB`Lc58V^MSpiIZ8t}6GkA1vO)*+SVO3EtG!eVqsD*d2C`$ zMQKP%cr!>?V`ei>LsDZ)VnRnzFIjajNmEv9dRlO9S2#v`SwuBa3N0-yAWlwJL3db8 zGH*d=S5;3oYfV!{H8N&5Ojkldd15eBb5BTFSxZYoYJXKuSqg2J{FJ8p`|>d)bYN2! zSM>Sl6BW<9!#YShLkS)_dyO=cNMi6~1`x->fyuj0Nr&IMkIt{%hX(0D)>plYJx1LR z+R4%KkJ#Db)qMT9l!W^Rlc>~TDChTe(*4`+Pj`ZsBUmtuVqdc47_f$V@~+WuUejr` zdiy4FD1X!yS;S*mL_RO0NYS#(0=2{X(I)fhYc{hFjK$B59G`PncSKarQ{KxWql_c0 z(;dWFX8=4Y)9_(t*0)o`V-`=z{Gt+({T0~Rwt5WWx}ju81PXt?LM&uJ)pDnSeWq=Q zoA?7odQqA0JgvF>*0Q6dt`37pZd!`*Zy+nl$28;!+(ET=UHj`Nx%OiGHa-1PTs5;N h2_J>173+8j2iJv!X>JypguC!D(5WZ1hL}WxP*AV59`*nL delta 701 zcmV;u0z&=I1RdF~qGHXw3HaTx% zD?@WfK?+PkL~>STH&;%2LNP-!Oie{rR#tgPL}6HFFEe^cW^H&`S!XMDIde5_Sqd#a zAaH4REpRe5HXwL$Q)M_&AVD~AXHGdVZEZJERc}ptaxX_jY=1&jXIf7+Qdm?lSwmrQ zWNu7OOm;zcIClzqV`)}qH*7*NdPG)IbZt&$H&QfVb3tfoXk#yNGc#pvWqgaH!Wv!Wnpt=AW~3lL~kiGX&_y3b5VO>OK%`)dOsuzD{*WyLP9rk zD@0gsc1n6?LVrecNmoolYDG~|Q)W4NSYudIa7s`$W;IqraCKr+LN9b^Y+7kSS4UMV zYBzZ>ZwhlqZb39NF-C4@bZ}^4S9NM*Q8H0xc{WvHV|aIOFG5O2SyOE^ZC6t;WHWbH zcx_ovRdh{iby;~?W^Yh=L^KLaZ!t*szHGg4qHZ)LJF=R_cT6arzWlCC6 zYfy1CZC7PYWo0l}S8;YjWo1i23PT?z7k4|_4)pkP*^Myr2Dll#6|rG&cHX>n*Y5d` zIQR79$;lSSs3e6N!DxT+PHB2045E{DjYg|GEbHyoC!sx|vJ}P+Z@Ik|0?gp>oxa{% z!!eN%U4O}9(?zZbB-giqG*H2Cf|$Q>ZMS)#CEV&+m?j}ksZoO>Wb@U$+zQ&FY2;aE zw?-RT`*aF&l9fGdGBF!S)GA7HYm$*RvSB5+PnLroMSAW4v48N;9C@q2z%A7L0&m#@&ifU4 diff --git a/hosts/zackbiene/secrets/wifi-clients.age b/hosts/zackbiene/secrets/wifi-clients.age index 71baefc9473f4430f70962db3573422106c1834e..0c63208693a7a0391a36d6b6aa1481e33cb8c6d4 100644 GIT binary patch delta 508 zcmVNO@XKRAWsvLuO}o zMtEaVcs5C8c?xhVa9L<%bZ&JyF>6UiIY~A!SXf$6FmFU?L`rCEVs>?Ec5rYtP*7$w zO$seOAW%_zIAUEbXL4m>b7de$Q9MU4E?WvvSu=7+ZAoHlR8w(SX?0>aH*+v^LNG*d zP&7wOMp8Cbc7I1vQ%F#EPj)wHLo-HqW?EEGPjhNjRWW8$P%lGT3Rq?{S~+iUFI zd0J~t3N=MUc}QY7FLpOoNM?F7b#_5jQEhcvHcVkJMNThEWOR5rHdjhyS5ZxG3N1b$ zaA|fea56PEAb4?8WjIkFL2yG_Ib~>fGDUY%MNe8zL@_oiIDctGa&&Q5W@$)wG&nD7 zQcYGhS8!-pR0=scD^fLPL3decVK7HQMn!QeLT@r!FGo=~MQ=%UQCUk*QD;;|R#|Ro zRSGRWAT~cOXL4m>b7df3MIe44Wj|dbAXo}BRcTIIL}x~CZg*8sVOK&>bVhJea7IZ{ zM`J5BO>TNPPk%&rHd#qV3N0-yAXIlbHgQ%tYH&72X-#HGOJZhncQImFD=S4eMQS!d zZE|ByLPkweO<^*33UyJoinREpTgmBc+TPYY*6rjJ9{epKnHI#0*^g(&pmx~X6AH)b zA)1hhEug73O1!dUa|L0YxTcm-gUwcnL9+>$0W2i^s1l-JMC?G5P{2UA7T64hb{+nv D6H1ce diff --git a/modules/hostapd.nix b/modules/hostapd.nix index ace87ed..6cb9d9d 100644 --- a/modules/hostapd.nix +++ b/modules/hostapd.nix @@ -4,7 +4,17 @@ pkgs, utils, ... -}: let +}: +# All hope abandon ye who enter here. hostapd's configuration +# format is ... special, and you won't be able to infer any +# of their assumptions from just reading the "documentation" +# (i.e. the example config). Also, do NOT try to make this RFC 42 +# compatible. The format is non-standard, some options may repeat, +# are order-dependent, or completely switch into another global context. +# Assume footguns at all points - to make informed decisions you +# will probably need to look at hostapd's code. You have been warned, +# proceed with care. +let inherit (lib) attrNames @@ -17,16 +27,19 @@ escapeShellArg filter getAttr + imap0 literalExpression mapAttrsToList mdDoc mkIf mkOption optional + optionals optionalString stringLength toLower types + unique ; cfg = config.services.hostapd; @@ -36,91 +49,60 @@ then "1" else "0"; - configFileForInterface = interface: ifcfg: - pkgs.writeText "hostapd-${interface}.conf" '' + genSaePasswordEntry = entry: + "sae_password=${entry.password}" + + optionalString (entry.mac != null) "|mac=${entry.mac}" + + optionalString (entry.vlanid != null) "|vlanid=${toString entry.vlanid}" + + optionalString (entry.pk != null) "|pk=${entry.pk}" + + optionalString (entry.id != null) "|id=${entry.id}" + + "\n"; + + # Generates the configuration for a single BSS (i.e. WiFi network) + writeBssConfig = radio: radioCfg: bss: bssCfg: bssIdx: let + pairwiseCiphers = + concatStringsSep " " (unique (bssCfg.authentication.pairwiseCiphers + ++ optionals bssCfg.authentication.enableRecommendedPairwiseCiphers ["CCMP" "CCMP-256" "GCMP" "GCMP-256"])); + in + pkgs.writeText "hostapd-radio-${radio}-bss-${bss}.conf" '' + ##### BSS ${toString bssIdx}: ${bss} ####################################### + + ${ + if bssIdx == 0 + then "interface" + else "bss" + }=${bss} + ssid=${bssCfg.ssid} + utf8_ssid=${bool01 bssCfg.utf8Ssid} + logger_syslog=-1 - logger_syslog_level=${toString ifcfg.logLevel} + logger_syslog_level=${toString bssCfg.logLevel} logger_stdout=-1 - logger_stdout_level=${toString ifcfg.logLevel} - - interface=${interface} - driver=${ifcfg.driver} + logger_stdout_level=${toString bssCfg.logLevel} ctrl_interface=/run/hostapd - ctrl_interface_group=${ifcfg.group} + ctrl_interface_group=${bssCfg.group} - ##### IEEE 802.11 general configuration ####################################### - - ssid=${ifcfg.ssid} - utf8_ssid=${bool01 ifcfg.utf8Ssid} - ${optionalString (ifcfg.countryCode != null) '' - country_code=${ifcfg.countryCode} - # IEEE 802.11d: Limit to frequencies allowed in country - ieee80211d=1 - # IEEE 802.11h: Enable radar detection and DFS (Dynamic Frequency Selection) - ieee80211h=1 - ''} - hw_mode=${ifcfg.hwMode} - channel=${toString ifcfg.channel} - noscan=${bool01 ifcfg.noScan} # Set the MAC-address access control mode - macaddr_acl=${ifcfg.macAcl} - ${optionalString (ifcfg.macAllow != [] || ifcfg.macAllowFile != null || ifcfg.authentication.saeAddToMacAllow) '' - accept_mac_file=/run/hostapd/${interface}.mac.allow + macaddr_acl=${bssCfg.macAcl} + ${optionalString (bssCfg.macAllow != [] || bssCfg.macAllowFile != null || bssCfg.authentication.saeAddToMacAllow) '' + accept_mac_file=/run/hostapd/${bss}.mac.allow ''} - ${optionalString (ifcfg.macDeny != [] || ifcfg.macDenyFile != null) '' - deny_mac_file=/run/hostapd/${interface}.mac.deny + ${optionalString (bssCfg.macDeny != [] || bssCfg.macDenyFile != null) '' + deny_mac_file=/run/hostapd/${bss}.mac.deny ''} # Only allow WPA, disable insecure WEP auth_algs=1 - ignore_broadcast_ssid=${ifcfg.ignoreBroadcastSsid} + ignore_broadcast_ssid=${bssCfg.ignoreBroadcastSsid} # Always enable QoS, which is required for 802.11n and above wmm_enabled=1 - ap_isolate=${bool01 ifcfg.apIsolate} - - ##### IEEE 802.11n (WiFi 4) related configuration ####################################### - - ieee80211n=${bool01 ifcfg.wifi4.enable} - ${optionalString ifcfg.wifi4.enable '' - ht_capab=${concatMapStrings (x: "[${x}]") ifcfg.wifi4.capabilities} - require_ht=${bool01 ifcfg.wifi4.require} - ''} - - ${optionalString ifcfg.wifi5.enable '' - ##### IEEE 802.11ac (WiFi 5) related configuration ##################################### - - ieee80211ac=1 - vht_capab=${concatMapStrings (x: "[${x}]") ifcfg.wifi5.capabilities} - require_vht=${bool01 ifcfg.wifi5.require} - vht_oper_chwidth=${ifcfg.wifi5.operatingChannelWidth} - ''} - ${optionalString ifcfg.wifi6.enable '' - ##### IEEE 802.11ax (WiFi 6) related configuration ##################################### - - ieee80211ax=1 - require_he=${bool01 ifcfg.wifi6.require} - he_oper_chwidth=${ifcfg.wifi6.operatingChannelWidth} - he_su_beamformer=${bool01 ifcfg.wifi6.singleUserBeamformer} - he_su_beamformee=${bool01 ifcfg.wifi6.singleUserBeamformee} - he_mu_beamformer=${bool01 ifcfg.wifi6.multiUserBeamformer} - ''} - ${optionalString ifcfg.wifi7.enable '' - ##### IEEE 802.11be (WiFi 7) related configuration ##################################### - - ieee80211be=1 - eht_oper_chwidth=${ifcfg.wifi7.operatingChannelWidth} - eht_su_beamformer=${bool01 ifcfg.wifi7.singleUserBeamformer} - eht_su_beamformee=${bool01 ifcfg.wifi7.singleUserBeamformee} - eht_mu_beamformer=${bool01 ifcfg.wifi7.multiUserBeamformer} - ''} - - ##### WPA/IEEE 802.11i configuration ########################################## + ap_isolate=${bool01 bssCfg.apIsolate} + ##### IEEE 802.11i (authentication) related configuration # Encrypt management frames to protect against deauthentication and similar attacks - ieee80211w=${ifcfg.managementFrameProtection} - ${optionalString (ifcfg.authentication.mode == "none") '' + ieee80211w=${bssCfg.managementFrameProtection} + ${optionalString (bssCfg.authentication.mode == "none") '' wpa=0 ''} - ${optionalString (ifcfg.authentication.mode == "wpa3-sae") '' + ${optionalString (bssCfg.authentication.mode == "wpa3-sae") '' wpa=2 wpa_key_mgmt=SAE # Derive PWE using both hunting-and-pecking loop and hash-to-element @@ -129,102 +111,176 @@ # disable any transition modes from now on. transition_disable=0x01 ''} - ${optionalString (ifcfg.authentication.mode == "wpa3-sae-transition") '' + ${optionalString (bssCfg.authentication.mode == "wpa3-sae-transition") '' wpa=2 wpa_key_mgmt=WPA-PSK-SHA256 SAE ''} - ${optionalString (ifcfg.authentication.mode == "wpa2-sha256") '' + ${optionalString (bssCfg.authentication.mode == "wpa2-sha256") '' wpa=2 wpa_key_mgmt=WPA-PSK-SHA256 ''} - ${optionalString (ifcfg.authentication.mode != "none") '' - wpa_pairwise=${concatStringsSep " " ifcfg.authentication.pairwiseCiphers} - rsn_pairwise=${concatStringsSep " " ifcfg.authentication.pairwiseCiphers} + ${optionalString (bssCfg.authentication.mode != "none") '' + wpa_pairwise=${pairwiseCiphers} + rsn_pairwise=${pairwiseCiphers} ''} - ${optionalString (ifcfg.authentication.wpaPassword != null) '' - wpa_passphrase=${ifcfg.authentication.wpaPassword} + ${optionalString (bssCfg.authentication.wpaPassword != null) '' + wpa_passphrase=${bssCfg.authentication.wpaPassword} ''} - ${optionalString (ifcfg.authentication.wpaPskFile != null) '' - wpa_passphrase=${ifcfg.authentication.wpaPskFile} + ${optionalString (bssCfg.authentication.wpaPskFile != null) '' + wpa_passphrase=${bssCfg.authentication.wpaPskFile} ''} - ${optionalString (ifcfg.authentication.saePasswords != []) (concatMapStrings (pw: "sae_password=${pw}\n") ifcfg.authentication.saePasswords)} + ${optionalString (bssCfg.authentication.saePasswords != []) (concatMapStrings genSaePasswordEntry bssCfg.authentication.saePasswords)} ''; - makeInterfaceRuntimeFiles = interface: ifcfg: let - # All MAC addresses from SAE entries that aren't the wildcard address - saeMacs = filter (mac: mac != null && (toLower mac) != "ff:ff:ff:ff:ff:ff") (map (x: x.mac) ifcfg.authentication.saePasswords); - in - pkgs.writeShellScript "make-hostapd-${interface}-files" ('' + writeRadioBaseConfig = radio: radioCfg: + pkgs.writeText "hostapd-radio-${radio}-base.conf" '' + driver=${radioCfg.driver} + + ##### IEEE 802.11 general configuration ####################################### + ${optionalString (radioCfg.countryCode != null) '' + country_code=${radioCfg.countryCode} + # IEEE 802.11d: Limit to frequencies allowed in country + ieee80211d=1 + # IEEE 802.11h: Enable radar detection and DFS (Dynamic Frequency Selection) + ieee80211h=1 + ''} + hw_mode=${radioCfg.hwMode} + channel=${toString radioCfg.channel} + noscan=${bool01 radioCfg.noScan} + + ##### IEEE 802.11n (WiFi 4) related configuration ####################################### + ieee80211n=${bool01 radioCfg.wifi4.enable} + ${optionalString radioCfg.wifi4.enable '' + ht_capab=${concatMapStrings (x: "[${x}]") radioCfg.wifi4.capabilities} + require_ht=${bool01 radioCfg.wifi4.require} + ''} + + ##### IEEE 802.11ac (WiFi 5) related configuration ##################################### + ieee80211ac=${bool01 radioCfg.wifi5.enable} + ${optionalString radioCfg.wifi5.enable '' + vht_capab=${concatMapStrings (x: "[${x}]") radioCfg.wifi5.capabilities} + require_vht=${bool01 radioCfg.wifi5.require} + vht_oper_chwidth=${radioCfg.wifi5.operatingChannelWidth} + ''} + + ${ # ieee80211ax support must be enabled in hostapd, + # so the enable option cannot be included unconditionally + optionalString radioCfg.wifi6.enable '' + ##### IEEE 802.11ax (WiFi 6) related configuration ##################################### + ieee80211ax=1 + require_he=${bool01 radioCfg.wifi6.require} + he_oper_chwidth=${radioCfg.wifi6.operatingChannelWidth} + he_su_beamformer=${bool01 radioCfg.wifi6.singleUserBeamformer} + he_su_beamformee=${bool01 radioCfg.wifi6.singleUserBeamformee} + he_mu_beamformer=${bool01 radioCfg.wifi6.multiUserBeamformer} + '' + } + + ${ # ieee80211be support must be enabled in hostapd, + # so the enable option cannot be included unconditionally + optionalString radioCfg.wifi7.enable '' + ##### IEEE 802.11be (WiFi 7) related configuration ##################################### + ieee80211be=1 + eht_oper_chwidth=${radioCfg.wifi7.operatingChannelWidth} + eht_su_beamformer=${bool01 radioCfg.wifi7.singleUserBeamformer} + eht_su_beamformee=${bool01 radioCfg.wifi7.singleUserBeamformee} + eht_mu_beamformer=${bool01 radioCfg.wifi7.multiUserBeamformer} + '' + } + ''; + + makeRadioRuntimeFiles = radio: radioCfg: + pkgs.writeShellScript "make-hostapd-${radio}-files" ('' set -euo pipefail - mac_allow_file=/run/hostapd/${escapeShellArg interface}.mac.allow - mac_deny_file=/run/hostapd/${escapeShellArg interface}.mac.deny - hostapd_config_file=/run/hostapd/${escapeShellArg interface}.hostapd.conf - - rm -f "$mac_allow_file" - touch "$mac_allow_file" - rm -f "$mac_deny_file" - touch "$mac_deny_file" + hostapd_config_file=/run/hostapd/${escapeShellArg radio}.hostapd.conf rm -f "$hostapd_config_file" - cp ${configFileForInterface interface ifcfg} "$hostapd_config_file" + cp ${writeRadioBaseConfig radio radioCfg} "$hostapd_config_file" + ${optionalString (radioCfg.extraConfig != "") '' + cat >> "$hostapd_config_file" <> "$hostapd_config_file" + ''} '' - + concatStringsSep "\n" ( - optional (ifcfg.macAllow != []) '' - cat >> "$mac_allow_file" <> "$mac_allow_file" - '' - # Populate mac allow list from saePasswords - ++ optional (ifcfg.authentication.saeAddToMacAllow && saeMacs != []) '' - cat >> "$mac_allow_file" <> "$mac_allow_file" - '' - # Create combined mac.deny list from macDeny and macDenyFile - ++ optional (ifcfg.macDeny != []) '' - cat >> "$mac_deny_file" <> "$mac_deny_file" - '' - # Add WPA passphrase from file if necessary - ++ optional (ifcfg.authentication.wpaPasswordFile != null) '' - cat >> "$hostapd_config_file" <> "$hostapd_config_file" - '' - # Finally append extraConfig if necessary. - ++ optional (ifcfg.extraConfig != "") '' - cat >> "$hostapd_config_file" <> "$hostapd_config_file" - '' - )); + mac_deny_file=/run/hostapd/${escapeShellArg bss}.mac.deny + rm -f "$mac_deny_file" + touch "$mac_deny_file" - runtimeConfigFiles = mapAttrsToList (i: _: "/run/hostapd/${i}.hostapd.conf") cfg.interfaces; + cat ${writeBssConfig radio radioCfg bss bssCfg bssIdx} >> "$hostapd_config_file" + + '' + + concatStringsSep "\n" ( + optional (bssCfg.macAllow != []) '' + cat >> "$mac_allow_file" <> "$mac_allow_file" + '' + # Populate mac allow list from saePasswords + ++ optional (bssCfg.authentication.saeAddToMacAllow && saeMacs != []) '' + cat >> "$mac_allow_file" <> "$mac_allow_file" + '' + # Create combined mac.deny list from macDeny and macDenyFile + ++ optional (bssCfg.macDeny != []) '' + cat >> "$mac_deny_file" <> "$mac_deny_file" + '' + # Add WPA passphrase from file if necessary + ++ optional (bssCfg.authentication.wpaPasswordFile != null) '' + cat >> "$hostapd_config_file" <> "$hostapd_config_file" + '' + # Finally append extraConfig if necessary. + ++ optional (bssCfg.extraConfig != "") '' + cat >> "$hostapd_config_file" <> "$hostapd_config_file" + '' + ) + ) + radioCfg.networks))); + + runtimeConfigFiles = mapAttrsToList (radio: _: "/run/hostapd/${radio}.hostapd.conf") cfg.radios; in { options = { services.hostapd = { @@ -239,61 +295,64 @@ in { ''; }; - interfaces = mkOption { + radios = mkOption { default = {}; example = literalExpression '' { # Simple 2.4GHz AP - "wlp2s0" = { - ssid = "AP 1"; + wlp2s0 = { # countryCode = "US"; - authentication.saePasswords = [{ password = "a flakey password"; }]; # Use saePasswordsFile if possible. + networks.wlp2s0 = { + ssid = "AP 1"; + authentication.saePasswords = [{ password = "a flakey password"; }]; # Use saePasswordsFile if possible. + }; }; - # WiFi 5 (5GHz) - "wlp4s0" = { - ssid = "Open AP with WiFi5"; - # countryCode = "US"; + # WiFi 5 (5GHz) with two advertised networks + wlp3s0 = { hwMode = "a"; - authentication.mode = "none"; + channel = 0; # Enable automatic channel selection (ACS). Use only if your hardware supports it. + # countryCode = "US"; + networks.wlp3s0 = { + ssid = "My AP"; + authentication.saePasswords = [{ password = "a flakey password"; }]; # Use saePasswordsFile if possible. + }; + networks.wlp3s0-1 = { + ssid = "Open AP with WiFi5"; + authentication.mode = "none"; + }; }; # Legacy WPA2 example - "wlp5s0" = { - ssid = "AP 2"; + wlp4s0 = { # countryCode = "US"; - channel = 0; # Enables automatic channel selection ACS. Use only if your hardware support's it. - authentication = { - mode = "wpa2-sha256"; - wpaPassword = "a flakey password"; # Use wpaPasswordFile if possible. + networks.wlp4s0 = { + ssid = "AP 2"; + authentication = { + mode = "wpa2-sha256"; + wpaPassword = "a flakey password"; # Use wpaPasswordFile if possible. + }; }; }; } ''; description = mdDoc '' - This option allows you to define APs for one or multiple interfaces. - Each attribute specifies a interface and associates it to its configuration. - At least one interface must be specified. + This option allows you to define APs for one or multiple physical radios. + At least one radio must be specified. - Each interface can only support a single hardware-mode that is configured via - ({option}`services.hostapd.interfaces..hwMode`). To create a dual-band - or tri-band AP, you will have to use a device that supports configuring multiple APs - (Refer to valid interface combinations in {command}`iw list`). For each mode hostapd - requires a separate logical interface (like wlp3s0, wlp3s1, ...). Often this needs - to be configured manually by utilizing udev rules - details will differ for each device. - Alternatively, one can also just use distinct devices for each mode. + For each radio, hostapd requires a separate logical interface (like wlp3s0, wlp3s1, ...). + A default interface is usually be created automatically by your system, but to use + multiple radios with a single device, you will likely have to use {command}`iw` to create + additional logical interfaces. + + Each physical radio can only support a single hardware-mode that is configured via + ({option}`services.hostapd.radios..hwMode`). To create a dual-band + or tri-band AP, you will have to use a device that has multiple physical radios + and supports configuring multiple APs (Refer to valid interface combinations in + {command}`iw list`). ''; type = types.attrsOf (types.submodule { options = { - noScan = mkOption { - type = types.bool; - default = false; - description = mdDoc '' - Disables scan for overlapping BSSs in HT40+/- mode. - Caution: turning this on will likely violate regulatory requirements! - ''; - }; - driver = mkOption { default = "nl80211"; example = "none"; @@ -307,40 +366,13 @@ in { ''; }; - logLevel = mkOption { - default = 2; - type = types.int; - description = mdDoc '' - Levels (minimum value for logged events): - 0 = verbose debugging - 1 = debugging - 2 = informational messages - 3 = notification - 4 = warning - ''; - }; - - group = mkOption { - default = "wheel"; - example = "network"; - type = types.str; - description = mdDoc '' - Members of this group can access the control socket for this interface. - ''; - }; - - utf8Ssid = mkOption { - default = true; + noScan = mkOption { type = types.bool; - description = mdDoc "Whether the SSID is to be interpreted using UTF-8 encoding"; - }; - - ssid = mkOption { - default = config.system.nixos.distroId; - defaultText = literalExpression "config.system.nixos.distroId"; - example = "❄️ cool ❄️"; - type = types.str; - description = mdDoc "SSID to be used in IEEE 802.11 management frames."; + default = false; + description = mdDoc '' + Disables scan for overlapping BSSs in HT40+/- mode. + Caution: turning this on will likely violate regulatory requirements! + ''; }; countryCode = mkOption { @@ -373,12 +405,12 @@ in { g = IEEE 802.11g (2.4 GHz), ad = IEEE 802.11ad (60 GHz); a/g options are used with IEEE 802.11n (HT), too, to specify band). For IEEE 802.11ac (VHT), this needs to be set to hw_mode=a. For IEEE 802.11ax (HE) on 6 GHz this needs - to be set to hw_mode=a. When using ACS (see channel parameter), a - special value "any" can be used to indicate that any support band can be used. - This special case is currently supported only with drivers with which - offloaded ACS is used. + to be set to hw_mode=a. When using ACS (see `channel` parameter), the + special value "any" can be used to indicate that any supported band may be selected. + This special case is currently only supported with hardware for which the driver + implements offloaded ACS. - Most likely you to select 'a' (5GHz & 6GHz a/n/ac/ax) or 'g' (2.4GHz b/g/n) here. + Most likely you want to select 'a' (5GHz & 6GHz a/n/ac/ax) or 'g' (2.4GHz b/g/n) here. ''; }; @@ -393,327 +425,16 @@ in { ''; }; - macAcl = mkOption { - default = "allow"; - type = types.enum ["allow" "deny" "radius"]; - apply = x: - getAttr x { - "allow" = "0"; - "deny" = "1"; - "radius" = "2"; - }; - description = mdDoc '' - Station MAC address -based authentication. The following modes are available: - - - {var}`"allow"`: Allow unless listed in {option}`macDeny` (default) - - {var}`"deny"`: Deny unless listed in {option}`macAllow` - - {var}`"radius"`: Use external radius server, but check both {option}`macAllow` and {option}`macDeny` first - - Please note that this kind of access control requires a driver that uses - hostapd to take care of management frame processing and as such, this can be - used with driver=hostap or driver=nl80211, but not with driver=atheros. - ''; - }; - - macAllow = mkOption { - type = types.listOf types.str; - default = []; - example = ["11:22:33:44:55:66"]; - description = mdDoc '' - Specifies the MAC addresses to allow if {option}`macAcl` is set to {var}`"deny"` or {var}`"radius"`. - These values will be world-readable in the Nix store. Values will automatically be merged with - {option}`macAllowFile` if necessary. - ''; - }; - - macAllowFile = mkOption { - type = types.uniq (types.nullOr types.path); - default = null; - 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 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. - ''; - }; - - macDeny = mkOption { - type = types.listOf types.str; - default = []; - example = ["11:22:33:44:55:66"]; - description = mdDoc '' - Specifies the MAC addresses to deny if {option}`macAcl` is set to {var}`"allow"` or {var}`"radius"`. - These values will be world-readable in the Nix store. Values will automatically be merged with - {option}`macDenyFile` if necessary. - ''; - }; - - macDenyFile = mkOption { - type = types.uniq (types.nullOr types.path); - default = null; - 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 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. - ''; - }; - - ignoreBroadcastSsid = mkOption { - default = "disabled"; - type = types.enum ["disabled" "empty" "clear"]; - apply = x: - getAttr x { - "disabled" = "0"; - "empty" = "1"; - "clear" = "2"; - }; - description = mdDoc '' - Send empty SSID in beacons and ignore probe request frames that do not - specify full SSID, i.e., require stations to know SSID. Note that this does - not increase security, since your clients will then broadcast the SSID instead, - which can increase congestion. - - - {var}`"disabled"`: Advertise ssid normally. - - {var}`"empty"`: send empty (length=0) SSID in beacon and ignore probe request for broadcast SSID - - {var}`"clear"`: clear SSID (ASCII 0), but keep the original length (this may be required with some - legacy clients that do not support empty SSID) and ignore probe requests for broadcast SSID. Only - use this if empty does not work with your clients. - ''; - }; - - apIsolate = mkOption { - default = false; - type = types.bool; - description = mdDoc '' - Isolate traffic between stations (clients) and prevent them from - communicating with each other. - ''; - }; - extraConfig = mkOption { default = ""; example = '' - multi_ap=1 + acs_exclude_dfs=1 ''; type = types.lines; - description = mdDoc "Extra configuration options to put at the end of this interface's hostapd.conf."; - }; - - #### IEEE 802.11i (WPA) configuration - - authentication = { - mode = mkOption { - default = "wpa3-sae"; - type = types.enum ["none" "wpa2-sha256" "wpa3-sae-transition" "wpa3-sae"]; - description = mdDoc '' - Selects the authentication mode for this AP. - - - {var}`"none"`: Don't configure any authentication. This will disable wpa alltogether - and create an open AP. Use {option}`extraConfig` together with this option if you - want to configure the authentication manually. Any password options will still be - effective, if set. - - {var}`"wpa2-sha256"`: WPA2-Personal using SHA256 (IEEE 802.11i/RSN). Passwords are set - using {option}`wpaPassword` or preferably by {option}`wpaPasswordFile` or {option}`wpaPskFile`. - - {var}`"wpa3-sae-transition"`: Use WPA3-Personal (SAE) if possible, otherwise fallback - to WPA2-SHA256. Only use if necessary and switch to the newer WPA3-SAE when possible. - You will have to specify both {option}`wpaPassword` and {option}`saePasswords` (or one of their alternatives). - - {var}`"wpa3-sae"`: Use WPA3-Personal (SAE). This is currently the recommended way to - setup a secured WiFi AP (as of March 2023) and therefore the default. Passwords are set - using either {option}`saePasswords` or preferably {option}`saePasswordsFile`. - ''; - }; - - 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"; - type = types.uniq (types.nullOr types.str); - description = mdDoc '' - Sets the password for WPA-PSK that will be converted to the pre-shared key. - The password length must be in the range [8, 63] characters. While some devices - may allow arbitrary characters (such as UTF-8) to be used, but the standard specifies - that each character in the passphrase must be an ASCII character in the range [0x20, 0x7e] - (IEEE Std. 802.11i-2004, Annex H.4.1). Use emojis at your own risk. - - Not used when {option}`mode` is {var}`"wpa3-sae"`. - - Warning: This password will get put into a world-readable file in the Nix store! - Using {option}`wpaPasswordFile` or {option}`wpaPskFile` instead is recommended. - ''; - }; - - wpaPasswordFile = mkOption { - default = null; - type = types.uniq (types.nullOr types.path); - description = mdDoc '' - Sets the password for WPA-PSK. Follows the same rules as {option}`wpaPassword`, - but reads the password from the given file to prevent the password from being - put into the Nix store. - - Not used when {option}`mode` is {var}`"wpa3-sae"`. - ''; - }; - - wpaPskFile = mkOption { - default = null; - type = types.uniq (types.nullOr types.path); - description = mdDoc '' - Sets the password(s) for WPA-PSK. Similar to {option}`wpaPasswordFile`, - but additionally allows specifying multiple passwords, and some other options. - - Each line, except for empty lines and lines starting with #, must contain a - MAC address and either a 64-hex-digit PSK or a password separated with a space. - The password must follow the same rules as outlined in {option}`wpaPassword`. - The special MAC address `00:00:00:00:00:00` can be used to configure PSKs - that any client can use. - - An optional key identifier can be added by prefixing the line with `keyid=` - An optional VLAN ID can be specified by prefixing the line with `vlanid=`. - An optional WPS tag can be added by prefixing the line with `wps=<0/1>` (default: 0). - Any matching entry with that tag will be used when generating a PSK for a WPS Enrollee - instead of generating a new random per-Enrollee PSK. - - Not used when {option}`mode` is {var}`"wpa3-sae"`. - ''; - }; - - saePasswords = mkOption { - default = []; - example = literalExpression '' - [ - # Any client may use these passwords - { password = "Wi-Figure it out"; } - { password = "second password for everyone"; mac = "ff:ff:ff:ff:ff:ff"; } - - # Only the client with MAC-address 11:22:33:44:55:66 can use this password - { password = "sekret pazzword"; mac = "11:22:33:44:55:66"; } - ] - ''; - description = mdDoc '' - Sets allowed passwords for WPA3-SAE. - - The last matching (based on peer MAC address and identifier) entry is used to - select which password to use. An empty string has the special meaning of - removing all previously added entries. - - Warning: These entries will get put into a world-readable file in - the Nix store! Using {option}`saePasswordFile` instead is recommended. - - Not used when {option}`mode` is {var}`"wpa2-sha256"`. - ''; - type = types.listOf (types.submodule { - options = { - password = mkOption { - example = "a flakey password"; - type = types.str; - description = mdDoc '' - The password for this entry. SAE technically imposes no restrictions on - password length or character set. But due to limitations of {command}`hostapd`'s - config file format, a true newline character cannot be parsed. - - Warning: This password will get put into a world-readable file in - the Nix store! Using {option}`wpaPasswordFile` or {option}`wpaPskFile` is recommended. - ''; - }; - - mac = mkOption { - default = null; - example = "11:22:33:44:55:66"; - type = types.uniq (types.nullOr types.str); - description = mdDoc '' - If this attribute is not included, or if is set to the wildcard address (`ff:ff:ff:ff:ff:ff`), - the entry is available for any station (client) to use. If a specific peer MAC address is included, - only a station with that MAC address is allowed to use the entry. - ''; - }; - - vlanid = mkOption { - default = null; - example = 1; - type = types.uniq (types.nullOr types.int); - description = mdDoc "If this attribute is given, all clients using this entry will get tagged with the given VLAN ID."; - }; - - pk = mkOption { - default = null; - example = ""; - type = types.uniq (types.nullOr types.str); - description = mdDoc '' - If this attribute is given, SAE-PK will be enabled for this connection. - This prevents evil-twin attacks, but a public key is required additionally to connect. - (Essentially adds pubkey authentication such that the client can verify identity of the AP) - ''; - }; - - id = mkOption { - default = null; - example = ""; - type = types.uniq (types.nullOr types.str); - description = mdDoc '' - If this attribute is given with non-zero length, it will set the password identifier - for this entry. It can then only be used with that identifier. - ''; - }; - }; - }); - }; - - saePasswordsFile = mkOption { - default = null; - type = types.uniq (types.nullOr types.path); - description = mdDoc '' - Sets the password for WPA3-SAE. Follows the same rules as {option}`saePasswords`, - but reads the entries from the given file to prevent them from being - put into the Nix store. - - One entry per line, empty lines and lines beginning with # will be ignored. - Each line must match the following format, although the order of optional - parameters doesn't matter: - `[|mac=][|vlanid=][|pk=][|id=]` - - Not used when {option}`mode` is {var}`"wpa2-sha256"`. - ''; - }; - - saeAddToMacAllow = mkOption { - type = types.bool; - default = false; - description = mdDoc '' - If set, all sae password entries that have a non-wildcard MAC associated to - them will additionally be used to populate the MAC allow list. This is - additional to any entries set via {option}`macAllow` or {option}`macAllowFile`. - ''; - }; - }; - - managementFrameProtection = mkOption { - default = "required"; - type = types.enum ["disabled" "optional" "required"]; - apply = x: - getAttr x { - "disabled" = "0"; - "optional" = "1"; - "required" = "2"; - }; description = mdDoc '' - Management frame protection (MFP) authenticates management frames - to prevent deauthentication (or related) attacks. - - - {var}`"disabled"`: No management frame protection - - {var}`"optional"`: Use MFP if a connection allows it - - {var}`"required"`: Force MFP for all clients + Extra configuration options to put at the end of global initialization, before defining BSSes. + To find out which options are global and which are per-bss you have to read hostapd's source code, + this is non-trivial and not documented otherwise. ''; }; @@ -801,7 +522,7 @@ in { wifi6 = { enable = mkOption { - # TODO Change this once WiFi 6 is enabled in hostapd upstream + # TODO Change this default once WiFi 6 is enabled in hostapd upstream default = false; type = types.bool; description = mdDoc "Enables support for IEEE 802.11ax (WiFi 6, HE)"; @@ -903,6 +624,410 @@ in { ''; }; }; + + #### BSS definitions + + networks = mkOption { + default = {}; + example = literalExpression '' + { + wlp2s0 = { + ssid = "Primary advertised network"; + authentication.saePasswords = [{ password = "a flakey password"; }]; # Use saePasswordsFile if possible. + }; + wlp2s0-1 = { + ssid = "Secondary advertised network (Open)"; + authentication.mode = "none"; + }; + } + ''; + description = mdDoc '' + This defines a BSS, colloquially known as a WiFi network. + You have to specify at least one. + ''; + type = types.attrsOf (types.submodule { + options = { + logLevel = mkOption { + default = 2; + type = types.int; + description = mdDoc '' + Levels (minimum value for logged events): + 0 = verbose debugging + 1 = debugging + 2 = informational messages + 3 = notification + 4 = warning + ''; + }; + + group = mkOption { + default = "wheel"; + example = "network"; + type = types.str; + description = mdDoc '' + Members of this group can access the control socket for this interface. + ''; + }; + + utf8Ssid = mkOption { + default = true; + type = types.bool; + description = mdDoc "Whether the SSID is to be interpreted using UTF-8 encoding"; + }; + + ssid = mkOption { + default = config.system.nixos.distroId; + defaultText = literalExpression "config.system.nixos.distroId"; + example = "❄️ cool ❄️"; + type = types.str; + description = mdDoc "SSID to be used in IEEE 802.11 management frames."; + }; + + macAcl = mkOption { + default = "allow"; + type = types.enum ["allow" "deny" "radius"]; + apply = x: + getAttr x { + "allow" = "0"; + "deny" = "1"; + "radius" = "2"; + }; + description = mdDoc '' + Station MAC address -based authentication. The following modes are available: + + - {var}`"allow"`: Allow unless listed in {option}`macDeny` (default) + - {var}`"deny"`: Deny unless listed in {option}`macAllow` + - {var}`"radius"`: Use external radius server, but check both {option}`macAllow` and {option}`macDeny` first + + Please note that this kind of access control requires a driver that uses + hostapd to take care of management frame processing and as such, this can be + used with driver=hostap or driver=nl80211, but not with driver=atheros. + ''; + }; + + macAllow = mkOption { + type = types.listOf types.str; + default = []; + example = ["11:22:33:44:55:66"]; + description = mdDoc '' + Specifies the MAC addresses to allow if {option}`macAcl` is set to {var}`"deny"` or {var}`"radius"`. + These values will be world-readable in the Nix store. Values will automatically be merged with + {option}`macAllowFile` if necessary. + ''; + }; + + macAllowFile = mkOption { + type = types.uniq (types.nullOr types.path); + default = null; + 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 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. + ''; + }; + + macDeny = mkOption { + type = types.listOf types.str; + default = []; + example = ["11:22:33:44:55:66"]; + description = mdDoc '' + Specifies the MAC addresses to deny if {option}`macAcl` is set to {var}`"allow"` or {var}`"radius"`. + These values will be world-readable in the Nix store. Values will automatically be merged with + {option}`macDenyFile` if necessary. + ''; + }; + + macDenyFile = mkOption { + type = types.uniq (types.nullOr types.path); + default = null; + 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 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. + ''; + }; + + ignoreBroadcastSsid = mkOption { + default = "disabled"; + type = types.enum ["disabled" "empty" "clear"]; + apply = x: + getAttr x { + "disabled" = "0"; + "empty" = "1"; + "clear" = "2"; + }; + description = mdDoc '' + Send empty SSID in beacons and ignore probe request frames that do not + specify full SSID, i.e., require stations to know SSID. Note that this does + not increase security, since your clients will then broadcast the SSID instead, + which can increase congestion. + + - {var}`"disabled"`: Advertise ssid normally. + - {var}`"empty"`: send empty (length=0) SSID in beacon and ignore probe request for broadcast SSID + - {var}`"clear"`: clear SSID (ASCII 0), but keep the original length (this may be required with some + legacy clients that do not support empty SSID) and ignore probe requests for broadcast SSID. Only + use this if empty does not work with your clients. + ''; + }; + + apIsolate = mkOption { + default = false; + type = types.bool; + description = mdDoc '' + Isolate traffic between stations (clients) and prevent them from + communicating with each other. + ''; + }; + + extraConfig = mkOption { + default = ""; + example = '' + multi_ap=1 + ''; + type = types.lines; + description = mdDoc "Extra configuration options to put at the end of this interface's hostapd.conf."; + }; + + #### IEEE 802.11i (WPA) configuration + + authentication = { + mode = mkOption { + default = "wpa3-sae"; + type = types.enum ["none" "wpa2-sha256" "wpa3-sae-transition" "wpa3-sae"]; + description = mdDoc '' + Selects the authentication mode for this AP. + + - {var}`"none"`: Don't configure any authentication. This will disable wpa alltogether + and create an open AP. Use {option}`extraConfig` together with this option if you + want to configure the authentication manually. Any password options will still be + effective, if set. + - {var}`"wpa2-sha256"`: WPA2-Personal using SHA256 (IEEE 802.11i/RSN). Passwords are set + using {option}`wpaPassword` or preferably by {option}`wpaPasswordFile` or {option}`wpaPskFile`. + - {var}`"wpa3-sae-transition"`: Use WPA3-Personal (SAE) if possible, otherwise fallback + to WPA2-SHA256. Only use if necessary and switch to the newer WPA3-SAE when possible. + You will have to specify both {option}`wpaPassword` and {option}`saePasswords` (or one of their alternatives). + - {var}`"wpa3-sae"`: Use WPA3-Personal (SAE). This is currently the recommended way to + setup a secured WiFi AP (as of March 2023) and therefore the default. Passwords are set + using either {option}`saePasswords` or preferably {option}`saePasswordsFile`. + ''; + }; + + pairwiseCiphers = mkOption { + default = ["CCMP"]; + example = ["CCMP-256" "GCMP-256"]; + type = types.listOf types.str; + description = mdDoc '' + Set of accepted cipher suites (encryption algorithms) for pairwise keys (unicast packets). + By default this allows just CCMP, which is the only commonly supported secure option. + Use {option}`enableRecommendedPairwiseCiphers` to also enable newer recommended ciphers. + + 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 with devices supporting WiFi 5 (IEEE 802.11ac) or higher. + ''; + }; + + enableRecommendedPairwiseCiphers = mkOption { + default = false; + example = true; + type = types.bool; + description = mdDoc '' + Additionally enable the recommended set of pairwise ciphers. + This enables newer secure ciphers, additionally to those defined in {option}`pairwiseCiphers`. + You will have to test whether your hardware supports these by trial-and-error, because + even if `iw list` indicates hardware support, your driver might not expose it. + + Beware {command}`hostapd` will most likely not return a useful error message in case + this is enabled despite the driver or hardware not supporting the newer ciphers. + Look out for messages like `Failed to set beacon parameters`. + ''; + }; + + wpaPassword = mkOption { + default = null; + example = "a flakey password"; + type = types.uniq (types.nullOr types.str); + description = mdDoc '' + Sets the password for WPA-PSK that will be converted to the pre-shared key. + The password length must be in the range [8, 63] characters. While some devices + may allow arbitrary characters (such as UTF-8) to be used, but the standard specifies + that each character in the passphrase must be an ASCII character in the range [0x20, 0x7e] + (IEEE Std. 802.11i-2004, Annex H.4.1). Use emojis at your own risk. + + Not used when {option}`mode` is {var}`"wpa3-sae"`. + + Warning: This password will get put into a world-readable file in the Nix store! + Using {option}`wpaPasswordFile` or {option}`wpaPskFile` instead is recommended. + ''; + }; + + wpaPasswordFile = mkOption { + default = null; + type = types.uniq (types.nullOr types.path); + description = mdDoc '' + Sets the password for WPA-PSK. Follows the same rules as {option}`wpaPassword`, + but reads the password from the given file to prevent the password from being + put into the Nix store. + + Not used when {option}`mode` is {var}`"wpa3-sae"`. + ''; + }; + + wpaPskFile = mkOption { + default = null; + type = types.uniq (types.nullOr types.path); + description = mdDoc '' + Sets the password(s) for WPA-PSK. Similar to {option}`wpaPasswordFile`, + but additionally allows specifying multiple passwords, and some other options. + + Each line, except for empty lines and lines starting with #, must contain a + MAC address and either a 64-hex-digit PSK or a password separated with a space. + The password must follow the same rules as outlined in {option}`wpaPassword`. + The special MAC address `00:00:00:00:00:00` can be used to configure PSKs + that any client can use. + + An optional key identifier can be added by prefixing the line with `keyid=` + An optional VLAN ID can be specified by prefixing the line with `vlanid=`. + An optional WPS tag can be added by prefixing the line with `wps=<0/1>` (default: 0). + Any matching entry with that tag will be used when generating a PSK for a WPS Enrollee + instead of generating a new random per-Enrollee PSK. + + Not used when {option}`mode` is {var}`"wpa3-sae"`. + ''; + }; + + saePasswords = mkOption { + default = []; + example = literalExpression '' + [ + # Any client may use these passwords + { password = "Wi-Figure it out"; } + { password = "second password for everyone"; mac = "ff:ff:ff:ff:ff:ff"; } + + # Only the client with MAC-address 11:22:33:44:55:66 can use this password + { password = "sekret pazzword"; mac = "11:22:33:44:55:66"; } + ] + ''; + description = mdDoc '' + Sets allowed passwords for WPA3-SAE. + + The last matching (based on peer MAC address and identifier) entry is used to + select which password to use. An empty string has the special meaning of + removing all previously added entries. + + Warning: These entries will get put into a world-readable file in + the Nix store! Using {option}`saePasswordFile` instead is recommended. + + Not used when {option}`mode` is {var}`"wpa2-sha256"`. + ''; + type = types.listOf (types.submodule { + options = { + password = mkOption { + example = "a flakey password"; + type = types.str; + description = mdDoc '' + The password for this entry. SAE technically imposes no restrictions on + password length or character set. But due to limitations of {command}`hostapd`'s + config file format, a true newline character cannot be parsed. + + Warning: This password will get put into a world-readable file in + the Nix store! Using {option}`wpaPasswordFile` or {option}`wpaPskFile` is recommended. + ''; + }; + + mac = mkOption { + default = null; + example = "11:22:33:44:55:66"; + type = types.uniq (types.nullOr types.str); + description = mdDoc '' + If this attribute is not included, or if is set to the wildcard address (`ff:ff:ff:ff:ff:ff`), + the entry is available for any station (client) to use. If a specific peer MAC address is included, + only a station with that MAC address is allowed to use the entry. + ''; + }; + + vlanid = mkOption { + default = null; + example = 1; + type = types.uniq (types.nullOr types.int); + description = mdDoc "If this attribute is given, all clients using this entry will get tagged with the given VLAN ID."; + }; + + pk = mkOption { + default = null; + example = ""; + type = types.uniq (types.nullOr types.str); + description = mdDoc '' + If this attribute is given, SAE-PK will be enabled for this connection. + This prevents evil-twin attacks, but a public key is required additionally to connect. + (Essentially adds pubkey authentication such that the client can verify identity of the AP) + ''; + }; + + id = mkOption { + default = null; + example = ""; + type = types.uniq (types.nullOr types.str); + description = mdDoc '' + If this attribute is given with non-zero length, it will set the password identifier + for this entry. It can then only be used with that identifier. + ''; + }; + }; + }); + }; + + saePasswordsFile = mkOption { + default = null; + type = types.uniq (types.nullOr types.path); + description = mdDoc '' + Sets the password for WPA3-SAE. Follows the same rules as {option}`saePasswords`, + but reads the entries from the given file to prevent them from being + put into the Nix store. + + One entry per line, empty lines and lines beginning with # will be ignored. + Each line must match the following format, although the order of optional + parameters doesn't matter: + `[|mac=][|vlanid=][|pk=][|id=]` + + Not used when {option}`mode` is {var}`"wpa2-sha256"`. + ''; + }; + + saeAddToMacAllow = mkOption { + type = types.bool; + default = false; + description = mdDoc '' + If set, all sae password entries that have a non-wildcard MAC associated to + them will additionally be used to populate the MAC allow list. This is + additional to any entries set via {option}`macAllow` or {option}`macAllowFile`. + ''; + }; + }; + + managementFrameProtection = mkOption { + default = "required"; + type = types.enum ["disabled" "optional" "required"]; + apply = x: + getAttr x { + "disabled" = "0"; + "optional" = "1"; + "required" = "2"; + }; + description = mdDoc '' + Management frame protection (MFP) authenticates management frames + to prevent deauthentication (or related) attacks. + + - {var}`"disabled"`: No management frame protection + - {var}`"optional"`: Use MFP if a connection allows it + - {var}`"required"`: Force MFP for all clients + ''; + }; + }; + }); + }; }; }); }; @@ -913,52 +1038,72 @@ in { assertions = [ { - assertion = cfg.interfaces != {}; - message = "At least one interface must be configured with hostapd!"; + assertion = cfg.radios != {}; + message = "At least one radio must be configured with hostapd!"; } ] - # Interface warnings - ++ (concatLists (mapAttrsToList (interface: ifcfg: let - countWpaPasswordDefinitions = count (x: x != null) [ - ifcfg.authentication.wpaPassword - ifcfg.authentication.wpaPasswordFile - ifcfg.authentication.wpaPskFile - ]; - in [ - { - assertion = ifcfg.authentication.mode == "wpa3-sae" -> ifcfg.managementFrameProtection == "2"; - message = ''hostapd interface ${interface} uses WPA3-SAE which requires managementFrameProtection="required"''; - } - { - assertion = ifcfg.authentication.mode == "wpa3-sae-transition" -> ifcfg.managementFrameProtection != "0"; - 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 = ifcfg.authentication.wpaPassword != null -> (stringLength ifcfg.authentication.wpaPassword >= 8 && stringLength ifcfg.authentication.wpaPassword <= 63); - message = ''hostapd interface ${interface} uses a wpaPassword of invalid length (must be in [8,63]).''; - } - { - assertion = ifcfg.authentication.saePasswords == [] || ifcfg.authentication.saePasswordsFile == null; - message = ''hostapd interface ${interface} must use only one SAE password option (saePasswords or saePasswordsFile)''; - } - { - assertion = ifcfg.authentication.mode == "wpa3-sae" -> (ifcfg.authentication.saePasswords != [] || ifcfg.authentication.saePasswordsFile != null); - message = ''hostapd interface ${interface} uses WPA3-SAE which requires defining a sae password option''; - } - { - assertion = ifcfg.authentication.mode == "wpa3-sae-transition" -> (ifcfg.authentication.saePasswords != [] || ifcfg.authentication.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''; - } - ]) - cfg.interfaces)); + # TODO check that multiple BSS -> bssid is defined or hardware bssids are used. + # TODO check that network name for each bss is prefixed with radio name + # TODO check that no bss name appears twice, even globally. + # TODO check that first bss has same name as radio. + # Radio warnings + ++ (concatLists (mapAttrsToList ( + radio: radioCfg: + [ + { + assertion = radioCfg.networks != {}; + message = "hostapd radio ${radio}: At least one network must be configured!"; + } + { + assertion = radioCfg.hwMode == "a" -> radioCfg.wifi5.enable; + message = ''hostapd radio ${radio}: Must set at least wifi5.enable=true in order to use hwMode="a"''; + } + ] + # BSS warnings + ++ (concatLists (mapAttrsToList (bss: bssCfg: let + auth = bssCfg.authentication; + countWpaPasswordDefinitions = count (x: x != null) [ + auth.wpaPassword + auth.wpaPasswordFile + auth.wpaPskFile + ]; + in [ + { + assertion = auth.mode == "wpa3-sae" -> bssCfg.managementFrameProtection == "2"; + message = ''hostapd radio ${radio} bss ${bss}: uses WPA3-SAE which requires managementFrameProtection="required"''; + } + { + assertion = auth.mode == "wpa3-sae-transition" -> bssCfg.managementFrameProtection != "0"; + message = ''hostapd radio ${radio} bss ${bss}: uses WPA3-SAE in transition mode with WPA2-SHA256, which requires managementFrameProtection="optional" or ="required"''; + } + { + assertion = countWpaPasswordDefinitions <= 1; + message = ''hostapd radio ${radio} bss ${bss}: must use at most one WPA password option (wpaPassword, wpaPasswordFile, wpaPskFile)''; + } + { + assertion = auth.wpaPassword != null -> (stringLength auth.wpaPassword >= 8 && stringLength auth.wpaPassword <= 63); + message = ''hostapd radio ${radio} bss ${bss}: uses a wpaPassword of invalid length (must be in [8,63]).''; + } + { + assertion = auth.saePasswords == [] || auth.saePasswordsFile == null; + message = ''hostapd radio ${radio} bss ${bss}: must use only one SAE password option (saePasswords or saePasswordsFile)''; + } + { + assertion = auth.mode == "wpa3-sae" -> (auth.saePasswords != [] || auth.saePasswordsFile != null); + message = ''hostapd radio ${radio} bss ${bss}: uses WPA3-SAE which requires defining a sae password option''; + } + { + assertion = auth.mode == "wpa3-sae-transition" -> (auth.saePasswords != [] || auth.saePasswordsFile != null) && countWpaPasswordDefinitions == 1; + message = ''hostapd radio ${radio} bss ${bss}: uses WPA3-SAE in transition mode requires defining both a wpa password option and a sae password option''; + } + { + assertion = auth.mode == "wpa2-sha256" -> countWpaPasswordDefinitions == 1; + message = ''hostapd radio ${radio} bss ${bss}: uses WPA2-SHA256 which requires defining a wpa password option''; + } + ]) + radioCfg.networks)) + ) + cfg.radios)); environment.systemPackages = [pkgs.hostapd]; @@ -968,12 +1113,12 @@ in { description = "IEEE 802.11 Host Access-Point Daemon"; path = [pkgs.hostapd]; - after = map (interface: "sys-subsystem-net-devices-${utils.escapeSystemdPath interface}.device") (attrNames cfg.interfaces); - bindsTo = map (interface: "sys-subsystem-net-devices-${utils.escapeSystemdPath interface}.device") (attrNames cfg.interfaces); + after = map (radio: "sys-subsystem-net-devices-${utils.escapeSystemdPath radio}.device") (attrNames cfg.radios); + bindsTo = map (radio: "sys-subsystem-net-devices-${utils.escapeSystemdPath radio}.device") (attrNames cfg.radios); wantedBy = ["multi-user.target"]; - # Create merged configuration and acl files for each interface prior to starting - preStart = concatStringsSep "\n" (mapAttrsToList makeInterfaceRuntimeFiles cfg.interfaces); + # Create merged configuration and acl files for each radio (and their bsses) prior to starting + preStart = concatStringsSep "\n" (mapAttrsToList makeRadioRuntimeFiles cfg.radios); serviceConfig = { ExecStart = "${pkgs.hostapd}/bin/hostapd ${concatStringsSep " " runtimeConfigFiles}";