nixos/wpa_supplicant: allow duplicate network blocks (#441410)

This commit is contained in:
Michele Guerini Rocco
2025-09-15 03:52:15 +00:00
committed by GitHub
3 changed files with 254 additions and 158 deletions
@@ -250,6 +250,8 @@
- `services.gitea` supports sending notifications with sendmail again. To do this, activate the parameter `services.gitea.mailerUseSendmail` and configure SMTP server.
- `networking.wireless.networks.<name>` now has an option to specify SSID, hence allowing duplicated SSID setup. The BSSID option is added along side with this.
- Revamp of the ACME certificate acquisication and renewal process to help scale systems with lots (100+) of certificates.
Units and targets have been reshaped to better support more specific dependency propagation and avoid
@@ -37,7 +37,9 @@ let
mkWPA2Fallback = opts: opts // { authProtocols = subtractLists wpa3Protocols opts.authProtocols; };
# Networks attrset as a list
networkList = mapAttrsToList (ssid: opts: opts // { inherit ssid; }) cfg.networks;
# We use the ssid from the options, which defaults to the ssid but can be overridden.
# The ssid in attrNames is hence unused here.
networkList = attrValues cfg.networks;
# List of all networks (normal + generated fallbacks)
allNetworks =
@@ -89,6 +91,7 @@ let
"key_mgmt=NONE"
)
]
++ optional (opts.bssid != null) "bssid=${opts.bssid}"
++ optional opts.hidden "scan_ssid=1"
++ optional (pskString != null) "psk=${pskString}"
++ optionals (opts.auth != null) (filter (x: x != "") (splitString "\n" opts.auth))
@@ -280,170 +283,195 @@ in
networks = mkOption {
type = types.attrsOf (
types.submodule {
options = {
psk = mkOption {
type = types.nullOr (types.strMatching "[[:print:]]{8,63}");
default = null;
description = ''
The network's pre-shared key in plaintext defaulting
to being a network without any authentication.
types.submodule (
{ name, ... }:
{
options = {
ssid = mkOption {
type = types.str;
default = name;
description = ''
You could use this field to override the network's ssid.
This can be useful to, for example, specify two networks
that share the same SSID but not the same password.
Specifying the BSSID of the network can make two entries of
the same SSID show up as different ones in wpa_cli.
'';
};
::: {.warning}
Be aware that this will be written to the Nix store
in plaintext! Use {var}`pskRaw` with an external
reference to keep it safe.
:::
bssid = mkOption {
type = types.nullOr types.str;
default = null;
example = "02:00:00:00:00:01";
description = ''
If set, this network block is used only when associating with
the AP using the configured BSSID.
'';
};
::: {.note}
Mutually exclusive with {var}`pskRaw`.
:::
'';
};
psk = mkOption {
type = types.nullOr (types.strMatching "[[:print:]]{8,63}");
default = null;
description = ''
The network's pre-shared key in plaintext defaulting
to being a network without any authentication.
pskRaw = mkOption {
type = types.nullOr (types.strMatching "([[:xdigit:]]{64})|(ext:[^=]+)");
default = null;
example = "ext:name_of_the_secret_here";
description = ''
Either the raw pre-shared key in hexadecimal format
or the name of the secret (as defined inside
[](#opt-networking.wireless.secretsFile) and prefixed
with `ext:`) containing the network pre-shared key.
::: {.warning}
Be aware that this will be written to the Nix store
in plaintext! Use {var}`pskRaw` with an external
reference to keep it safe.
:::
::: {.warning}
Be aware that this will be written to the Nix store
in plaintext! Always use an external reference.
:::
::: {.note}
Mutually exclusive with {var}`pskRaw`.
:::
'';
};
::: {.note}
The external secret can be either the plaintext
passphrase or the raw pre-shared key.
:::
pskRaw = mkOption {
type = types.nullOr (types.strMatching "([[:xdigit:]]{64})|(ext:[^=]+)");
default = null;
example = "ext:name_of_the_secret_here";
description = ''
Either the raw pre-shared key in hexadecimal format
or the name of the secret (as defined inside
[](#opt-networking.wireless.secretsFile) and prefixed
with `ext:`) containing the network pre-shared key.
::: {.note}
Mutually exclusive with {var}`psk` and {var}`auth`.
:::
'';
};
::: {.warning}
Be aware that this will be written to the Nix store
in plaintext! Always use an external reference.
:::
authProtocols = mkOption {
default = [
# WPA2 and WPA3
"WPA-PSK"
"WPA-EAP"
"SAE"
# 802.11r variants of the above
"FT-PSK"
"FT-EAP"
"FT-SAE"
];
# The list can be obtained by running this command
# awk '
# /^# key_mgmt: /{ run=1 }
# /^#$/{ run=0 }
# /^# [A-Z0-9-]{2,}/{ if(run){printf("\"%s\"\n", $2)} }
# ' /run/current-system/sw/share/doc/wpa_supplicant/wpa_supplicant.conf.example
type = types.listOf (
types.enum [
::: {.note}
The external secret can be either the plaintext
passphrase or the raw pre-shared key.
:::
::: {.note}
Mutually exclusive with {var}`psk` and {var}`auth`.
:::
'';
};
authProtocols = mkOption {
default = [
# WPA2 and WPA3
"WPA-PSK"
"WPA-EAP"
"IEEE8021X"
"NONE"
"WPA-NONE"
"SAE"
# 802.11r variants of the above
"FT-PSK"
"FT-EAP"
"FT-EAP-SHA384"
"WPA-PSK-SHA256"
"WPA-EAP-SHA256"
"SAE"
"FT-SAE"
"WPA-EAP-SUITE-B"
"WPA-EAP-SUITE-B-192"
"OSEN"
"FILS-SHA256"
"FILS-SHA384"
"FT-FILS-SHA256"
"FT-FILS-SHA384"
"OWE"
"DPP"
]
);
description = ''
The list of authentication protocols accepted by this network.
This corresponds to the `key_mgmt` option in wpa_supplicant.
'';
];
# The list can be obtained by running this command
# awk '
# /^# key_mgmt: /{ run=1 }
# /^#$/{ run=0 }
# /^# [A-Z0-9-]{2,}/{ if(run){printf("\"%s\"\n", $2)} }
# ' /run/current-system/sw/share/doc/wpa_supplicant/wpa_supplicant.conf.example
type = types.listOf (
types.enum [
"WPA-PSK"
"WPA-EAP"
"IEEE8021X"
"NONE"
"WPA-NONE"
"FT-PSK"
"FT-EAP"
"FT-EAP-SHA384"
"WPA-PSK-SHA256"
"WPA-EAP-SHA256"
"SAE"
"FT-SAE"
"WPA-EAP-SUITE-B"
"WPA-EAP-SUITE-B-192"
"OSEN"
"FILS-SHA256"
"FILS-SHA384"
"FT-FILS-SHA256"
"FT-FILS-SHA384"
"OWE"
"DPP"
]
);
description = ''
The list of authentication protocols accepted by this network.
This corresponds to the `key_mgmt` option in wpa_supplicant.
'';
};
auth = mkOption {
type = types.nullOr types.str;
default = null;
example = ''
eap=PEAP
identity="user@example.com"
password=ext:example_password
'';
description = ''
Use this option to configure advanced authentication methods
like EAP. See {manpage}`wpa_supplicant.conf(5)` for example
configurations.
::: {.warning}
Be aware that this will be written to the Nix store
in plaintext! Use an external reference like
`ext:secretname` for secrets.
:::
::: {.note}
Mutually exclusive with {var}`psk` and {var}`pskRaw`.
:::
'';
};
hidden = mkOption {
type = types.bool;
default = false;
description = ''
Set this to `true` if the SSID of the network is hidden.
'';
example = literalExpression ''
{ echelon = {
hidden = true;
psk = "abcdefgh";
};
}
'';
};
priority = mkOption {
type = types.nullOr types.int;
default = null;
description = ''
By default, all networks will get same priority group (0). If
some of the networks are more desirable, this field can be used
to change the order in which wpa_supplicant goes through the
networks when selecting a BSS. The priority groups will be
iterated in decreasing priority (i.e., the larger the priority
value, the sooner the network is matched against the scan
results). Within each priority group, networks will be selected
based on security policy, signal strength, etc.
'';
};
extraConfig = mkOption {
type = types.str;
default = "";
example = ''
bssid_blacklist=02:11:22:33:44:55 02:22:aa:44:55:66
'';
description = ''
Extra configuration lines appended to the network block.
See {manpage}`wpa_supplicant.conf(5)` for available options.
'';
};
};
auth = mkOption {
type = types.nullOr types.str;
default = null;
example = ''
eap=PEAP
identity="user@example.com"
password=ext:example_password
'';
description = ''
Use this option to configure advanced authentication methods
like EAP. See {manpage}`wpa_supplicant.conf(5)` for example
configurations.
::: {.warning}
Be aware that this will be written to the Nix store
in plaintext! Use an external reference like
`ext:secretname` for secrets.
:::
::: {.note}
Mutually exclusive with {var}`psk` and {var}`pskRaw`.
:::
'';
};
hidden = mkOption {
type = types.bool;
default = false;
description = ''
Set this to `true` if the SSID of the network is hidden.
'';
example = literalExpression ''
{ echelon = {
hidden = true;
psk = "abcdefgh";
};
}
'';
};
priority = mkOption {
type = types.nullOr types.int;
default = null;
description = ''
By default, all networks will get same priority group (0). If
some of the networks are more desirable, this field can be used
to change the order in which wpa_supplicant goes through the
networks when selecting a BSS. The priority groups will be
iterated in decreasing priority (i.e., the larger the priority
value, the sooner the network is matched against the scan
results). Within each priority group, networks will be selected
based on security policy, signal strength, etc.
'';
};
extraConfig = mkOption {
type = types.str;
default = "";
example = ''
bssid_blacklist=02:11:22:33:44:55 02:22:aa:44:55:66
'';
description = ''
Extra configuration lines appended to the network block.
See {manpage}`wpa_supplicant.conf(5)` for available options.
'';
};
};
}
}
)
);
description = ''
The network definitions to automatically connect to when
+72 -6
View File
@@ -13,8 +13,31 @@ let
naughtyPassphrase = ''!,./;'[]\-=<>?:"{}|_+@$%^&*()`~ # ceci n'est pas un commentaire'';
runBssidTest =
name: expectedBssid: extraConfig:
runSimulatorTest name extraConfig ''
with subtest("Daemon can connect to the right access point"):
machine.wait_for_unit("wpa_supplicant-wlan1.service")
machine.wait_until_succeeds(
"wpa_cli -i wlan1 status | grep -q wpa_state=COMPLETED"
)
machine.wait_until_succeeds(
"wpa_cli -i wlan1 status | grep -q bssid=${expectedBssid}"
)
'';
runConnectionTest =
name: extraConfig:
runSimulatorTest name extraConfig ''
with subtest("Daemon can connect to the access point"):
machine.wait_for_unit("wpa_supplicant-wlan1.service")
machine.wait_until_succeeds(
"wpa_cli -i wlan1 status | grep -q wpa_state=COMPLETED"
)
'';
runSimulatorTest =
name: extraConfig: extraTestScript:
runTest {
name = "wpa_supplicant-${name}";
inherit meta;
@@ -50,12 +73,22 @@ let
bssid = "02:00:00:00:00:01";
};
wlan0-2 = {
ssid = "nixos-test-mixed";
authentication = {
mode = "wpa3-sae-transition";
saeAddToMacAllow = true;
saePasswordsFile = pkgs.writeText "password" naughtyPassphrase;
wpaPasswordFile = pkgs.writeText "password" naughtyPassphrase;
};
bssid = "02:00:00:00:00:02";
};
wlan0-3 = {
ssid = "nixos-test-wpa2";
authentication = {
mode = "wpa2-sha256";
wpaPassword = naughtyPassphrase;
};
bssid = "02:00:00:00:00:02";
bssid = "02:00:00:00:00:03";
};
};
};
@@ -85,11 +118,7 @@ let
machine.wait_for_unit("hostapd.service")
machine.copy_from_vm("/run/hostapd/wlan0.hostapd.conf")
with subtest("Daemon can connect to the access point"):
machine.wait_for_unit("wpa_supplicant-wlan1.service")
machine.wait_until_succeeds(
"wpa_cli -i wlan1 status | grep -q wpa_state=COMPLETED"
)
${extraTestScript}
'';
};
@@ -129,6 +158,18 @@ in
psk = "password";
authProtocols = [ "SAE" ];
};
# Test duplicate SSID generation
duplicate1 = {
ssid = "duplicate";
bssid = "00:00:00:00:00:01";
psk = "password";
};
duplicate2 = {
ssid = "duplicate";
bssid = "00:00:00:00:00:02";
psk = "password";
};
};
};
};
@@ -148,6 +189,12 @@ in
assert int(machine.succeed(f"grep -c sae-only {config_file}")) == 1
assert int(machine.succeed(f"grep -c mixed-wpa {config_file}")) == 2
with subtest("Duplicate SSID network blocks have been generated"):
# more duplication due to fallbacks
assert int(machine.succeed(f"grep -c duplicate {config_file}")) == 4
assert int(machine.succeed(f"grep -c bssid=00:00:00:00:00:01 {config_file}")) == 2
assert int(machine.succeed(f"grep -c bssid=00:00:00:00:00:02 {config_file}")) == 2
# save file for manual inspection
machine.copy_from_vm(config_file)
'';
@@ -222,4 +269,23 @@ in
authProtocols = [ "WPA-PSK-SHA256" ];
};
};
# Test connection with the highest prio "matching" network block found.
# "Matching" meaning with the right SSID and BSSID
bssidGuard = runBssidTest "bssid-guard" "02:00:00:00:00:02" {
networks = {
"1_first" = {
ssid = "nixos-test-mixed";
bssid = "02:00:00:00:00:01";
pskRaw = "ext:psk_nixos_test";
};
"2_second" = {
ssid = "nixos-test-mixed";
bssid = "02:00:00:00:00:02";
pskRaw = "ext:psk_nixos_test";
priority = 1;
};
};
};
}