2025-01-03 20:27:46 +01:00
{
config ,
lib ,
pkgs ,
utils ,
. . .
} :
2023-03-22 00:49:08 +01:00
# 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). 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
2025-01-03 20:27:46 +01:00
inherit ( lib )
2023-03-22 00:49:08 +01:00
attrNames
attrValues
concatLists
concatMapStrings
concatStringsSep
count
escapeShellArg
filter
generators
getAttr
hasPrefix
imap0
2025-01-03 20:28:08 +01:00
imap1
2023-03-22 00:49:08 +01:00
isInt
isString
length
literalExpression
maintainers
mapAttrsToList
mkDefault
mkEnableOption
mkIf
mkOption
mkPackageOption
mkRemovedOptionModule
optionalAttrs
optionalString
optionals
stringLength
toLower
types
unique
;
2012-10-05 21:39:56 -07:00
2023-03-22 00:49:08 +01:00
cfg = config . services . hostapd ;
2012-10-05 21:39:56 -07:00
2023-03-22 00:49:08 +01:00
extraSettingsFormat = {
2025-01-03 20:27:46 +01:00
type =
let
singleAtom = types . oneOf [
types . bool
types . int
types . str
] ;
atom = types . either singleAtom ( types . listOf singleAtom ) // {
description = " a t o m ( b o o l , i n t o r s t r i n g ) o r a l i s t o f t h e m f o r d u p l i c a t e k e y s " ;
} ;
in
types . attrsOf atom ;
generate =
name : value :
pkgs . writeText name (
generators . toKeyValue {
listsAsDuplicateKeys = true ;
mkKeyValue = generators . mkKeyValueDefault {
mkValueString =
v :
if isInt v then
toString v
else if isString v then
v
else if true = = v then
" 1 "
else if false = = v then
" 0 "
else
throw " u n s u p p o r t e d t y p e ${ builtins . typeOf v } : ${ ( generators . toPretty { } ) v } " ;
} " = " ;
} value
) ;
2023-03-22 00:49:08 +01:00
} ;
2012-10-05 21:39:56 -07:00
2023-03-22 00:49:08 +01:00
# Generates the header for a single BSS (i.e. WiFi network)
2025-01-03 20:27:46 +01:00
writeBssHeader =
radio : bss : bssIdx :
pkgs . writeText " h o s t a p d - r a d i o - ${ radio } - b s s - ${ bss } . c o n f " ''
'' \ n ''\n # B S S ${ toString bssIdx } : ${ bss }
2023-03-22 00:49:08 +01:00
################################
2012-10-05 21:39:56 -07:00
2025-01-03 20:27:46 +01:00
$ { if bssIdx = = 0 then " i n t e r f a c e " else " b s s " } = $ { bss }
'' ;
2012-10-05 21:39:56 -07:00
2025-01-03 20:27:46 +01:00
makeRadioRuntimeFiles =
radio : radioCfg :
pkgs . writeShellScript " m a k e - h o s t a p d - ${ radio } - f i l e s " (
''
set - euo pipefail
hostapd_config_file = /run/hostapd / $ { escapeShellArg radio } . hostapd . conf
rm - f " $ h o s t a p d _ c o n f i g _ f i l e "
cat > " $ h o s t a p d _ c o n f i g _ f i l e " < < EOF
# Radio base configuration: ${radio}
################################
EOF
cat $ { escapeShellArg ( extraSettingsFormat . generate " h o s t a p d - r a d i o - ${ radio } - e x t r a . c o n f " radioCfg . settings ) } > > " $ h o s t a p d _ c o n f i g _ f i l e "
$ { concatMapStrings ( script : " ${ script } \" $ h o s t a p d _ c o n f i g _ f i l e \" \n " ) (
attrValues radioCfg . dynamicConfigScripts
) }
''
+ concatMapStrings ( x : " ${ x } \n " ) (
imap0 ( i : f : f i ) (
mapAttrsToList ( bss : bssCfg : bssIdx : ''
'' \ n # B S S c o n f i g u r a t i o n : ${ bss }
mac_allow_file = /run/hostapd / $ { escapeShellArg bss } . mac . allow
rm - f " $ m a c _ a l l o w _ f i l e "
touch " $ m a c _ a l l o w _ f i l e "
mac_deny_file = /run/hostapd / $ { escapeShellArg bss } . mac . deny
rm - f " $ m a c _ d e n y _ f i l e "
touch " $ m a c _ d e n y _ f i l e "
cat $ { writeBssHeader radio bss bssIdx } > > " $ h o s t a p d _ c o n f i g _ f i l e "
cat $ { escapeShellArg ( extraSettingsFormat . generate " h o s t a p d - r a d i o - ${ radio } - b s s - ${ bss } - e x t r a . c o n f " bssCfg . settings ) } > > " $ h o s t a p d _ c o n f i g _ f i l e "
$ { concatMapStrings (
script : " ${ script } \" $ h o s t a p d _ c o n f i g _ f i l e \" \" $ m a c _ a l l o w _ f i l e \" \" $ m a c _ d e n y _ f i l e \" \n "
) ( attrValues bssCfg . dynamicConfigScripts ) }
'' ) r a d i o C f g . n e t w o r k s
)
)
) ;
2012-10-05 21:39:56 -07:00
2023-03-22 00:49:08 +01:00
runtimeConfigFiles = mapAttrsToList ( radio : _ : " / r u n / h o s t a p d / ${ radio } . h o s t a p d . c o n f " ) cfg . radios ;
2025-01-03 20:27:46 +01:00
in
{
2023-03-22 00:49:08 +01:00
meta . maintainers = with maintainers ; [ oddlama ] ;
2012-10-05 21:39:56 -07:00
2023-03-22 00:49:08 +01:00
options = {
2012-10-05 21:39:56 -07:00
services . hostapd = {
2023-03-22 00:49:08 +01:00
enable = mkEnableOption ''
2023-10-18 22:59:26 +02:00
hostapd , a user space daemon for access point and
2023-03-22 00:49:08 +01:00
authentication servers . It implements IEEE 802 .11 access point management ,
IEEE 802.1X/WPA/WPA2/EAP Authenticators , RADIUS client , EAP server , and RADIUS
2023-10-18 22:59:26 +02:00
authentication server
2023-03-22 00:49:08 +01:00
'' ;
2012-10-05 21:39:56 -07:00
2025-01-03 20:27:46 +01:00
package = mkPackageOption pkgs " h o s t a p d " { } ;
2012-10-05 21:39:56 -07:00
2023-03-22 00:49:08 +01:00
radios = mkOption {
2025-01-03 20:27:46 +01:00
default = { } ;
2023-03-22 00:49:08 +01:00
example = literalExpression ''
{
# Simple 2.4GHz AP
wlp2s0 = {
# countryCode = "US";
networks . wlp2s0 = {
ssid = " A P 1 " ;
2025-01-03 20:28:08 +01:00
authentication . saePasswords = [ { passwordFile = " / r u n / s e c r e t s / m y - p a s s w o r d " ; } ] ;
2023-03-22 00:49:08 +01:00
} ;
} ;
2012-10-05 21:39:56 -07:00
2023-03-22 00:49:08 +01:00
# WiFi 5 (5GHz) with two advertised networks
wlp3s0 = {
band = " 5 g " ;
channel = 0 ; # Enable automatic channel selection (ACS). Use only if your hardware supports it.
# countryCode = "US";
networks . wlp3s0 = {
ssid = " M y A P " ;
2025-01-03 20:28:08 +01:00
authentication . saePasswords = [ { passwordFile = " / r u n / s e c r e t s / m y - p a s s w o r d " ; } ] ;
2023-03-22 00:49:08 +01:00
} ;
networks . wlp3s0-1 = {
ssid = " O p e n A P w i t h W i F i 5 " ;
authentication . mode = " n o n e " ;
} ;
} ;
2019-06-23 21:33:14 +02:00
2023-03-22 00:49:08 +01:00
# Legacy WPA2 example
wlp4s0 = {
# countryCode = "US";
networks . wlp4s0 = {
ssid = " A P 2 " ;
authentication = {
mode = " w p a 2 - s h a 2 5 6 " ;
wpaPassword = " a f l a k e y p a s s w o r d " ; # Use wpaPasswordFile if possible.
} ;
} ;
} ;
}
2015-12-05 11:54:27 +01:00
'' ;
2023-03-22 00:49:08 +01:00
description = ''
This option allows you to define APs for one or multiple physical radios .
At least one radio must be specified .
2012-10-05 21:39:56 -07:00
2023-03-22 00:49:08 +01:00
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 of a single device , it may be required to create additional logical interfaces
for example by using { option } ` networking . wlanInterfaces ` .
2012-10-05 21:39:56 -07:00
2023-03-22 00:49:08 +01:00
Each physical radio can only support a single hardware-mode that is configured via
( { option } ` services . hostapd . radios . <radio> . band ` ) . 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 ` ) .
2015-12-05 11:54:27 +01:00
'' ;
2025-01-03 20:27:46 +01:00
type = types . attrsOf (
types . submodule ( radioSubmod : {
options = {
driver = mkOption {
default = " n l 8 0 2 1 1 " ;
example = " n o n e " ;
type = types . str ;
description = ''
The driver { command } ` hostapd ` will use .
{ var } ` nl80211 ` is used with all Linux mac80211 drivers .
{ var } ` none ` is used if building a standalone RADIUS server that does
not control any wireless/wired driver .
Most applications will probably use the default .
'' ;
2023-03-22 00:49:08 +01:00
} ;
2025-01-03 20:27:46 +01:00
noScan = mkOption {
2023-03-22 00:49:08 +01:00
type = types . bool ;
2025-01-03 20:27:46 +01:00
default = false ;
2023-03-22 00:49:08 +01:00
description = ''
2025-01-03 20:27:46 +01:00
Disables scan for overlapping BSSs in HT40+/- mode .
Caution : turning this on will likely violate regulatory requirements !
2023-03-22 00:49:08 +01:00
'' ;
} ;
2025-01-03 20:27:46 +01:00
countryCode = mkOption {
default = null ;
example = " U S " ;
type = types . nullOr types . str ;
2023-03-22 00:49:08 +01:00
description = ''
2025-01-03 20:27:46 +01:00
Country code ( ISO/IEC 3 1 6 6 -1 ) . Used to set regulatory domain .
Set as needed to indicate country in which device is operating .
This can limit available channels and transmit power .
These two octets are used as the first two octets of the Country String
( dot11CountryString ) .
2023-03-22 00:49:08 +01:00
2025-01-03 20:27:46 +01:00
Setting this will force you to also enable IEEE 802 . 1 1 d and IEEE 802 . 1 1 h .
2023-03-22 00:49:08 +01:00
2025-01-03 20:27:46 +01:00
IEEE 802 . 1 1 d : This advertises the countryCode and the set of allowed channels
and transmit power levels based on the regulatory limits .
2023-03-22 00:49:08 +01:00
2025-01-03 20:27:46 +01:00
IEEE802 . 1 1 h : This enables radar detection and DFS ( Dynamic Frequency Selection )
support if available . DFS support is required on outdoor 5 GHz channels in most
countries of the world .
'' ;
2023-03-22 00:49:08 +01:00
} ;
2025-01-03 20:27:46 +01:00
band = mkOption {
default = " 2 g " ;
type = types . enum [
" 2 g "
" 5 g "
" 6 g "
" 6 0 g "
] ;
2023-03-22 00:49:08 +01:00
description = ''
2025-01-03 20:27:46 +01:00
Specifies the frequency band to use , possible values are 2 g for 2 .4 GHz ,
5 g for 5 GHz , 6 g for 6 GHz and 6 0 g for 60 GHz .
2023-03-22 00:49:08 +01:00
'' ;
} ;
2025-01-03 20:27:46 +01:00
channel = mkOption {
default = 0 ;
example = 11 ;
type = types . int ;
description = ''
The channel to operate on . Use 0 to enable ACS ( Automatic Channel Selection ) .
Beware that not every device supports ACS in which case { command } ` hostapd `
will fail to start .
'' ;
2023-03-22 00:49:08 +01:00
} ;
2025-01-03 20:27:46 +01:00
settings = mkOption {
default = { } ;
example = {
acs_exclude_dfs = true ;
} ;
type = types . submodule {
freeformType = extraSettingsFormat . type ;
} ;
2023-03-22 00:49:08 +01:00
description = ''
2025-01-03 20:27:46 +01:00
Extra configuration options to put at the end of global initialization , before defining BSSs .
To find out which options are global and which are per-bss you have to read hostapd's source code ,
which is non-trivial and not documented otherwise .
2023-03-22 00:49:08 +01:00
2025-01-03 20:27:46 +01:00
Lists will be converted to multiple definitions of the same key , and booleans to 0/1.
Otherwise , the inputs are not modified or checked for correctness .
2023-03-22 00:49:08 +01:00
'' ;
} ;
2025-01-03 20:27:46 +01:00
dynamicConfigScripts = mkOption {
default = { } ;
type = types . attrsOf types . path ;
example = literalExpression ''
{
exampleDynamicConfig = pkgs . writeShellScript " d y n a m i c - c o n f i g " '' '
HOSTAPD_CONFIG = $ 1
cat > > " $ H O S T A P D _ C O N F I G " < < EOF
# Add some dynamically generated statements here,
# for example based on the physical adapter in use
EOF
'' ' ;
}
'' ;
description = ''
All of these scripts will be executed in lexicographical order before hostapd
is started , right after the global segment was generated and may dynamically
append global options the generated configuration file .
2023-03-22 00:49:08 +01:00
2025-01-03 20:27:46 +01:00
The first argument will point to the configuration file that you may append to .
'' ;
2023-03-22 00:49:08 +01:00
} ;
2025-01-03 20:27:46 +01:00
#### IEEE 802.11n (WiFi 4) related configuration
wifi4 = {
enable = mkOption {
default = true ;
type = types . bool ;
description = ''
Enables support for IEEE 802 . 1 1 n ( WiFi 4 , HT ) .
This is enabled by default , since the vase majority of devices
are expected to support this .
'' ;
} ;
2023-03-22 00:49:08 +01:00
2025-01-03 20:27:46 +01:00
capabilities = mkOption {
type = types . listOf types . str ;
default = [
" H T 4 0 "
" S H O R T - G I - 2 0 "
" S H O R T - G I - 4 0 "
] ;
example = [
" L D P C "
" H T 4 0 + "
" H T 4 0 - "
" G F "
" S H O R T - G I - 2 0 "
" S H O R T - G I - 4 0 "
" T X - S T B C "
" R X - S T B C 1 "
] ;
description = ''
HT ( High Throughput ) capabilities given as a list of flags .
Please refer to the hostapd documentation for allowed values and
only set values supported by your physical adapter .
The default contains common values supported by most adapters .
'' ;
} ;
2023-03-22 00:49:08 +01:00
2025-01-03 20:27:46 +01:00
require = mkOption {
default = false ;
type = types . bool ;
description = " R e q u i r e s t a t i o n s ( c l i e n t s ) t o s u p p o r t W i F i 4 ( H T ) a n d d i s a s s o c i a t e t h e m i f t h e y d o n ' t . " ;
} ;
2023-03-22 00:49:08 +01:00
} ;
2025-01-03 20:27:46 +01:00
#### IEEE 802.11ac (WiFi 5) related configuration
2023-03-22 00:49:08 +01:00
2025-01-03 20:27:46 +01:00
wifi5 = {
enable = mkOption {
default = true ;
type = types . bool ;
description = " E n a b l e s s u p p o r t f o r I E E E 8 0 2 . 1 1 a c ( W i F i 5 , V H T ) " ;
} ;
2023-03-22 00:49:08 +01:00
2025-01-03 20:27:46 +01:00
capabilities = mkOption {
type = types . listOf types . str ;
default = [ ] ;
example = [
" S H O R T - G I - 8 0 "
" T X - S T B C - 2 B Y 1 "
" R X - S T B C - 1 "
" R X - A N T E N N A - P A T T E R N "
" T X - A N T E N N A - P A T T E R N "
] ;
description = ''
VHT ( Very High Throughput ) capabilities given as a list of flags .
Please refer to the hostapd documentation for allowed values and
only set values supported by your physical adapter .
'' ;
} ;
2023-03-22 00:49:08 +01:00
2025-01-03 20:27:46 +01:00
require = mkOption {
default = false ;
type = types . bool ;
description = " R e q u i r e s t a t i o n s ( c l i e n t s ) t o s u p p o r t W i F i 5 ( V H T ) a n d d i s a s s o c i a t e t h e m i f t h e y d o n ' t . " ;
} ;
2023-03-22 00:49:08 +01:00
2025-01-03 20:27:46 +01:00
operatingChannelWidth = mkOption {
default = " 2 0 o r 4 0 " ;
type = types . enum [
" 2 0 o r 4 0 "
" 8 0 "
" 1 6 0 "
" 8 0 + 8 0 "
] ;
apply =
x :
getAttr x {
" 2 0 o r 4 0 " = 0 ;
" 8 0 " = 1 ;
" 1 6 0 " = 2 ;
" 8 0 + 8 0 " = 3 ;
} ;
description = ''
Determines the operating channel width for VHT .
- { var } ` " 2 0 o r 4 0 " ` : 20 or 40 MHz operating channel width
- { var } ` " 8 0 " ` : 80 MHz channel width
- { var } ` " 1 6 0 " ` : 160 MHz channel width
- { var } ` " 8 0 + 8 0 " ` : 80 + 80 MHz channel width
'' ;
} ;
2023-03-22 00:49:08 +01:00
} ;
2025-01-03 20:27:46 +01:00
#### IEEE 802.11ax (WiFi 6) related configuration
2023-03-22 00:49:08 +01:00
2025-01-03 20:27:46 +01:00
wifi6 = {
enable = mkOption {
default = false ;
type = types . bool ;
description = " E n a b l e s s u p p o r t f o r I E E E 8 0 2 . 1 1 a x ( W i F i 6 , H E ) " ;
} ;
require = mkOption {
default = false ;
type = types . bool ;
description = " R e q u i r e s t a t i o n s ( c l i e n t s ) t o s u p p o r t W i F i 6 ( H E ) a n d d i s a s s o c i a t e t h e m i f t h e y d o n ' t . " ;
} ;
singleUserBeamformer = mkOption {
default = false ;
type = types . bool ;
description = " H E s i n g l e u s e r b e a m f o r m e r s u p p o r t " ;
} ;
singleUserBeamformee = mkOption {
default = false ;
type = types . bool ;
description = " H E s i n g l e u s e r b e a m f o r m e e s u p p o r t " ;
} ;
multiUserBeamformer = mkOption {
default = false ;
type = types . bool ;
description = " H E m u l t i u s e r b e a m f o r m e e s u p p o r t " ;
} ;
operatingChannelWidth = mkOption {
default = " 2 0 o r 4 0 " ;
type = types . enum [
" 2 0 o r 4 0 "
" 8 0 "
" 1 6 0 "
" 8 0 + 8 0 "
] ;
apply =
x :
getAttr x {
" 2 0 o r 4 0 " = 0 ;
" 8 0 " = 1 ;
" 1 6 0 " = 2 ;
" 8 0 + 8 0 " = 3 ;
} ;
description = ''
Determines the operating channel width for HE .
- { var } ` " 2 0 o r 4 0 " ` : 20 or 40 MHz operating channel width
- { var } ` " 8 0 " ` : 80 MHz channel width
- { var } ` " 1 6 0 " ` : 160 MHz channel width
- { var } ` " 8 0 + 8 0 " ` : 80 + 80 MHz channel width
'' ;
} ;
2023-03-22 00:49:08 +01:00
} ;
2025-01-03 20:27:46 +01:00
#### IEEE 802.11be (WiFi 7) related configuration
wifi7 = {
enable = mkOption {
default = false ;
type = types . bool ;
description = ''
Enables support for IEEE 802 . 1 1 be ( WiFi 7 , EHT ) . This is currently experimental
and requires you to manually enable CONFIG_IEEE80211BE when building hostapd .
'' ;
} ;
singleUserBeamformer = mkOption {
default = false ;
type = types . bool ;
description = " E H T s i n g l e u s e r b e a m f o r m e r s u p p o r t " ;
} ;
singleUserBeamformee = mkOption {
default = false ;
type = types . bool ;
description = " E H T s i n g l e u s e r b e a m f o r m e e s u p p o r t " ;
} ;
multiUserBeamformer = mkOption {
default = false ;
type = types . bool ;
description = " E H T m u l t i u s e r b e a m f o r m e e s u p p o r t " ;
} ;
operatingChannelWidth = mkOption {
default = " 2 0 o r 4 0 " ;
type = types . enum [
" 2 0 o r 4 0 "
" 8 0 "
" 1 6 0 "
" 8 0 + 8 0 "
] ;
apply =
x :
getAttr x {
" 2 0 o r 4 0 " = 0 ;
" 8 0 " = 1 ;
" 1 6 0 " = 2 ;
" 8 0 + 8 0 " = 3 ;
} ;
description = ''
Determines the operating channel width for EHT .
- { var } ` " 2 0 o r 4 0 " ` : 20 or 40 MHz operating channel width
- { var } ` " 8 0 " ` : 80 MHz channel width
- { var } ` " 1 6 0 " ` : 160 MHz channel width
- { var } ` " 8 0 + 8 0 " ` : 80 + 80 MHz channel width
'' ;
} ;
2023-03-22 00:49:08 +01:00
} ;
2025-01-03 20:27:46 +01:00
#### BSS definitions
2023-03-22 00:49:08 +01:00
2025-01-03 20:27:46 +01:00
networks = mkOption {
default = { } ;
example = literalExpression ''
{
wlp2s0 = {
ssid = " P r i m a r y a d v e r t i s e d n e t w o r k " ;
2025-01-03 20:28:08 +01:00
authentication . saePasswords = [ { passwordFile = " / r u n / s e c r e t s / m y - p a s s w o r d " ; } ] ;
2025-01-03 20:27:46 +01:00
} ;
wlp2s0-1 = {
ssid = " S e c o n d a r y a d v e r t i s e d n e t w o r k ( O p e n ) " ;
authentication . mode = " n o n e " ;
} ;
}
2023-03-22 00:49:08 +01:00
'' ;
2025-01-03 20:27:46 +01:00
description = ''
This defines a BSS , colloquially known as a WiFi network .
You have to specify at least one .
'' ;
type = types . attrsOf (
types . submodule ( bssSubmod : {
options = {
logLevel = mkOption {
default = 2 ;
type = types . int ;
description = ''
Levels ( minimum value for logged events ) :
0 = verbose debugging
1 = debugging
2 = informational messages
3 = notification
4 = warning
'' ;
} ;
2023-03-22 00:49:08 +01:00
2025-01-03 20:27:46 +01:00
group = mkOption {
default = " w h e e l " ;
example = " n e t w o r k " ;
type = types . str ;
description = ''
Members of this group can access the control socket for this interface .
'' ;
} ;
2023-03-22 00:49:08 +01:00
2025-01-03 20:27:46 +01:00
utf8Ssid = mkOption {
default = true ;
type = types . bool ;
description = " W h e t h e r t h e S S I D i s t o b e i n t e r p r e t e d u s i n g U T F - 8 e n c o d i n g . " ;
2023-03-22 00:49:08 +01:00
} ;
2025-01-03 20:27:46 +01:00
ssid = mkOption {
example = " ❄ ️ c o o l ❄ ️ " ;
type = types . str ;
description = " S S I D t o b e u s e d i n I E E E 8 0 2 . 1 1 m a n a g e m e n t f r a m e s . " ;
2023-03-22 00:49:08 +01:00
} ;
2025-01-03 20:27:46 +01:00
bssid = mkOption {
type = types . nullOr types . str ;
default = null ;
example = " 1 1 : 2 2 : 3 3 : 4 4 : 5 5 : 6 6 " ;
description = ''
Specifies the BSSID for this BSS . Usually determined automatically ,
but for now you have to manually specify them when using multiple BSS .
Try assigning related addresses from the locally administered MAC address ranges ,
by reusing the hardware address but replacing the second nibble with 2 , 6 , A or E .
( e . g . if real address is ` XX:XX:XX:XX:XX ` , try ` X2:XX:XX:XX:XX:XX ` , ` X6:XX:XX:XX:XX:XX ` , . . .
for the second , third , . . . BSS )
'' ;
} ;
2023-03-22 00:49:08 +01:00
2025-01-03 20:27:46 +01:00
macAcl = mkOption {
default = " d e n y " ;
type = types . enum [
" d e n y "
" a l l o w "
" r a d i u s "
] ;
apply =
x :
getAttr x {
" d e n y " = 0 ;
" a l l o w " = 1 ;
" r a d i u s " = 2 ;
} ;
description = ''
Station MAC address - based authentication . The following modes are available :
2023-03-22 00:49:08 +01:00
2025-01-03 20:27:46 +01:00
- { var } ` " d e n y " ` : Allow unless listed in { option } ` macDeny ` ( default )
- { var } ` " a l l o w " ` : Deny unless listed in { option } ` macAllow `
- { var } ` " r a d i u s " ` : Use external radius server , but check both { option } ` macAllow ` and { option } ` macDeny ` first
2023-03-22 00:49:08 +01:00
2025-01-03 20:27:46 +01:00
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 .
'' ;
} ;
2023-03-22 00:49:08 +01:00
2025-01-03 20:27:46 +01:00
macAllow = mkOption {
type = types . listOf types . str ;
default = [ ] ;
example = [ " 1 1 : 2 2 : 3 3 : 4 4 : 5 5 : 6 6 " ] ;
description = ''
Specifies the MAC addresses to allow if { option } ` macAcl ` is set to { var } ` " a l l o w " ` or { var } ` " r a d i u s " ` .
These values will be world-readable in the Nix store . Values will automatically be merged with
{ option } ` macAllowFile ` if necessary .
'' ;
} ;
2023-03-22 00:49:08 +01:00
2025-01-03 20:27:46 +01:00
macAllowFile = mkOption {
type = types . nullOr types . path ;
default = null ;
description = ''
Specifies a file containing the MAC addresses to allow if { option } ` macAcl ` is set to { var } ` " a l l o w " ` or { var } ` " r a d i u s " ` .
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 .
'' ;
} ;
2023-03-22 00:49:08 +01:00
2025-01-03 20:27:46 +01:00
macDeny = mkOption {
type = types . listOf types . str ;
default = [ ] ;
example = [ " 1 1 : 2 2 : 3 3 : 4 4 : 5 5 : 6 6 " ] ;
description = ''
Specifies the MAC addresses to deny if { option } ` macAcl ` is set to { var } ` " d e n y " ` or { var } ` " r a d i u s " ` .
These values will be world-readable in the Nix store . Values will automatically be merged with
{ option } ` macDenyFile ` if necessary .
'' ;
} ;
2023-03-22 00:49:08 +01:00
2025-01-03 20:27:46 +01:00
macDenyFile = mkOption {
type = types . nullOr types . path ;
default = null ;
description = ''
Specifies a file containing the MAC addresses to deny if { option } ` macAcl ` is set to { var } ` " d e n y " ` or { var } ` " r a d i u s " ` .
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 .
'' ;
} ;
2023-03-22 00:49:08 +01:00
2025-01-03 20:27:46 +01:00
ignoreBroadcastSsid = mkOption {
default = " d i s a b l e d " ;
type = types . enum [
" d i s a b l e d "
" e m p t y "
" c l e a r "
] ;
apply =
x :
getAttr x {
" d i s a b l e d " = 0 ;
" e m p t y " = 1 ;
" c l e a r " = 2 ;
2023-03-22 00:49:08 +01:00
} ;
2025-01-03 20:27:46 +01:00
description = ''
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 } ` " d i s a b l e d " ` : Advertise ssid normally .
- { var } ` " e m p t y " ` : send empty ( length = 0 ) SSID in beacon and ignore probe request for broadcast SSID
- { var } ` " c l e a r " ` : 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 .
'' ;
} ;
2023-03-22 00:49:08 +01:00
2025-01-03 20:27:46 +01:00
apIsolate = mkOption {
default = false ;
type = types . bool ;
description = ''
Isolate traffic between stations ( clients ) and prevent them from
communicating with each other .
'' ;
} ;
2023-03-22 00:49:08 +01:00
2025-01-03 20:27:46 +01:00
settings = mkOption {
default = { } ;
example = {
multi_ap = true ;
2023-03-22 00:49:08 +01:00
} ;
2025-01-03 20:27:46 +01:00
type = types . submodule {
freeformType = extraSettingsFormat . type ;
} ;
description = ''
Extra configuration options to put at the end of this BSS's defintion in the
hostapd . conf for the associated interface . To find out which options are global
and which are per-bss you have to read hostapd's source code , which is non-trivial
and not documented otherwise .
Lists will be converted to multiple definitions of the same key , and booleans to 0/1.
Otherwise , the inputs are not modified or checked for correctness .
'' ;
} ;
2023-03-22 00:49:08 +01:00
2025-01-03 20:27:46 +01:00
dynamicConfigScripts = mkOption {
default = { } ;
type = types . attrsOf types . path ;
example = literalExpression ''
{
exampleDynamicConfig = pkgs . writeShellScript " d y n a m i c - c o n f i g " '' '
HOSTAPD_CONFIG = $ 1
# These always exist, but may or may not be used depending on the actual configuration
MAC_ALLOW_FILE = $ 2
MAC_DENY_FILE = $ 3
cat > > " $ H O S T A P D _ C O N F I G " < < EOF
# Add some dynamically generated statements here
EOF
'' ' ;
}
'' ;
description = ''
All of these scripts will be executed in lexicographical order before hostapd
is started , right after the bss segment was generated and may dynamically
append bss options to the generated configuration file .
The first argument will point to the configuration file that you may append to .
The second and third argument will point to this BSS's MAC allow and MAC deny file respectively .
'' ;
} ;
2023-03-22 00:49:08 +01:00
2025-01-03 20:27:46 +01:00
#### IEEE 802.11i (WPA) configuration
authentication = {
mode = mkOption {
default = " w p a 3 - s a e " ;
type = types . enum [
" n o n e "
" w p a 2 - s h a 1 "
" w p a 2 - s h a 2 5 6 "
" w p a 3 - s a e - t r a n s i t i o n "
" w p a 3 - s a e "
] ;
description = ''
Selects the authentication mode for this AP .
- { var } ` " n o n e " ` : Don't configure any authentication . This will disable wpa alltogether
and create an open AP . Use { option } ` settings ` together with this option if you
want to configure the authentication manually . Any password options will still be
effective , if set .
- { var } ` " w p a 2 - s h a 1 " ` : Not recommended . WPA2-Personal using HMAC-SHA1 . Passwords are set
using { option } ` wpaPassword ` or preferably by { option } ` wpaPasswordFile ` or { option } ` wpaPskFile ` .
- { var } ` " w p a 2 - s h a 2 5 6 " ` : WPA2-Personal using HMAC-SHA256 ( IEEE 802.11i/RSN ) . Passwords are set
using { option } ` wpaPassword ` or preferably by { option } ` wpaPasswordFile ` or { option } ` wpaPskFile ` .
- { var } ` " w p a 3 - s a e - t r a n s i t i o n " ` : 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 } ` " w p a 3 - s a e " ` : 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
2025-01-03 20:28:08 +01:00
using either { option } ` saePasswords ` or { option } ` saePasswordsFile ` .
2025-01-03 20:27:46 +01:00
'' ;
} ;
pairwiseCiphers = mkOption {
default = [ " C C M P " ] ;
example = [
" G C M P "
" G C M P - 2 5 6 "
] ;
type = types . listOf types . str ;
description = ''
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 and GCMP-256 is often only available with devices supporting WiFi 5 ( IEEE 802 . 1 1 ac ) or higher .
CCMP-256 support is rare .
'' ;
} ;
enableRecommendedPairwiseCiphers = mkOption {
default = false ;
example = true ;
type = types . bool ;
description = ''
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 f l a k e y p a s s w o r d " ;
type = types . nullOr types . str ;
description = ''
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 [ 0 x20 , 0 x7e ]
( IEEE Std . 802 . 1 1 i-2004 , Annex H .4 .1 ) . Use emojis at your own risk .
Not used when { option } ` mode ` is { var } ` " w p a 3 - s a e " ` .
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 . nullOr types . path ;
description = ''
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 } ` " w p a 3 - s a e " ` .
'' ;
} ;
wpaPskFile = mkOption {
default = null ;
type = types . nullOr types . path ;
description = ''
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 6 4 - 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 = <keyid_string> `
An optional VLAN ID can be specified by prefixing the line with ` vlanid = < VLAN ID > ` .
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 } ` " w p a 3 - s a e " ` .
'' ;
} ;
saePasswords = mkOption {
default = [ ] ;
example = literalExpression ''
[
# Any client may use these passwords
{ password = " W i - F i g u r e i t o u t " ; }
2025-01-03 20:28:08 +01:00
{ passwordFile = " / r u n / s e c r e t s / m y - p a s s w o r d - f i l e " ; mac = " f f : f f : f f : f f : f f : f f " ; }
2025-01-03 20:27:46 +01:00
# Only the client with MAC-address 11:22:33:44:55:66 can use this password
{ password = " s e k r e t p a z z w o r d " ; mac = " 1 1 : 2 2 : 3 3 : 4 4 : 5 5 : 6 6 " ; }
]
'' ;
description = ''
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 } ` " w p a 2 - s h a 1 " ` or { var } ` " w p a 2 - s h a 2 5 6 " ` .
'' ;
type = types . listOf (
types . submodule {
options = {
password = mkOption {
2025-01-03 20:28:08 +01:00
default = null ;
2025-01-03 20:27:46 +01:00
example = " a f l a k e y p a s s w o r d " ;
2025-01-03 20:28:08 +01:00
type = types . nullOr types . str ;
2025-01-03 20:27:46 +01:00
description = ''
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
2025-01-03 20:28:08 +01:00
the Nix store ! Prefer using the sibling option { option } ` passwordFile ` or directly set { option } ` saePasswordsFile ` .
'' ;
} ;
passwordFile = mkOption {
default = null ;
type = types . nullOr types . path ;
description = ''
The password for this entry , read from the given file when starting hostapd .
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 .
2025-01-03 20:27:46 +01:00
'' ;
} ;
mac = mkOption {
default = null ;
example = " 1 1 : 2 2 : 3 3 : 4 4 : 5 5 : 6 6 " ;
type = types . nullOr types . str ;
description = ''
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 . nullOr types . int ;
description = " I f t h i s a t t r i b u t e i s g i v e n , a l l c l i e n t s u s i n g t h i s e n t r y w i l l g e t t a g g e d w i t h t h e g i v e n V L A N I D . " ;
} ;
pk = mkOption {
default = null ;
example = " " ;
type = types . nullOr types . str ;
description = ''
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 . nullOr types . str ;
description = ''
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 . nullOr types . path ;
description = ''
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 :
` <password> [ | mac = < peer mac > ] [ | vlanid = < VLAN ID > ] [ | pk = < m:ECPrivateKey-base64 > ] [ | id = <identifier> ] `
Not used when { option } ` mode ` is { var } ` " w p a 2 - s h a 1 " ` or { var } ` " w p a 2 - s h a 2 5 6 " ` .
'' ;
} ;
saeAddToMacAllow = mkOption {
type = types . bool ;
default = false ;
description = ''
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 ` .
'' ;
} ;
} ;
2023-03-22 00:49:08 +01:00
} ;
2025-01-03 20:27:46 +01:00
config =
let
bssCfg = bssSubmod . config ;
pairwiseCiphers = concatStringsSep " " (
unique (
bssCfg . authentication . pairwiseCiphers
++ optionals bssCfg . authentication . enableRecommendedPairwiseCiphers [
" C C M P "
" G C M P "
" G C M P - 2 5 6 "
]
)
) ;
in
{
settings =
{
ssid = bssCfg . ssid ;
utf8_ssid = bssCfg . utf8Ssid ;
logger_syslog = mkDefault ( -1 ) ;
logger_syslog_level = bssCfg . logLevel ;
logger_stdout = mkDefault ( -1 ) ;
logger_stdout_level = bssCfg . logLevel ;
ctrl_interface = mkDefault " / r u n / h o s t a p d " ;
ctrl_interface_group = bssCfg . group ;
macaddr_acl = bssCfg . macAcl ;
ignore_broadcast_ssid = bssCfg . ignoreBroadcastSsid ;
# IEEE 802.11i (authentication) related configuration
# Encrypt management frames to protect against deauthentication and similar attacks
ieee80211w = mkDefault 1 ;
sae_require_mfp = mkDefault 1 ;
# Only allow WPA by default and disable insecure WEP
auth_algs = mkDefault 1 ;
# Always enable QoS, which is required for 802.11n and above
wmm_enabled = mkDefault true ;
ap_isolate = bssCfg . apIsolate ;
}
// optionalAttrs ( bssCfg . bssid != null ) {
bssid = bssCfg . bssid ;
}
//
optionalAttrs
( bssCfg . macAllow != [ ] || bssCfg . macAllowFile != null || bssCfg . authentication . saeAddToMacAllow )
{
accept_mac_file = " / r u n / h o s t a p d / ${ bssCfg . _module . args . name } . m a c . a l l o w " ;
}
// optionalAttrs ( bssCfg . macDeny != [ ] || bssCfg . macDenyFile != null ) {
deny_mac_file = " / r u n / h o s t a p d / ${ bssCfg . _module . args . name } . m a c . d e n y " ;
}
// optionalAttrs ( bssCfg . authentication . mode = = " n o n e " ) {
wpa = mkDefault 0 ;
}
// optionalAttrs ( bssCfg . authentication . mode = = " w p a 3 - s a e " ) {
wpa = 2 ;
wpa_key_mgmt = " S A E " ;
# Derive PWE using both hunting-and-pecking loop and hash-to-element
sae_pwe = 2 ;
# Prevent downgrade attacks by indicating to clients that they should
# disable any transition modes from now on.
transition_disable = " 0 x 0 1 " ;
}
// optionalAttrs ( bssCfg . authentication . mode = = " w p a 3 - s a e - t r a n s i t i o n " ) {
wpa = 2 ;
wpa_key_mgmt = " W P A - P S K - S H A 2 5 6 S A E " ;
}
// optionalAttrs ( bssCfg . authentication . mode = = " w p a 2 - s h a 1 " ) {
wpa = 2 ;
wpa_key_mgmt = " W P A - P S K " ;
}
// optionalAttrs ( bssCfg . authentication . mode = = " w p a 2 - s h a 2 5 6 " ) {
wpa = 2 ;
wpa_key_mgmt = " W P A - P S K - S H A 2 5 6 " ;
}
// optionalAttrs ( bssCfg . authentication . mode != " n o n e " ) {
wpa_pairwise = pairwiseCiphers ;
rsn_pairwise = pairwiseCiphers ;
}
// optionalAttrs ( bssCfg . authentication . wpaPassword != null ) {
wpa_passphrase = bssCfg . authentication . wpaPassword ;
}
// optionalAttrs ( bssCfg . authentication . wpaPskFile != null ) {
wpa_psk_file = toString bssCfg . authentication . wpaPskFile ;
} ;
dynamicConfigScripts =
let
# All MAC addresses from SAE entries that aren't the wildcard address
saeMacs = filter ( mac : mac != null && ( toLower mac ) != " f f : f f : f f : f f : f f : f f " ) (
map ( x : x . mac ) bssCfg . authentication . saePasswords
) ;
in
{
" 2 0 - a d d M a c A l l o w " = mkIf ( bssCfg . macAllow != [ ] ) (
pkgs . writeShellScript " a d d - m a c - a l l o w " ''
MAC_ALLOW_FILE = $ 2
cat > > " $ M A C _ A L L O W _ F I L E " < < EOF
$ { concatStringsSep " \n " bssCfg . macAllow }
EOF
''
) ;
" 2 0 - a d d M a c A l l o w F i l e " = mkIf ( bssCfg . macAllowFile != null ) (
pkgs . writeShellScript " a d d - m a c - a l l o w - f i l e " ''
MAC_ALLOW_FILE = $ 2
grep - Eo ' ^ ( [ 0 - 9 A-Fa-f ] { 2 } [ : ] ) { 5 } ( [ 0 - 9 A-Fa-f ] { 2 } ) ' $ { escapeShellArg bssCfg . macAllowFile } > > " $ M A C _ A L L O W _ F I L E "
''
) ;
" 2 0 - a d d M a c A l l o w F r o m S a e " = mkIf ( bssCfg . authentication . saeAddToMacAllow && saeMacs != [ ] ) (
pkgs . writeShellScript " a d d - m a c - a l l o w - f r o m - s a e " ''
MAC_ALLOW_FILE = $ 2
cat > > " $ M A C _ A L L O W _ F I L E " < < EOF
$ { concatStringsSep " \n " saeMacs }
EOF
''
) ;
# Populate mac allow list from saePasswordsFile
# (filter for lines with mac=; exclude commented lines; filter for real mac-addresses; strip mac=)
" 2 0 - a d d M a c A l l o w F r o m S a e F i l e " =
mkIf ( bssCfg . authentication . saeAddToMacAllow && bssCfg . authentication . saePasswordsFile != null )
(
pkgs . writeShellScript " a d d - m a c - a l l o w - f r o m - s a e - f i l e " ''
MAC_ALLOW_FILE = $ 2
grep mac = $ { escapeShellArg bssCfg . authentication . saePasswordsFile } \
| grep - v ' ^ \ s * #' \
| grep - Eo ' mac = ( [ 0 - 9 A-Fa-f ] { 2 } [ : ] ) { 5 } ( [ 0 - 9 A-Fa-f ] { 2 } ) ' \
| sed ' s | ^ mac = || ' > > " $ M A C _ A L L O W _ F I L E "
''
) ;
" 2 0 - a d d M a c D e n y " = mkIf ( bssCfg . macDeny != [ ] ) (
pkgs . writeShellScript " a d d - m a c - d e n y " ''
MAC_DENY_FILE = $ 3
cat > > " $ M A C _ D E N Y _ F I L E " < < EOF
$ { concatStringsSep " \n " bssCfg . macDeny }
EOF
''
) ;
" 2 0 - a d d M a c D e n y F i l e " = mkIf ( bssCfg . macDenyFile != null ) (
pkgs . writeShellScript " a d d - m a c - d e n y - f i l e " ''
MAC_DENY_FILE = $ 3
grep - Eo ' ^ ( [ 0 - 9 A-Fa-f ] { 2 } [ : ] ) { 5 } ( [ 0 - 9 A-Fa-f ] { 2 } ) ' $ { escapeShellArg bssCfg . macDenyFile } > > " $ M A C _ D E N Y _ F I L E "
''
) ;
# Add wpa_passphrase from file
" 2 0 - w p a P a s s w o r d F i l e " = mkIf ( bssCfg . authentication . wpaPasswordFile != null ) (
pkgs . writeShellScript " w p a - p a s s w o r d - f i l e " ''
HOSTAPD_CONFIG_FILE = $ 1
cat > > " $ H O S T A P D _ C O N F I G _ F I L E " < < EOF
wpa_passphrase = $ ( cat $ { escapeShellArg bssCfg . authentication . wpaPasswordFile } )
EOF
''
) ;
# Add sae passwords from file
" 2 0 - s a e P a s s w o r d s F i l e " = mkIf ( bssCfg . authentication . saePasswordsFile != null ) (
pkgs . writeShellScript " s a e - p a s s w o r d s - f i l e " ''
HOSTAPD_CONFIG_FILE = $ 1
grep - v ' ^ \ s * #' ${escapeShellArg bssCfg.authentication.saePasswordsFile} \
| sed ' s / ^ /sae_password = / ' > > " $ H O S T A P D _ C O N F I G _ F I L E "
''
) ;
2025-01-03 20:28:08 +01:00
# Add sae passwords from nix definitions, potentially reading secrets
" 2 0 - s a e P a s s w o r d s " = mkIf ( bssCfg . authentication . saePasswords != [ ] ) (
pkgs . writeShellScript " s a e - p a s s w o r d s " (
''
HOSTAPD_CONFIG_FILE = $ 1
''
+ concatMapStrings (
entry :
let
lineSuffix =
optionalString ( entry . password != null ) entry . password
+ optionalString ( entry . mac != null ) " | m a c = ${ entry . mac } "
+ optionalString ( entry . vlanid != null ) " | v l a n i d = ${ toString entry . vlanid } "
+ optionalString ( entry . pk != null ) " | p k = ${ entry . pk } "
+ optionalString ( entry . id != null ) " | i d = ${ entry . id } " ;
in
''
(
echo - n ' sae_password = '
$ { optionalString ( entry . passwordFile != null ) '' t r - d ' \ n ' < ${ entry . passwordFile } '' }
echo $ { escapeShellArg lineSuffix }
) > > " $ H O S T A P D _ C O N F I G _ F I L E "
''
) bssCfg . authentication . saePasswords
)
) ;
2025-01-03 20:27:46 +01:00
} ;
} ;
} )
) ;
} ;
2023-03-22 00:49:08 +01:00
} ;
2025-01-03 20:27:46 +01:00
config . settings =
let
radioCfg = radioSubmod . config ;
in
{
driver = radioCfg . driver ;
hw_mode =
{
" 2 g " = " g " ;
" 5 g " = " a " ;
" 6 g " = " a " ;
" 6 0 g " = " a d " ;
}
. ${ radioCfg . band } ;
channel = radioCfg . channel ;
noscan = radioCfg . noScan ;
}
// optionalAttrs ( radioCfg . countryCode != null ) {
country_code = radioCfg . countryCode ;
# IEEE 802.11d: Limit to frequencies allowed in country
ieee80211d = true ;
# IEEE 802.11h: Enable radar detection and DFS (Dynamic Frequency Selection)
ieee80211h = true ;
}
// optionalAttrs radioCfg . wifi4 . enable {
# IEEE 802.11n (WiFi 4) related configuration
ieee80211n = true ;
require_ht = radioCfg . wifi4 . require ;
ht_capab = concatMapStrings ( x : " [ ${ x } ] " ) radioCfg . wifi4 . capabilities ;
}
// optionalAttrs radioCfg . wifi5 . enable {
# IEEE 802.11ac (WiFi 5) related configuration
ieee80211ac = true ;
require_vht = radioCfg . wifi5 . require ;
vht_oper_chwidth = radioCfg . wifi5 . operatingChannelWidth ;
vht_capab = concatMapStrings ( x : " [ ${ x } ] " ) radioCfg . wifi5 . capabilities ;
}
// optionalAttrs radioCfg . wifi6 . enable {
# IEEE 802.11ax (WiFi 6) related configuration
ieee80211ax = true ;
require_he = mkIf radioCfg . wifi6 . require true ;
he_oper_chwidth = radioCfg . wifi6 . operatingChannelWidth ;
he_su_beamformer = radioCfg . wifi6 . singleUserBeamformer ;
he_su_beamformee = radioCfg . wifi6 . singleUserBeamformee ;
he_mu_beamformer = radioCfg . wifi6 . multiUserBeamformer ;
}
// optionalAttrs radioCfg . wifi7 . enable {
# IEEE 802.11be (WiFi 7) related configuration
ieee80211be = true ;
eht_oper_chwidth = radioCfg . wifi7 . operatingChannelWidth ;
eht_su_beamformer = radioCfg . wifi7 . singleUserBeamformer ;
eht_su_beamformee = radioCfg . wifi7 . singleUserBeamformee ;
eht_mu_beamformer = radioCfg . wifi7 . multiUserBeamformer ;
} ;
} )
) ;
2012-10-05 21:39:56 -07:00
} ;
} ;
2012-10-09 12:19:09 -07:00
} ;
2012-10-05 21:39:56 -07:00
2025-01-03 20:27:46 +01:00
imports =
let
renamedOptionMessage = message : ''
$ { message }
Refer to the documentation of ` services . hostapd . radios ` for an example and more information .
'' ;
in
[
( mkRemovedOptionModule [ " s e r v i c e s " " h o s t a p d " " i n t e r f a c e " ] (
renamedOptionMessage " A l l o t h e r o p t i o n s f o r t h i s i n t e r f a c e a r e n o w s e t v i a ` s e r v i c e s . h o s t a p d . r a d i o s . « i n t e r f a c e » . * ` . "
) )
( mkRemovedOptionModule [ " s e r v i c e s " " h o s t a p d " " d r i v e r " ] (
renamedOptionMessage " I t h a s b e e n r e p l a c e d b y ` s e r v i c e s . h o s t a p d . r a d i o s . « i n t e r f a c e » . d r i v e r ` . "
) )
( mkRemovedOptionModule [ " s e r v i c e s " " h o s t a p d " " n o S c a n " ] (
renamedOptionMessage " I t h a s b e e n r e p l a c e d b y ` s e r v i c e s . h o s t a p d . r a d i o s . « i n t e r f a c e » . n o S c a n ` . "
) )
( mkRemovedOptionModule [ " s e r v i c e s " " h o s t a p d " " c o u n t r y C o d e " ] (
renamedOptionMessage " I t h a s b e e n r e p l a c e d b y ` s e r v i c e s . h o s t a p d . r a d i o s . « i n t e r f a c e » . c o u n t r y C o d e ` . "
) )
( mkRemovedOptionModule [ " s e r v i c e s " " h o s t a p d " " h w M o d e " ] (
renamedOptionMessage " I t h a s b e e n r e p l a c e d b y ` s e r v i c e s . h o s t a p d . r a d i o s . « i n t e r f a c e » . b a n d ` . "
) )
( mkRemovedOptionModule [ " s e r v i c e s " " h o s t a p d " " c h a n n e l " ] (
renamedOptionMessage " I t h a s b e e n r e p l a c e d b y ` s e r v i c e s . h o s t a p d . r a d i o s . « i n t e r f a c e » . c h a n n e l ` . "
) )
( mkRemovedOptionModule [ " s e r v i c e s " " h o s t a p d " " e x t r a C o n f i g " ] ( renamedOptionMessage ''
2023-03-22 00:49:08 +01:00
It has been replaced by ` services . hostapd . radios . « interface » . settings ` and
` services . hostapd . radios . « interface » . networks . « network » . settings ` respectively
for per-radio and per-network extra configuration . The module now supports a lot more
options inherently , so please re-check whether using settings is still necessary . '' ) )
2025-01-03 20:27:46 +01:00
( mkRemovedOptionModule [ " s e r v i c e s " " h o s t a p d " " l o g L e v e l " ] (
renamedOptionMessage " I t h a s b e e n r e p l a c e d b y ` s e r v i c e s . h o s t a p d . r a d i o s . « i n t e r f a c e » . n e t w o r k s . « n e t w o r k » . l o g L e v e l ` . "
) )
( mkRemovedOptionModule [ " s e r v i c e s " " h o s t a p d " " g r o u p " ] (
renamedOptionMessage " I t h a s b e e n r e p l a c e d b y ` s e r v i c e s . h o s t a p d . r a d i o s . « i n t e r f a c e » . n e t w o r k s . « n e t w o r k » . g r o u p ` . "
) )
( mkRemovedOptionModule [ " s e r v i c e s " " h o s t a p d " " s s i d " ] (
renamedOptionMessage " I t h a s b e e n r e p l a c e d b y ` s e r v i c e s . h o s t a p d . r a d i o s . « i n t e r f a c e » . n e t w o r k s . « n e t w o r k » . s s i d ` . "
) )
( mkRemovedOptionModule [ " s e r v i c e s " " h o s t a p d " " w p a " ] (
renamedOptionMessage " I t h a s b e e n r e p l a c e d b y ` s e r v i c e s . h o s t a p d . r a d i o s . « i n t e r f a c e » . n e t w o r k s . « n e t w o r k » . a u t h e n t i c a t i o n . m o d e ` . "
) )
( mkRemovedOptionModule [ " s e r v i c e s " " h o s t a p d " " w p a P a s s p h r a s e " ]
( renamedOptionMessage ''
It has been replaced by ` services . hostapd . radios . « interface » . networks . « network » . authentication . wpaPassword ` .
While upgrading your config , please consider using the newer SAE authentication scheme
and one of the new ` passwordFile ` - like options to avoid putting the password into the world readable nix-store . '' )
)
] ;
2012-10-05 21:39:56 -07:00
config = mkIf cfg . enable {
2023-03-22 00:49:08 +01:00
assertions =
[
{
2025-01-03 20:27:46 +01:00
assertion = cfg . radios != { } ;
2023-03-22 00:49:08 +01:00
message = " A t l e a s t o n e r a d i o m u s t b e c o n f i g u r e d w i t h h o s t a p d ! " ;
}
]
# Radio warnings
2025-01-03 20:27:46 +01:00
++ ( concatLists (
mapAttrsToList (
2023-03-22 00:49:08 +01:00
radio : radioCfg :
2025-01-03 20:27:46 +01:00
[
{
assertion = radioCfg . networks != { } ;
message = " h o s t a p d r a d i o ${ radio } : A t l e a s t o n e n e t w o r k m u s t b e c o n f i g u r e d ! " ;
}
# XXX: There could be many more useful assertions about (band == xy) -> ensure other required settings.
# see https://github.com/openwrt/openwrt/blob/539cb5389d9514c99ec1f87bd4465f77c7ed9b93/package/kernel/mac80211/files/lib/netifd/wireless/mac80211.sh#L158
{
assertion = length ( filter ( bss : bss = = radio ) ( attrNames radioCfg . networks ) ) = = 1 ;
message = '' h o s t a p d r a d i o ${ radio } : E x a c t l y o n e n e t w o r k m u s t b e n a m e d l i k e t h e r a d i o , f o r r e a s o n s i n t e r n a l t o h o s t a p d . '' ;
}
{
assertion =
( radioCfg . wifi4 . enable && builtins . elem " H T 4 0 - " radioCfg . wifi4 . capabilities )
-> radioCfg . channel != 0 ;
message = '' h o s t a p d r a d i o ${ radio } : u s i n g A C S ( c h a n n e l = 0 ) t o g e t h e r w i t h H T 4 0 - ( w i f i 4 . c a p a b i l i t i e s ) i s u n s u p p o r t e d b y h o s t a p d '' ;
}
]
# BSS warnings
++ ( concatLists (
mapAttrsToList (
bss : bssCfg :
let
2023-03-22 00:49:08 +01:00
auth = bssCfg . authentication ;
countWpaPasswordDefinitions = count ( x : x != null ) [
auth . wpaPassword
auth . wpaPasswordFile
auth . wpaPskFile
] ;
2025-01-03 20:27:46 +01:00
in
[
2023-03-22 00:49:08 +01:00
{
assertion = hasPrefix radio bss ;
message = " h o s t a p d r a d i o ${ radio } b s s ${ bss } : T h e b s s ( n e t w o r k ) n a m e ${ bss } i s i n v a l i d . I t m u s t b e p r e f i x e d b y t h e r a d i o n a m e f o r r e a s o n s i n t e r n a l t o h o s t a p d . A v a l i d n a m e w o u l d b e e . g . ${ radio } , ${ radio } - 1 , . . . " ;
}
{
assertion = ( length ( attrNames radioCfg . networks ) > 1 ) -> ( bssCfg . bssid != null ) ;
message = '' h o s t a p d r a d i o ${ radio } b s s ${ bss } : b s s i d m u s t b e s p e c i f i e d m a n u a l l y ( f o r n o w ) s i n c e t h i s r a d i o u s e s m u l t i p l e B S S . '' ;
}
{
assertion = countWpaPasswordDefinitions <= 1 ;
message = '' h o s t a p d r a d i o ${ radio } b s s ${ bss } : m u s t u s e a t m o s t o n e W P A p a s s w o r d o p t i o n ( w p a P a s s w o r d , w p a P a s s w o r d F i l e , w p a P s k F i l e ) '' ;
}
{
2025-01-03 20:27:46 +01:00
assertion =
auth . wpaPassword != null
-> ( stringLength auth . wpaPassword >= 8 && stringLength auth . wpaPassword <= 63 ) ;
2023-03-22 00:49:08 +01:00
message = '' h o s t a p d r a d i o ${ radio } b s s ${ bss } : u s e s a w p a P a s s w o r d o f i n v a l i d l e n g t h ( m u s t b e i n [ 8 , 6 3 ] ) . '' ;
}
{
2025-01-03 20:27:46 +01:00
assertion = auth . saePasswords = = [ ] || auth . saePasswordsFile = = null ;
2023-03-22 00:49:08 +01:00
message = '' h o s t a p d r a d i o ${ radio } b s s ${ bss } : m u s t u s e o n l y o n e S A E p a s s w o r d o p t i o n ( s a e P a s s w o r d s o r s a e P a s s w o r d s F i l e ) '' ;
}
{
2025-01-03 20:27:46 +01:00
assertion = auth . mode = = " w p a 3 - s a e " -> ( auth . saePasswords != [ ] || auth . saePasswordsFile != null ) ;
2023-03-22 00:49:08 +01:00
message = '' h o s t a p d r a d i o ${ radio } b s s ${ bss } : u s e s W P A 3 - S A E w h i c h r e q u i r e s d e f i n i n g a s a e p a s s w o r d o p t i o n '' ;
}
{
2025-01-03 20:27:46 +01:00
assertion =
auth . mode = = " w p a 3 - s a e - t r a n s i t i o n "
-> ( auth . saePasswords != [ ] || auth . saePasswordsFile != null ) && countWpaPasswordDefinitions = = 1 ;
2023-03-22 00:49:08 +01:00
message = '' h o s t a p d r a d i o ${ radio } b s s ${ bss } : u s e s W P A 3 - S A E i n t r a n s i t i o n m o d e r e q u i r e s d e f i n i n g b o t h a w p a p a s s w o r d o p t i o n a n d a s a e p a s s w o r d o p t i o n '' ;
}
{
2025-01-03 20:27:46 +01:00
assertion =
( auth . mode = = " w p a 2 - s h a 1 " || auth . mode = = " w p a 2 - s h a 2 5 6 " ) -> countWpaPasswordDefinitions = = 1 ;
2024-05-04 22:18:33 +10:00
message = '' h o s t a p d r a d i o ${ radio } b s s ${ bss } : u s e s W P A 2 - P S K w h i c h r e q u i r e s d e f i n i n g a w p a p a s s w o r d o p t i o n '' ;
2023-03-22 00:49:08 +01:00
}
2025-01-03 20:27:46 +01:00
]
2025-01-03 20:28:08 +01:00
++ optionals ( auth . saePasswords != [ ] ) (
imap1 ( i : entry : {
assertion = ( entry . password = = null ) != ( entry . passwordFile = = null ) ;
message = '' h o s t a p d r a d i o ${ radio } b s s ${ bss } s a e P a s s w o r d e n t r y ${ i } : m u s t s e t e x a c t l y o n e o f ` p a s s w o r d ` o r ` p a s s w o r d F i l e ` '' ;
} ) auth . saePasswords
)
2025-01-03 20:27:46 +01:00
) radioCfg . networks
) )
) cfg . radios
) ) ;
2012-10-05 21:39:56 -07:00
2025-01-03 20:27:46 +01:00
environment . systemPackages = [ cfg . package ] ;
2012-10-05 21:39:56 -07:00
2023-03-22 00:49:08 +01:00
systemd . services . hostapd = {
description = " I E E E 8 0 2 . 1 1 H o s t A c c e s s - P o i n t D a e m o n " ;
2013-02-24 03:11:45 -08:00
2025-01-03 20:27:46 +01:00
path = [ cfg . package ] ;
after = map ( radio : " s y s - s u b s y s t e m - n e t - d e v i c e s - ${ utils . escapeSystemdPath radio } . d e v i c e " ) (
attrNames cfg . radios
) ;
bindsTo = map ( radio : " s y s - s u b s y s t e m - n e t - d e v i c e s - ${ utils . escapeSystemdPath radio } . d e v i c e " ) (
attrNames cfg . radios
) ;
wantedBy = [ " m u l t i - u s e r . t a r g e t " ] ;
2013-02-24 03:11:45 -08:00
2023-03-22 00:49:08 +01:00
# Create merged configuration and acl files for each radio (and their bss's) prior to starting
preStart = concatStringsSep " \n " ( mapAttrsToList makeRadioRuntimeFiles cfg . radios ) ;
serviceConfig = {
ExecStart = " ${ cfg . package } / b i n / h o s t a p d ${ concatStringsSep " " runtimeConfigFiles } " ;
Restart = " a l w a y s " ;
ExecReload = " ${ pkgs . coreutils } / b i n / k i l l - H U P $ M A I N P I D " ;
RuntimeDirectory = " h o s t a p d " ;
# Hardening
LockPersonality = true ;
MemoryDenyWriteExecute = true ;
DevicePolicy = " c l o s e d " ;
DeviceAllow = " / d e v / r f k i l l r w " ;
NoNewPrivileges = true ;
PrivateUsers = false ; # hostapd requires true root access.
2025-03-12 13:22:28 +03:00
PrivateTmp = false ; # hostapd_cli opens a socket in /tmp
2023-03-22 00:49:08 +01:00
ProtectClock = true ;
ProtectControlGroups = true ;
ProtectHome = true ;
ProtectHostname = true ;
ProtectKernelLogs = true ;
ProtectKernelModules = true ;
ProtectKernelTunables = true ;
ProtectProc = " i n v i s i b l e " ;
ProcSubset = " p i d " ;
ProtectSystem = " s t r i c t " ;
RestrictAddressFamilies = [
" A F _ I N E T "
" A F _ I N E T 6 "
" A F _ N E T L I N K "
" A F _ U N I X "
2023-07-21 21:11:14 -05:00
" A F _ P A C K E T "
2023-03-22 00:49:08 +01:00
] ;
RestrictNamespaces = true ;
RestrictRealtime = true ;
RestrictSUIDSGID = true ;
SystemCallArchitectures = " n a t i v e " ;
SystemCallFilter = [
" @ s y s t e m - s e r v i c e "
" ~ @ p r i v i l e g e d "
" @ c h o w n "
] ;
UMask = " 0 0 7 7 " ;
2012-10-05 21:39:56 -07:00
} ;
2023-03-22 00:49:08 +01:00
} ;
2012-10-05 21:39:56 -07:00
} ;
}