NetworkManager/src/core/supplicant/nm-supplicant-config.c
Louis Kotze f74b5809a1
supplicant: dedupe MLO per-link BSSIDs in bgscan multi-AP heuristic
nm_supplicant_config_add_bgscan() picks "simple:30:-65:300" for any
profile that has been seen on more than one BSSID, on the
assumption that this is a multi-AP ESS where periodic roam-candidate
scanning is desirable. Each scan blocks the data path on the radio
for the full scan duration (5-7 s on rtw89 USB at MCS 13), causing
audible/visible stalls in real-time UDP applications such as Teams
and Discord every 5 minutes.

For single-AP Wi-Fi 7 MLO, this heuristic misfires: the AP
advertises one BSSID per link, so a 2-link or 3-link MLD looks
like a 2-3 AP ESS to the seen-bssids count. Verified locally on
RTL8922AU + Bazzite Linux 6.19.11 against TP-Link Deco BE63 mesh;
the connection's seen-bssids list contains only the per-link
BSSIDs of the single physical AP it has been associated with
(Link 1 and Link 2 of the MLD), but bgscan flips to the multi-AP
value with the user-visible stall pattern.

Add a conservative heuristic to detect MLO per-link BSSIDs and
exempt them from the multi-AP path. The detection requires ALL of:

  - 2 to 3 seen BSSIDs (802.11be defines tri-link 2.4 + 5 + 6 GHz
    as the maximum)
  - every BSSID has the locally-administered bit set (vendors use
    LAA-flagged virtual MACs for per-link addresses; verified on
    TP-Link Deco BE63: Link 1 f6:75:0c:74:4b:75 and Link 2
    ca:75:0c:74:4b:70 both have bit 1 set)
  - every BSSID shares octets 1-4 with the others (per-link MACs
    derived from a common base, varying only the first and last
    octets)

Real multi-AP ESSes (mesh, enterprise) typically use vendor-assigned
MACs (LAA bit unset) and unrelated address blocks per AP, so the
heuristic should not false-positive on legitimate multi-AP networks.
The connection's seen-bssids list is bounded at 30 entries (LRU) per
NM_SETTINGS_CONNECTION_SEEN_BSSIDS_MAX; the strv passed in from
build_supplicant_config() is the daemon-managed authoritative copy
loaded from /var/lib/NetworkManager/seen-bssids, not the unreliable
NMSettingWireless property.

Extend nm_supplicant_config_add_bgscan() to take the BSSID strv as
a parameter alongside the existing num_seen_bssids count (mirroring
the 2023 fix in commit 07c6f933d1 ('wifi: fix aggressively
roaming (background Wi-Fi scanning) based on seen-bssids') for
num_seen_bssids), so the heuristic can inspect actual addresses
rather than only the count.

Add a regression test test_wifi_bgscan_mlo_dedup() in
test-supplicant-config.c covering four cases: 2-link MLO BSSIDs
(LAA + shared octets 1-4) producing the long-interval bgscan;
tri-link MLO BSSIDs (3-BSSID variant matching 802.11be's tri-link
maximum) also producing the long-interval bgscan; real multi-AP
BSSIDs (UAA, unrelated blocks) producing the short interval; and
all-LAA BSSIDs with unrelated octets 1-4 still producing the short
interval (no false-positive on LAA alone).

Empirical A/B on the local rig (2026-04-29):
  - With this dedup applied: bgscan = "simple:30:-70:86400";
    0 scans observed in 18-min capture; 0 audio/video stalls in
    a 30-min Discord call
  - Without (stock 1.54): bgscan = "simple:30:-65:300";
    6 full-band scans at exactly 305 s cadence; 4 stall bursts
    coinciding with scan windows in 30 min

Heuristic was calibrated against TP-Link Deco BE63; non-Deco MLO
hardware was not available for testing. The conservative
all-of-three-conditions check should reject most non-MLO traffic,
but vendor diversity in MLO per-link MAC derivation has not been
characterised. Testers with non-Deco MLO hardware welcome.

Signed-off-by: Louis Kotze <loukot@gmail.com>
2026-05-06 14:37:22 +02:00

2085 lines
81 KiB
C

/* SPDX-License-Identifier: GPL-2.0-or-later */
/*
* Copyright (C) 2006 - 2012 Red Hat, Inc.
* Copyright (C) 2007 - 2008 Novell, Inc.
*/
#include "src/core/nm-default-daemon.h"
#include "nm-supplicant-config.h"
#include <stdlib.h>
#include "libnm-glib-aux/nm-str-buf.h"
#include "libnm-core-intern/nm-core-internal.h"
#include "nm-supplicant-settings-verify.h"
#include "nm-setting.h"
#include "libnm-core-aux-intern/nm-auth-subject.h"
#include "NetworkManagerUtils.h"
#include "nm-utils.h"
#include "nm-setting-ip4-config.h"
typedef struct {
char *value;
guint32 len;
NMSupplOptType type;
} ConfigOption;
/*****************************************************************************/
typedef struct {
GHashTable *config;
GHashTable *blobs;
char *private_user;
NMSupplCapMask capabilities;
guint32 ap_scan;
bool fast_required : 1;
bool dispose_has_run : 1;
bool ap_isolation : 1;
} NMSupplicantConfigPrivate;
struct _NMSupplicantConfig {
GObject parent;
NMSupplicantConfigPrivate _priv;
};
struct _NMSupplicantConfigClass {
GObjectClass parent;
};
G_DEFINE_TYPE(NMSupplicantConfig, nm_supplicant_config, G_TYPE_OBJECT)
#define NM_SUPPLICANT_CONFIG_GET_PRIVATE(self) \
_NM_GET_PRIVATE(self, NMSupplicantConfig, NM_IS_SUPPLICANT_CONFIG)
/*****************************************************************************/
static gboolean
_get_capability(NMSupplicantConfigPrivate *priv, NMSupplCapType type)
{
return NM_SUPPL_CAP_MASK_GET(priv->capabilities, type) == NM_TERNARY_TRUE;
}
NMSupplicantConfig *
nm_supplicant_config_new(NMSupplCapMask capabilities, const char *private_user)
{
NMSupplicantConfigPrivate *priv;
NMSupplicantConfig *self;
self = g_object_new(NM_TYPE_SUPPLICANT_CONFIG, NULL);
priv = NM_SUPPLICANT_CONFIG_GET_PRIVATE(self);
priv->capabilities = capabilities;
priv->private_user = g_strdup(private_user);
return self;
}
static void
config_option_free(ConfigOption *opt)
{
g_free(opt->value);
g_slice_free(ConfigOption, opt);
}
static void
nm_supplicant_config_init(NMSupplicantConfig *self)
{
NMSupplicantConfigPrivate *priv = NM_SUPPLICANT_CONFIG_GET_PRIVATE(self);
priv->config = g_hash_table_new_full(nm_str_hash,
g_str_equal,
g_free,
(GDestroyNotify) config_option_free);
priv->ap_scan = 1;
priv->dispose_has_run = FALSE;
}
static gboolean
nm_supplicant_config_add_option_with_type(NMSupplicantConfig *self,
const char *key,
const char *value,
gint32 len,
NMSupplOptType opt_type,
const char *display_value,
GError **error)
{
NMSupplicantConfigPrivate *priv;
ConfigOption *old_opt;
ConfigOption *opt;
NMSupplOptType type;
g_return_val_if_fail(NM_IS_SUPPLICANT_CONFIG(self), FALSE);
g_return_val_if_fail(key != NULL, FALSE);
g_return_val_if_fail(value != NULL, FALSE);
nm_assert(!error || !*error);
priv = NM_SUPPLICANT_CONFIG_GET_PRIVATE(self);
if (len < 0)
len = strlen(value);
if (opt_type != NM_SUPPL_OPT_TYPE_INVALID)
type = opt_type;
else {
type = nm_supplicant_settings_verify_setting(key, value, len);
if (type == NM_SUPPL_OPT_TYPE_INVALID) {
gs_free char *str_free = NULL;
const char *str;
str = nm_utils_buf_utf8safe_escape(value,
len,
NM_UTILS_STR_UTF8_SAFE_FLAG_ESCAPE_CTRL,
&str_free);
str = nm_strquote_a(255, str);
g_set_error(error,
NM_SUPPLICANT_ERROR,
NM_SUPPLICANT_ERROR_CONFIG,
"key '%s' and/or value %s invalid",
key,
display_value ?: str);
return FALSE;
}
}
old_opt = (ConfigOption *) g_hash_table_lookup(priv->config, key);
if (old_opt) {
g_set_error(error,
NM_SUPPLICANT_ERROR,
NM_SUPPLICANT_ERROR_CONFIG,
"key '%s' already configured",
key);
return FALSE;
}
opt = g_slice_new(ConfigOption);
*opt = (ConfigOption) {
.value = nm_memdup_nul(value, len),
.len = len,
.type = type,
};
{
char buf[255];
memset(&buf[0], 0, sizeof(buf));
memcpy(&buf[0], opt->value, opt->len > 254 ? 254 : opt->len);
nm_log_info(LOGD_SUPPLICANT,
"Config: added '%s' value '%s'",
key,
display_value ?: &buf[0]);
}
g_hash_table_insert(priv->config, g_strdup(key), opt);
return TRUE;
}
static gboolean
nm_supplicant_config_add_option(NMSupplicantConfig *self,
const char *key,
const char *value,
gint32 len,
const char *display_value,
GError **error)
{
return nm_supplicant_config_add_option_with_type(self,
key,
value,
len,
NM_SUPPL_OPT_TYPE_INVALID,
display_value,
error);
}
static gboolean
nm_supplicant_config_add_blob(NMSupplicantConfig *self,
const char *key,
GBytes *value,
const char *blobid,
GError **error)
{
NMSupplicantConfigPrivate *priv;
ConfigOption *old_opt;
ConfigOption *opt;
NMSupplOptType type;
gsize data_len;
gs_free char *full_value = NULL;
g_return_val_if_fail(NM_IS_SUPPLICANT_CONFIG(self), FALSE);
g_return_val_if_fail(key != NULL, FALSE);
g_return_val_if_fail(value != NULL, FALSE);
g_return_val_if_fail(blobid != NULL, FALSE);
g_bytes_get_data(value, &data_len);
g_return_val_if_fail(data_len > 0, FALSE);
if (data_len > 32 * 1024 * 1024) {
g_set_error(error,
NM_SUPPLICANT_ERROR,
NM_SUPPLICANT_ERROR_CONFIG,
"blob '%s' is larger than 32MiB",
key);
return FALSE;
}
priv = NM_SUPPLICANT_CONFIG_GET_PRIVATE(self);
full_value = g_strdup_printf("blob://%s", blobid);
type = nm_supplicant_settings_verify_setting(key, full_value, strlen(full_value));
if (type == NM_SUPPL_OPT_TYPE_INVALID) {
g_set_error(error,
NM_SUPPLICANT_ERROR,
NM_SUPPLICANT_ERROR_CONFIG,
"key '%s' and/or its contained value is invalid",
key);
return FALSE;
}
old_opt = (ConfigOption *) g_hash_table_lookup(priv->config, key);
if (old_opt) {
g_set_error(error,
NM_SUPPLICANT_ERROR,
NM_SUPPLICANT_ERROR_CONFIG,
"key '%s' already configured",
key);
return FALSE;
}
opt = g_slice_new0(ConfigOption);
opt->value = g_steal_pointer(&full_value);
opt->len = strlen(opt->value);
opt->type = type;
nm_log_info(LOGD_SUPPLICANT, "Config: added '%s' value '%s'", key, opt->value);
g_hash_table_insert(priv->config, g_strdup(key), opt);
if (!priv->blobs) {
priv->blobs =
g_hash_table_new_full(nm_str_hash, g_str_equal, g_free, (GDestroyNotify) g_bytes_unref);
}
g_hash_table_insert(priv->blobs, g_strdup(blobid), g_bytes_ref(value));
return TRUE;
}
static gboolean
nm_supplicant_config_add_blob_for_connection(NMSupplicantConfig *self,
GBytes *field,
const char *name,
const char *con_uuid,
GError **error)
{
if (field && g_bytes_get_size(field)) {
gs_free char *blob_id = NULL;
char *p;
blob_id = g_strdup_printf("%s-%s", con_uuid, name);
for (p = blob_id; *p; p++) {
if (*p == '/')
*p = '-';
}
if (!nm_supplicant_config_add_blob(self, name, field, blob_id, error))
return FALSE;
}
return TRUE;
}
static void
nm_supplicant_config_finalize(GObject *object)
{
NMSupplicantConfigPrivate *priv = NM_SUPPLICANT_CONFIG_GET_PRIVATE(object);
g_hash_table_destroy(priv->config);
nm_clear_pointer(&priv->blobs, g_hash_table_destroy);
nm_clear_pointer(&priv->private_user, g_free);
G_OBJECT_CLASS(nm_supplicant_config_parent_class)->finalize(object);
}
static void
nm_supplicant_config_class_init(NMSupplicantConfigClass *klass)
{
GObjectClass *object_class = G_OBJECT_CLASS(klass);
object_class->finalize = nm_supplicant_config_finalize;
}
guint32
nm_supplicant_config_get_ap_scan(NMSupplicantConfig *self)
{
g_return_val_if_fail(NM_IS_SUPPLICANT_CONFIG(self), 1);
return NM_SUPPLICANT_CONFIG_GET_PRIVATE(self)->ap_scan;
}
gboolean
nm_supplicant_config_fast_required(NMSupplicantConfig *self)
{
g_return_val_if_fail(NM_IS_SUPPLICANT_CONFIG(self), FALSE);
return NM_SUPPLICANT_CONFIG_GET_PRIVATE(self)->fast_required;
}
GVariant *
nm_supplicant_config_to_variant(NMSupplicantConfig *self)
{
NMSupplicantConfigPrivate *priv;
GVariantBuilder builder;
GHashTableIter iter;
ConfigOption *option;
const char *key;
g_return_val_if_fail(NM_IS_SUPPLICANT_CONFIG(self), NULL);
priv = NM_SUPPLICANT_CONFIG_GET_PRIVATE(self);
g_variant_builder_init(&builder, G_VARIANT_TYPE_VARDICT);
g_hash_table_iter_init(&iter, priv->config);
while (g_hash_table_iter_next(&iter, (gpointer) &key, (gpointer) &option)) {
switch (option->type) {
case NM_SUPPL_OPT_TYPE_INT:
g_variant_builder_add(&builder, "{sv}", key, g_variant_new_int32(atoi(option->value)));
break;
case NM_SUPPL_OPT_TYPE_BYTES:
case NM_SUPPL_OPT_TYPE_UTF8:
g_variant_builder_add(&builder,
"{sv}",
key,
nm_g_variant_new_ay((const guint8 *) option->value, option->len));
break;
case NM_SUPPL_OPT_TYPE_KEYWORD:
case NM_SUPPL_OPT_TYPE_STRING:
g_variant_builder_add(&builder, "{sv}", key, g_variant_new_string(option->value));
break;
default:
break;
}
}
return g_variant_builder_end(&builder);
}
GHashTable *
nm_supplicant_config_get_blobs(NMSupplicantConfig *self)
{
g_return_val_if_fail(NM_IS_SUPPLICANT_CONFIG(self), NULL);
return NM_SUPPLICANT_CONFIG_GET_PRIVATE(self)->blobs;
}
static const char *
wifi_freqs_to_string(const char *band)
{
static const char *str_2ghz = NULL;
static const char *str_5ghz = NULL;
static const char *str_6ghz = NULL;
const char **f_p;
const char *f;
if (nm_streq0(band, "a"))
f_p = &str_5ghz;
else if (nm_streq0(band, "bg"))
f_p = &str_2ghz;
else if (nm_streq0(band, "6GHz"))
f_p = &str_6ghz;
else {
nm_assert_not_reached();
return NULL;
}
again:
f = g_atomic_pointer_get(f_p);
if (G_UNLIKELY(!f)) {
nm_auto_str_buf NMStrBuf strbuf = NM_STR_BUF_INIT(400, FALSE);
const guint *freqs;
int i;
if (f_p == &str_2ghz)
freqs = nm_utils_wifi_2ghz_freqs();
else if (f_p == &str_5ghz)
freqs = nm_utils_wifi_5ghz_freqs();
else
freqs = nm_utils_wifi_6ghz_freqs();
for (i = 0; freqs[i]; i++) {
if (i > 0)
nm_str_buf_append_c(&strbuf, ' ');
nm_str_buf_append_printf(&strbuf, "%u", freqs[i]);
}
f = g_strdup(nm_str_buf_get_str(&strbuf));
if (!g_atomic_pointer_compare_and_exchange(f_p, NULL, f)) {
g_free((char *) f);
goto again;
}
}
return f;
}
gboolean
nm_supplicant_config_add_setting_macsec(NMSupplicantConfig *self,
NMSettingMacsec *setting,
NMSettingMacsecOffload offload,
GError **error)
{
const char *value;
char buf[32];
int port;
gsize key_len;
const char *offload_str = NULL;
g_return_val_if_fail(NM_IS_SUPPLICANT_CONFIG(self), FALSE);
g_return_val_if_fail(setting != NULL, FALSE);
g_return_val_if_fail(!error || !*error, FALSE);
if (!nm_supplicant_config_add_option(self, "macsec_policy", "1", -1, NULL, error))
return FALSE;
value = nm_setting_macsec_get_encrypt(setting) ? "0" : "1";
if (!nm_supplicant_config_add_option(self, "macsec_integ_only", value, -1, NULL, error))
return FALSE;
port = nm_setting_macsec_get_port(setting);
if (port > 0 && port < 65534) {
g_snprintf(buf, sizeof(buf), "%d", port);
if (!nm_supplicant_config_add_option(self, "macsec_port", buf, -1, NULL, error))
return FALSE;
}
if (nm_setting_macsec_get_mode(setting) == NM_SETTING_MACSEC_MODE_PSK) {
guint8 buffer_cak[NM_SETTING_MACSEC_MKA_CAK_LENGTH / 2];
guint8 buffer_ckn[NM_SETTING_MACSEC_MKA_CKN_LENGTH / 2];
if (!nm_supplicant_config_add_option(self, "key_mgmt", "NONE", -1, NULL, error))
return FALSE;
value = nm_setting_macsec_get_mka_cak(setting);
if (!value || !nm_utils_hexstr2bin_buf(value, FALSE, FALSE, NULL, buffer_cak)) {
g_set_error_literal(error,
NM_SUPPLICANT_ERROR,
NM_SUPPLICANT_ERROR_CONFIG,
value ? "invalid MKA CAK" : "missing MKA CAK");
return FALSE;
}
if (!nm_supplicant_config_add_option(self,
"mka_cak",
(char *) buffer_cak,
sizeof(buffer_cak),
"<hidden>",
error))
return FALSE;
value = nm_setting_macsec_get_mka_ckn(setting);
if (!value
|| !nm_utils_hexstr2bin_full(value,
FALSE,
FALSE,
FALSE,
NULL,
0,
buffer_ckn,
G_N_ELEMENTS(buffer_ckn),
&key_len)) {
g_set_error_literal(error,
NM_SUPPLICANT_ERROR,
NM_SUPPLICANT_ERROR_CONFIG,
value ? "invalid MKA CKN" : "missing MKA CKN");
return FALSE;
}
if (!nm_supplicant_config_add_option(self,
"mka_ckn",
(char *) buffer_ckn,
key_len,
value,
error))
return FALSE;
}
switch (offload) {
case NM_SETTING_MACSEC_OFFLOAD_OFF:
/* This is the default in wpa_supplicant. Don't set the option,
* so that if user doesn't enable offload, the connection still
* works with previous versions of the supplicant.
*/
break;
case NM_SETTING_MACSEC_OFFLOAD_PHY:
offload_str = "1";
break;
case NM_SETTING_MACSEC_OFFLOAD_MAC:
offload_str = "2";
break;
case NM_SETTING_MACSEC_OFFLOAD_DEFAULT:
nm_assert_not_reached();
break;
}
if (offload_str
&& !nm_supplicant_config_add_option(self, "macsec_offload", offload_str, -1, NULL, error)) {
return FALSE;
}
return TRUE;
}
static void
get_ap_params(guint freq,
NMSettingWirelessChannelWidth width,
guint *out_ht40,
int *out_max_oper_chwidth,
guint *out_center_freq)
{
*out_ht40 = 0;
*out_max_oper_chwidth = -1;
*out_center_freq = 0;
switch (width) {
case NM_SETTING_WIRELESS_CHANNEL_WIDTH_40MHZ:
*out_ht40 = 1;
*out_max_oper_chwidth = 0;
return;
case NM_SETTING_WIRELESS_CHANNEL_WIDTH_80MHZ:
{
guint channel;
guint center_channel = 0;
/* Determine the center channel according to the table at
* https://en.wikipedia.org/wiki/List_of_WLAN_channels */
if (freq > 5950) {
/* 6 GHz */
channel = (freq - 5950) / 5;
channel = ((channel - 1) / 16) * 16 + 7;
*out_ht40 = 1;
*out_max_oper_chwidth = 1;
*out_center_freq = 5950 + 5 * channel;
} else {
/* 5 GHz */
if (freq < 5000) {
/* the setting is not valid */
nm_assert_not_reached();
return;
}
channel = (freq - 5000) / 5;
if (channel >= 36 && channel <= 48)
center_channel = 42;
else if (channel >= 52 && channel <= 64)
center_channel = 58;
else if (channel >= 100 && channel <= 112)
center_channel = 106;
else if (channel >= 116 && channel <= 128)
center_channel = 122;
else if (channel >= 132 && channel <= 144)
center_channel = 138;
else if (channel >= 149 && channel <= 161)
center_channel = 155;
else if (channel >= 165 && channel <= 177)
center_channel = 171;
if (center_channel) {
*out_ht40 = 1;
*out_max_oper_chwidth = 1;
*out_center_freq = 5000 + 5 * center_channel;
}
}
return;
}
case NM_SETTING_WIRELESS_CHANNEL_WIDTH_AUTO:
case NM_SETTING_WIRELESS_CHANNEL_WIDTH_20MHZ:
default:
/* in case of unknown enum value, fall back to the safest parameters */
return;
}
}
gboolean
nm_supplicant_config_add_setting_wireless(NMSupplicantConfig *self,
NMSettingWireless *setting,
guint32 fixed_freq,
GError **error)
{
NMSupplicantConfigPrivate *priv;
gboolean is_adhoc, is_ap, is_mesh;
const char *mode, *band;
guint32 channel;
GBytes *ssid;
const char *bssid;
g_return_val_if_fail(NM_IS_SUPPLICANT_CONFIG(self), FALSE);
g_return_val_if_fail(setting != NULL, FALSE);
g_return_val_if_fail(!error || !*error, FALSE);
priv = NM_SUPPLICANT_CONFIG_GET_PRIVATE(self);
mode = nm_setting_wireless_get_mode(setting);
is_adhoc = nm_streq0(mode, "adhoc");
is_ap = nm_streq0(mode, "ap");
is_mesh = nm_streq0(mode, "mesh");
if (is_adhoc || is_ap)
priv->ap_scan = 2;
else
priv->ap_scan = 1;
ssid = nm_setting_wireless_get_ssid(setting);
if (!nm_supplicant_config_add_option(self,
"ssid",
(char *) g_bytes_get_data(ssid, NULL),
g_bytes_get_size(ssid),
NULL,
error))
return FALSE;
if (is_adhoc) {
if (!nm_supplicant_config_add_option(self, "mode", "1", -1, NULL, error))
return FALSE;
}
if (is_ap) {
if (!nm_supplicant_config_add_option(self, "mode", "2", -1, NULL, error))
return FALSE;
if (nm_setting_wireless_get_hidden(setting)
&& !nm_supplicant_config_add_option(self,
"ignore_broadcast_ssid",
"1",
-1,
NULL,
error))
return FALSE;
}
if (is_mesh) {
if (!nm_supplicant_config_add_option(self, "mode", "5", -1, NULL, error))
return FALSE;
}
if ((is_adhoc || is_ap || is_mesh) && fixed_freq) {
gs_free char *str_freq = NULL;
guint ht40;
int max_oper_chwidth;
guint center_freq;
str_freq = g_strdup_printf("%u", fixed_freq);
if (!nm_supplicant_config_add_option(self, "frequency", str_freq, -1, NULL, error))
return FALSE;
if (is_ap) {
get_ap_params(fixed_freq,
nm_setting_wireless_get_channel_width(setting),
&ht40,
&max_oper_chwidth,
&center_freq);
if (!nm_supplicant_config_add_option(self, "ht40", ht40 ? "1" : "0", -1, NULL, error))
return FALSE;
if (center_freq != 0) {
g_free(str_freq);
str_freq = g_strdup_printf("%u", center_freq);
if (!nm_supplicant_config_add_option(self,
"vht_center_freq1",
str_freq,
-1,
NULL,
error))
return FALSE;
}
if (max_oper_chwidth >= 0) {
g_free(str_freq);
str_freq = g_strdup_printf("%u", max_oper_chwidth);
if (!nm_supplicant_config_add_option(self,
"max_oper_chwidth",
str_freq,
-1,
NULL,
error))
return FALSE;
}
}
}
/* Except for Ad-Hoc, Hotspot and Mesh, request that the driver probe for the
* specific SSID we want to associate with.
*/
if (!(is_adhoc || is_ap || is_mesh)) {
if (!nm_supplicant_config_add_option(self, "scan_ssid", "1", -1, NULL, error))
return FALSE;
}
bssid = nm_setting_wireless_get_bssid(setting);
if (bssid) {
if (!nm_supplicant_config_add_option(self, "bssid", bssid, strlen(bssid), NULL, error))
return FALSE;
}
band = nm_setting_wireless_get_band(setting);
channel = nm_setting_wireless_get_channel(setting);
if (band) {
if (channel) {
guint32 freq;
gs_free char *str_freq = NULL;
freq = nm_utils_wifi_channel_to_freq(channel, band);
str_freq = g_strdup_printf("%u", freq);
if (!nm_supplicant_config_add_option(self, "freq_list", str_freq, -1, NULL, error))
return FALSE;
} else {
const char *freqs = NULL;
freqs = wifi_freqs_to_string(band);
if (freqs
&& !nm_supplicant_config_add_option(self,
"freq_list",
freqs,
strlen(freqs),
NULL,
error))
return FALSE;
}
}
return TRUE;
}
/* Maximum number of seen BSSIDs we treat as evidence of a single Wi-Fi 7
* MLO AP. 802.11be defines tri-link (2.4 + 5 + 6 GHz) as the maximum;
* beyond 3 the heuristic does not apply and the multi-AP path runs.
*/
#define _BGSCAN_MLO_HEURISTIC_MAX_BSSIDS 3u
/* _seen_bssids_look_like_mlo_per_link:
*
* Heuristic returning TRUE when @seen_bssids appears to be the per-link
* BSSIDs of one Wi-Fi 7 MLO-capable AP rather than several physical APs.
*
* In MLO, one physical AP advertises one BSSID per link (2 for 5+6 GHz,
* 3 for tri-link). Vendors observed in the wild derive the per-link
* BSSIDs as locally-administered virtual MAC addresses sharing octets
* 1-4 with each other. The MLD address itself may or may not be in the
* seen-bssids list.
*
* Real multi-AP ESSes (mesh, enterprise) typically use vendor-assigned
* MACs (LAA bit unset) and unrelated address blocks per AP, so the
* heuristic should not false-positive on them.
*/
static gboolean
_seen_bssids_look_like_mlo_per_link(const char *const *seen_bssids, guint num_seen_bssids)
{
guint8 common_octets_1_to_4[4];
guint i;
if (num_seen_bssids < 2u || num_seen_bssids > _BGSCAN_MLO_HEURISTIC_MAX_BSSIDS)
return FALSE;
if (!seen_bssids)
return FALSE;
for (i = 0; i < num_seen_bssids; i++) {
const char *bssid = seen_bssids[i];
guint8 addr[6];
if (!bssid || !nm_utils_hwaddr_aton(bssid, addr, sizeof(addr)))
return FALSE;
/* Locally-administered bit is bit 1 (value 0x02) of the first octet. */
if (!(addr[0] & 0x02u))
return FALSE;
/* Octets 1-4 must be identical across all seen BSSIDs (only octets
* 0 and 5 vary across per-link MAC addresses for the same MLD).
*/
if (i == 0)
memcpy(common_octets_1_to_4, &addr[1], 4);
else if (memcmp(common_octets_1_to_4, &addr[1], 4) != 0)
return FALSE;
}
return TRUE;
}
gboolean
nm_supplicant_config_add_bgscan(NMSupplicantConfig *self,
NMConnection *connection,
guint num_seen_bssids,
const char *const *seen_bssids,
GError **error)
{
NMSettingWireless *s_wifi;
NMSettingWirelessSecurity *s_wsec;
const char *bgscan;
s_wifi = nm_connection_get_setting_wireless(connection);
g_assert(s_wifi);
/* Don't scan when a shared connection (either AP or Ad-Hoc) is active;
* it will disrupt connected clients.
*/
if (NM_IN_STRSET(nm_setting_wireless_get_mode(s_wifi),
NM_SETTING_WIRELESS_MODE_AP,
NM_SETTING_WIRELESS_MODE_ADHOC))
return TRUE;
/* Don't scan when the connection is locked to a specific AP, since
* intra-ESS roaming (which requires periodic scanning) isn't being
* used due to the specific AP lock. (bgo #513820)
*/
if (nm_setting_wireless_get_bssid(s_wifi))
return TRUE;
/* Default to a very long bgscan interval when signal is OK on the assumption
* that either (a) there aren't multiple APs and we don't need roaming, or
* (b) since EAP/802.1x isn't used and thus there are fewer steps to fail
* during a roam, we can wait longer before scanning for roam candidates.
*/
bgscan = "simple:30:-70:86400";
/* If using WPA Enterprise, Dynamic WEP or we have seen more than one AP use
* a shorter bgscan interval on the assumption that this is a multi-AP ESS
* in which we want more reliable roaming between APs. Thus trigger scans
* when the signal is still somewhat OK so we have an up-to-date roam
* candidate list when the signal gets bad.
*
* Exception: when @seen_bssids look like the per-link addresses of a
* single WiFi-7 MLO AP (locally-administered bit set + shared octets
* 1-4), keep the long interval. See _seen_bssids_look_like_mlo_per_link().
*/
if ((num_seen_bssids > 1u && !_seen_bssids_look_like_mlo_per_link(seen_bssids, num_seen_bssids))
|| ((s_wsec = nm_connection_get_setting_wireless_security(connection))
&& NM_IN_STRSET(nm_setting_wireless_security_get_key_mgmt(s_wsec),
"ieee8021x",
"wpa-eap",
"wpa-eap-suite-b-192")))
bgscan = "simple:30:-65:300";
return nm_supplicant_config_add_option(self, "bgscan", bgscan, -1, FALSE, error);
}
static gboolean
add_string_val(NMSupplicantConfig *self,
const char *field,
const char *name,
gboolean ucase,
const char *display_value,
GError **error)
{
if (field) {
gs_free char *value = NULL;
if (ucase) {
value = g_ascii_strup(field, -1);
field = value;
}
return nm_supplicant_config_add_option(self,
name,
field,
strlen(field),
display_value,
error);
}
return TRUE;
}
#define ADD_STRING_LIST_VAL(self, \
setting, \
setting_name, \
field, \
field_plural, \
name, \
separator, \
ucase, \
display_value, \
error) \
({ \
typeof(setting) _setting = (setting); \
gboolean _success = TRUE; \
\
if (nm_setting_##setting_name##_get_num_##field_plural(_setting)) { \
const char _separator = (separator); \
GString *_str = g_string_new(NULL); \
guint _k, _n; \
\
_n = nm_setting_##setting_name##_get_num_##field_plural(_setting); \
for (_k = 0; _k < _n; _k++) { \
const char *item = nm_setting_##setting_name##_get_##field(_setting, _k); \
\
if (!_str->len) { \
g_string_append(_str, item); \
} else { \
g_string_append_c(_str, _separator); \
g_string_append(_str, item); \
} \
} \
if ((ucase)) \
g_string_ascii_up(_str); \
if (_str->len) { \
if (!nm_supplicant_config_add_option((self), \
(name), \
_str->str, \
-1, \
(display_value), \
(error))) \
_success = FALSE; \
} \
g_string_free(_str, TRUE); \
} \
_success; \
})
static void
wep128_passphrase_hash(const char *input, gsize input_len, guint8 *digest /* 13 bytes */)
{
nm_auto_free_checksum GChecksum *sum = NULL;
guint8 md5[NM_UTILS_CHECKSUM_LENGTH_MD5];
guint8 data[64];
int i;
nm_assert(input);
nm_assert(input_len);
nm_assert(digest);
/* Get at least 64 bytes by repeating the passphrase into the buffer */
for (i = 0; i < sizeof(data); i++)
data[i] = input[i % input_len];
sum = g_checksum_new(G_CHECKSUM_MD5);
g_checksum_update(sum, data, sizeof(data));
nm_utils_checksum_get_digest(sum, md5);
/* WEP104 keys are 13 bytes in length (26 hex characters) */
memcpy(digest, md5, 13);
}
static gboolean
add_wep_key(NMSupplicantConfig *self,
const char *key,
const char *name,
NMWepKeyType wep_type,
GError **error)
{
gsize key_len;
if (!key || (key_len = strlen(key)) == 0)
return TRUE;
if (wep_type == NM_WEP_KEY_TYPE_UNKNOWN) {
if (nm_utils_wep_key_valid(key, NM_WEP_KEY_TYPE_KEY))
wep_type = NM_WEP_KEY_TYPE_KEY;
else if (nm_utils_wep_key_valid(key, NM_WEP_KEY_TYPE_PASSPHRASE))
wep_type = NM_WEP_KEY_TYPE_PASSPHRASE;
}
if ((wep_type == NM_WEP_KEY_TYPE_UNKNOWN) || (wep_type == NM_WEP_KEY_TYPE_KEY)) {
if ((key_len == 10) || (key_len == 26)) {
guint8 buffer[26 / 2];
if (!nm_utils_hexstr2bin_full(key,
FALSE,
FALSE,
FALSE,
NULL,
key_len / 2,
buffer,
sizeof(buffer),
NULL)) {
g_set_error(error,
NM_SUPPLICANT_ERROR,
NM_SUPPLICANT_ERROR_CONFIG,
"cannot add wep-key %s to supplicant config because key is not hex",
name);
return FALSE;
}
if (!nm_supplicant_config_add_option(self,
name,
(char *) buffer,
key_len / 2,
"<hidden>",
error))
return FALSE;
} else if ((key_len == 5) || (key_len == 13)) {
if (!nm_supplicant_config_add_option(self, name, key, key_len, "<hidden>", error))
return FALSE;
} else {
g_set_error(
error,
NM_SUPPLICANT_ERROR,
NM_SUPPLICANT_ERROR_CONFIG,
"Cannot add wep-key %s to supplicant config because key-length %u is invalid",
name,
(guint) key_len);
return FALSE;
}
} else if (wep_type == NM_WEP_KEY_TYPE_PASSPHRASE) {
guint8 digest[13];
wep128_passphrase_hash(key, key_len, digest);
if (!nm_supplicant_config_add_option(self,
name,
(const char *) digest,
sizeof(digest),
"<hidden>",
error))
return FALSE;
}
return TRUE;
}
gboolean
nm_supplicant_config_add_setting_wireless_security(NMSupplicantConfig *self,
NMSettingWirelessSecurity *setting,
NMSetting8021x *setting_8021x,
const char *con_uuid,
const char *mode,
guint32 mtu,
NMSettingWirelessSecurityPmf pmf,
NMSettingWirelessSecurityFils fils,
GHashTable *files,
GError **error)
{
NMSupplicantConfigPrivate *priv = NM_SUPPLICANT_CONFIG_GET_PRIVATE(self);
nm_auto_free_gstring GString *key_mgmt_conf = NULL;
const char *key_mgmt, *auth_alg;
const char *psk;
gboolean set_pmf, wps_disabled;
gboolean is_ap;
g_return_val_if_fail(NM_IS_SUPPLICANT_CONFIG(self), FALSE);
g_return_val_if_fail(setting != NULL, FALSE);
g_return_val_if_fail(con_uuid != NULL, FALSE);
g_return_val_if_fail(!error || !*error, FALSE);
/* Currently wpa_supplicant doesn't support FT in AP mode. Even
* if it did, it would require additional parameters as the nas
* identifier and the mobility domain. Therefore we disable all
* FT key-mgmts in AP mode.
*/
is_ap = nm_streq0(mode, NM_SETTING_WIRELESS_MODE_AP);
/* Check if we actually support FILS */
if (!_get_capability(priv, NM_SUPPL_CAP_TYPE_FILS)) {
if (fils == NM_SETTING_WIRELESS_SECURITY_FILS_REQUIRED) {
g_set_error_literal(error,
NM_SUPPLICANT_ERROR,
NM_SUPPLICANT_ERROR_CONFIG,
"Supplicant does not support FILS");
return FALSE;
} else if (fils == NM_SETTING_WIRELESS_SECURITY_FILS_OPTIONAL)
fils = NM_SETTING_WIRELESS_SECURITY_FILS_DISABLE;
}
key_mgmt = nm_setting_wireless_security_get_key_mgmt(setting);
key_mgmt_conf = g_string_new("");
if (nm_streq(key_mgmt, "none")) {
g_string_append(key_mgmt_conf, "NONE");
} else if (nm_streq(key_mgmt, "ieee8021x")) {
g_string_append(key_mgmt_conf, "IEEE8021X");
} else if (nm_streq(key_mgmt, "owe")) {
pmf = NM_SETTING_WIRELESS_SECURITY_PMF_REQUIRED;
g_string_append(key_mgmt_conf, "OWE");
} else if (nm_streq(key_mgmt, "wpa-psk")) {
if (pmf != NM_SETTING_WIRELESS_SECURITY_PMF_REQUIRED)
g_string_append(key_mgmt_conf, "WPA-PSK");
if (_get_capability(priv, NM_SUPPL_CAP_TYPE_PMF))
g_string_append(key_mgmt_conf, " WPA-PSK-SHA256");
if (!is_ap && _get_capability(priv, NM_SUPPL_CAP_TYPE_FT))
g_string_append(key_mgmt_conf, " FT-PSK");
/* For NM "key-mgmt=wpa-psk" doesn't strictly mean WPA1/wPA2 only,
* but also allows WPA3 (SAE), so that existing connections can
* benefit from the improved security when the AP gets upgraded.
*
* According to WPA3_Specification_v3.0 section 2.3, when operating
* in WPA3-Personal transition mode a STA:
*
* - should allow AKM suite selector: 00-0F-AC:6 (WPA-PSK-SHA256) to
* be selected for an association;
* - shall negotiate PMF when associating to an AP using SAE.
*
* Those conditions are met when the interface has capabilities
* SAE, PMF, BIP.
*
* According to WPA3_Specification_v3.0 section 2.3, when operating
* in WPA3-Personal transition mode an AP:
*
* - shall set MFPC to 1, MFPR to 0.
*
* Therefore, do not operate in WPA3-Personal transition mode when PMF
* is set to disabled. This also provides a way to be compatible with
* some devices that are not fully compatible with WPA3-Personal
* transition mode.
*/
if (_get_capability(priv, NM_SUPPL_CAP_TYPE_SAE)
&& _get_capability(priv, NM_SUPPL_CAP_TYPE_PMF)
&& _get_capability(priv, NM_SUPPL_CAP_TYPE_BIP)
&& (pmf != NM_SETTING_WIRELESS_SECURITY_PMF_DISABLE)) {
g_string_append(key_mgmt_conf, " SAE");
if (!is_ap && _get_capability(priv, NM_SUPPL_CAP_TYPE_FT))
g_string_append(key_mgmt_conf, " FT-SAE");
}
} else if (nm_streq(key_mgmt, "sae")) {
pmf = NM_SETTING_WIRELESS_SECURITY_PMF_REQUIRED;
g_string_append(key_mgmt_conf, "SAE");
if (!is_ap && _get_capability(priv, NM_SUPPL_CAP_TYPE_FT))
g_string_append(key_mgmt_conf, " FT-SAE");
} else if (nm_streq(key_mgmt, "wpa-eap")) {
if (pmf != NM_SETTING_WIRELESS_SECURITY_PMF_REQUIRED)
g_string_append(key_mgmt_conf, "WPA-EAP");
if (!is_ap && _get_capability(priv, NM_SUPPL_CAP_TYPE_FT)) {
g_string_append(key_mgmt_conf, " FT-EAP");
if (_get_capability(priv, NM_SUPPL_CAP_TYPE_SHA384))
g_string_append(key_mgmt_conf, " FT-EAP-SHA384");
}
if (_get_capability(priv, NM_SUPPL_CAP_TYPE_PMF)) {
g_string_append(key_mgmt_conf, " WPA-EAP-SHA256");
if (_get_capability(priv, NM_SUPPL_CAP_TYPE_SUITEB192)
&& pmf == NM_SETTING_WIRELESS_SECURITY_PMF_REQUIRED)
g_string_append(key_mgmt_conf, " WPA-EAP-SUITE-B-192");
}
switch (fils) {
case NM_SETTING_WIRELESS_SECURITY_FILS_REQUIRED:
g_string_truncate(key_mgmt_conf, 0);
if (!_get_capability(priv, NM_SUPPL_CAP_TYPE_PMF))
g_string_assign(key_mgmt_conf, "FILS-SHA256 FILS-SHA384");
/* fall-through */
case NM_SETTING_WIRELESS_SECURITY_FILS_OPTIONAL:
if (_get_capability(priv, NM_SUPPL_CAP_TYPE_PMF)) {
g_string_append(key_mgmt_conf, " FILS-SHA256 FILS-SHA384");
if (!is_ap && _get_capability(priv, NM_SUPPL_CAP_TYPE_FT)) {
g_string_append(key_mgmt_conf, " FT-FILS-SHA256");
if (_get_capability(priv, NM_SUPPL_CAP_TYPE_SHA384))
g_string_append(key_mgmt_conf, " FT-FILS-SHA384");
}
}
break;
default:
break;
}
} else if (nm_streq(key_mgmt, "wpa-eap-suite-b-192")) {
pmf = NM_SETTING_WIRELESS_SECURITY_PMF_REQUIRED;
g_string_append(key_mgmt_conf, "WPA-EAP-SUITE-B-192");
if (!is_ap && _get_capability(priv, NM_SUPPL_CAP_TYPE_FT)
&& _get_capability(priv, NM_SUPPL_CAP_TYPE_SHA384))
g_string_append(key_mgmt_conf, " FT-EAP-SHA384");
}
if (!add_string_val(self, key_mgmt_conf->str, "key_mgmt", TRUE, NULL, error))
return FALSE;
auth_alg = nm_setting_wireless_security_get_auth_alg(setting);
if (!add_string_val(self, auth_alg, "auth_alg", TRUE, NULL, error))
return FALSE;
psk = nm_setting_wireless_security_get_psk(setting);
if (psk) {
size_t psk_len = strlen(psk);
if (psk_len >= 8 && psk_len <= 63) {
/* Use NM_SUPPL_OPT_TYPE_STRING here so that it gets pushed to the
* supplicant as a string, and therefore gets quoted,
* and therefore the supplicant will interpret it as a
* passphrase and not a hex key.
*/
if (!nm_supplicant_config_add_option_with_type(self,
"psk",
psk,
-1,
NM_SUPPL_OPT_TYPE_STRING,
"<hidden>",
error))
return FALSE;
} else if (nm_streq(key_mgmt, "sae")) {
/* If the SAE password doesn't comply with WPA-PSK limitation,
* we need to call it "sae_password" instead of "psk".
*/
if (!nm_supplicant_config_add_option_with_type(self,
"sae_password",
psk,
-1,
NM_SUPPL_OPT_TYPE_STRING,
"<hidden>",
error))
return FALSE;
} else if (psk_len == 64) {
guint8 buffer[32];
/* Hex PSK */
if (!nm_utils_hexstr2bin_buf(psk, FALSE, FALSE, NULL, buffer)) {
g_set_error(error,
NM_SUPPLICANT_ERROR,
NM_SUPPLICANT_ERROR_CONFIG,
"Cannot add psk to supplicant config due to invalid hex");
return FALSE;
}
if (!nm_supplicant_config_add_option(self,
"psk",
(char *) buffer,
sizeof(buffer),
"<hidden>",
error))
return FALSE;
} else {
g_set_error(error,
NM_SUPPLICANT_ERROR,
NM_SUPPLICANT_ERROR_CONFIG,
"Cannot add psk to supplicant config due to invalid PSK length %u (not "
"between 8 and 63 characters)",
(guint) psk_len);
return FALSE;
}
}
/* Check if we actually support PMF */
set_pmf = TRUE;
if (!_get_capability(priv, NM_SUPPL_CAP_TYPE_PMF)) {
if (pmf == NM_SETTING_WIRELESS_SECURITY_PMF_REQUIRED) {
g_set_error_literal(error,
NM_SUPPLICANT_ERROR,
NM_SUPPLICANT_ERROR_CONFIG,
"Supplicant does not support PMF");
return FALSE;
}
set_pmf = FALSE;
}
/* Only WPA-specific things when using WPA */
if (NM_IN_STRSET(key_mgmt, "owe", "wpa-psk", "sae", "wpa-eap", "wpa-eap-suite-b-192")) {
if (!ADD_STRING_LIST_VAL(self,
setting,
wireless_security,
proto,
protos,
"proto",
' ',
TRUE,
NULL,
error))
return FALSE;
if (nm_streq(key_mgmt, "wpa-eap-suite-b-192")) {
if (!nm_supplicant_config_add_option(self, "pairwise", "GCMP-256", -1, NULL, error))
return FALSE;
if (!nm_supplicant_config_add_option(self, "group", "GCMP-256", -1, NULL, error))
return FALSE;
} else {
if (!ADD_STRING_LIST_VAL(self,
setting,
wireless_security,
pairwise,
pairwise,
"pairwise",
' ',
TRUE,
NULL,
error))
return FALSE;
if (!ADD_STRING_LIST_VAL(self,
setting,
wireless_security,
group,
groups,
"group",
' ',
TRUE,
NULL,
error))
return FALSE;
}
/* We set the supplicants global "pmf" config value to "1" (optional),
* so no need to set it network-specific again if PMF_OPTIONAL is set.
*/
if (set_pmf
&& NM_IN_SET(pmf,
NM_SETTING_WIRELESS_SECURITY_PMF_DISABLE,
NM_SETTING_WIRELESS_SECURITY_PMF_REQUIRED)) {
if (!nm_supplicant_config_add_option(
self,
"ieee80211w",
pmf == NM_SETTING_WIRELESS_SECURITY_PMF_DISABLE ? "0" : "2",
-1,
NULL,
error))
return FALSE;
}
/* In case the connection is saved as OWE / Enhanced Open, prevent
* unencrypted downgrade
*/
if (nm_streq(key_mgmt, "owe")) {
if (!nm_supplicant_config_add_option(self, "owe_only", "1", -1, NULL, error))
return FALSE;
}
}
/* WEP keys if required */
if (nm_streq(key_mgmt, "none")) {
NMWepKeyType wep_type = nm_setting_wireless_security_get_wep_key_type(setting);
const char *wep0 = nm_setting_wireless_security_get_wep_key(setting, 0);
const char *wep1 = nm_setting_wireless_security_get_wep_key(setting, 1);
const char *wep2 = nm_setting_wireless_security_get_wep_key(setting, 2);
const char *wep3 = nm_setting_wireless_security_get_wep_key(setting, 3);
if (!add_wep_key(self, wep0, "wep_key0", wep_type, error))
return FALSE;
if (!add_wep_key(self, wep1, "wep_key1", wep_type, error))
return FALSE;
if (!add_wep_key(self, wep2, "wep_key2", wep_type, error))
return FALSE;
if (!add_wep_key(self, wep3, "wep_key3", wep_type, error))
return FALSE;
if (wep0 || wep1 || wep2 || wep3) {
gs_free char *value = NULL;
value = g_strdup_printf("%d", nm_setting_wireless_security_get_wep_tx_keyidx(setting));
if (!nm_supplicant_config_add_option(self, "wep_tx_keyidx", value, -1, NULL, error))
return FALSE;
}
}
if (nm_streq0(auth_alg, "leap")) {
/* LEAP */
if (nm_streq(key_mgmt, "ieee8021x")) {
const char *tmp;
tmp = nm_setting_wireless_security_get_leap_username(setting);
if (!add_string_val(self, tmp, "identity", FALSE, NULL, error))
return FALSE;
tmp = nm_setting_wireless_security_get_leap_password(setting);
if (!add_string_val(self, tmp, "password", FALSE, "<hidden>", error))
return FALSE;
if (!add_string_val(self, "leap", "eap", TRUE, NULL, error))
return FALSE;
} else {
g_set_error(error,
NM_SUPPLICANT_ERROR,
NM_SUPPLICANT_ERROR_CONFIG,
"Invalid key-mgmt \"%s\" for leap",
key_mgmt);
return FALSE;
}
} else {
/* 802.1x for Dynamic WEP and WPA-Enterprise */
if (NM_IN_STRSET(key_mgmt, "ieee8021x", "wpa-eap", "wpa-eap-suite-b-192")) {
if (!setting_8021x) {
g_set_error(error,
NM_SUPPLICANT_ERROR,
NM_SUPPLICANT_ERROR_CONFIG,
"Cannot set key-mgmt %s with missing 8021x setting",
key_mgmt);
return FALSE;
}
if (!nm_supplicant_config_add_setting_8021x(self,
setting_8021x,
con_uuid,
mtu,
FALSE,
files,
error))
return FALSE;
}
if (NM_IN_STRSET(key_mgmt, "wpa-eap", "wpa-eap-suite-b-192")) {
/* When using WPA-Enterprise, we want to use Proactive Key Caching (also
* called Opportunistic Key Caching) to avoid full EAP exchanges when
* roaming between access points in the same mobility group.
*/
if (!nm_supplicant_config_add_option(self,
"proactive_key_caching",
"1",
-1,
NULL,
error))
return FALSE;
}
}
wps_disabled = (nm_setting_wireless_security_get_wps_method(setting)
== NM_SETTING_WIRELESS_SECURITY_WPS_METHOD_DISABLED);
if (wps_disabled) {
if (!nm_supplicant_config_add_option(self, "wps_disabled", "1", 1, NULL, error))
return FALSE;
}
return TRUE;
}
static gboolean
add_pkcs11_uri_with_pin(NMSupplicantConfig *self,
const char *name,
const char *uri,
const char *pin,
const NMSettingSecretFlags pin_flags,
GError **error)
{
gs_strfreev char **split = NULL;
gs_free char *tmp = NULL;
gs_free char *tmp_log = NULL;
gs_free char *pin_qattr = NULL;
char *escaped = NULL;
if (uri == NULL)
return TRUE;
/* We ignore the attributes -- RFC 7512 suggests that some of them
* might be unsafe and we want to be on the safe side. Also, we're
* installing our attributes, so this makes things a bit easier for us. */
split = g_strsplit(uri, "&", 2);
if (split[1])
nm_log_info(LOGD_SUPPLICANT, "URI attributes ignored");
/* Fill in the PIN if required. */
if (pin) {
escaped = g_uri_escape_string(pin, NULL, TRUE);
pin_qattr = g_strdup_printf("pin-value=%s", escaped);
g_free(escaped);
} else if (!(pin_flags & NM_SETTING_SECRET_FLAG_NOT_REQUIRED)) {
/* Include an empty PIN to indicate the login is still needed.
* Probably a token that has a PIN path and the actual PIN will
* be entered using a protected path. */
pin_qattr = g_strdup("pin-value=");
}
tmp = g_strdup_printf("%s%s%s", split[0], (pin_qattr ? "?" : ""), (pin_qattr ?: ""));
tmp_log = g_strdup_printf("%s%s%s",
split[0],
(pin_qattr ? "?" : ""),
(pin_qattr ? "pin-value=<hidden>" : ""));
return add_string_val(self, tmp, name, FALSE, tmp_log, error);
}
gboolean
nm_supplicant_config_add_setting_8021x(NMSupplicantConfig *self,
NMSetting8021x *setting,
const char *con_uuid,
guint32 mtu,
gboolean wired,
GHashTable *files,
GError **error)
{
NMSupplicantConfigPrivate *priv;
char *tmp;
const char *peapver, *value, *path;
gboolean added;
GString *phase1, *phase2;
GBytes *bytes;
gboolean fast = FALSE;
guint32 i, num_eap;
gboolean fast_provisoning_allowed = FALSE;
const char *ca_path_override = NULL, *ca_cert_override = NULL;
guint32 frag, hdrs;
gs_free char *frag_str = NULL;
NMSetting8021xAuthFlags phase1_auth_flags;
nm_auto_free_gstring GString *eap_str = NULL;
g_return_val_if_fail(NM_IS_SUPPLICANT_CONFIG(self), FALSE);
g_return_val_if_fail(setting != NULL, FALSE);
g_return_val_if_fail(con_uuid != NULL, FALSE);
priv = NM_SUPPLICANT_CONFIG_GET_PRIVATE(self);
value = nm_setting_802_1x_get_password(setting);
if (value) {
if (!add_string_val(self, value, "password", FALSE, "<hidden>", error))
return FALSE;
} else {
bytes = nm_setting_802_1x_get_password_raw(setting);
if (bytes) {
if (!nm_supplicant_config_add_option(self,
"password",
(const char *) g_bytes_get_data(bytes, NULL),
g_bytes_get_size(bytes),
"<hidden>",
error))
return FALSE;
}
}
value = nm_setting_802_1x_get_pin(setting);
if (!add_string_val(self, value, "pin", FALSE, "<hidden>", error))
return FALSE;
if (wired) {
if (!add_string_val(self, "IEEE8021X", "key_mgmt", FALSE, NULL, error))
return FALSE;
/* Wired 802.1x must always use eapol_flags=0 */
if (!add_string_val(self, "0", "eapol_flags", FALSE, NULL, error))
return FALSE;
priv->ap_scan = 0;
}
/* Build the "eap" option string while we check for EAP methods needing
* special handling: PEAP + GTC, FAST, external */
eap_str = g_string_new(NULL);
num_eap = nm_setting_802_1x_get_num_eap_methods(setting);
for (i = 0; i < num_eap; i++) {
const char *method = nm_setting_802_1x_get_eap_method(setting, i);
if (nm_streq(method, "fast")) {
fast = TRUE;
priv->fast_required = TRUE;
}
if (nm_streq(method, "external")) {
if (num_eap == 1) {
g_set_error(error,
NM_SUPPLICANT_ERROR,
NM_SUPPLICANT_ERROR_CONFIG,
"Connection settings managed externally to NM, connection"
" cannot be used with wpa_supplicant");
return FALSE;
}
continue;
}
if (eap_str->len)
g_string_append_c(eap_str, ' ');
g_string_append(eap_str, method);
}
g_string_ascii_up(eap_str);
if (eap_str->len
&& !nm_supplicant_config_add_option(self, "eap", eap_str->str, -1, NULL, error))
return FALSE;
/* Adjust the fragment size according to MTU, but do not set it higher than 1280-14
* for better compatibility */
hdrs = 14; /* EAPOL + EAP-TLS */
frag = 1280 - hdrs;
if (mtu > hdrs)
frag = CLAMP(mtu - hdrs, 100, frag);
frag_str = g_strdup_printf("%u", frag);
if (!nm_supplicant_config_add_option(self, "fragment_size", frag_str, -1, NULL, error))
return FALSE;
phase1 = g_string_new(NULL);
peapver = nm_setting_802_1x_get_phase1_peapver(setting);
if (peapver) {
if (nm_streq(peapver, "0"))
g_string_append(phase1, "peapver=0");
else if (nm_streq(peapver, "1"))
g_string_append(phase1, "peapver=1");
}
if (nm_setting_802_1x_get_phase1_peaplabel(setting)) {
if (phase1->len)
g_string_append_c(phase1, ' ');
g_string_append_printf(phase1,
"peaplabel=%s",
nm_setting_802_1x_get_phase1_peaplabel(setting));
}
value = nm_setting_802_1x_get_phase1_fast_provisioning(setting);
if (value) {
if (phase1->len)
g_string_append_c(phase1, ' ');
g_string_append_printf(phase1, "fast_provisioning=%s", value);
if (!nm_streq(value, "0"))
fast_provisoning_allowed = TRUE;
}
phase1_auth_flags = nm_setting_802_1x_get_phase1_auth_flags(setting);
if (NM_FLAGS_HAS(phase1_auth_flags, NM_SETTING_802_1X_AUTH_FLAGS_TLS_1_0_ENABLE))
g_string_append_printf(phase1, "%stls_disable_tlsv1_0=0", (phase1->len ? " " : ""));
else if (NM_FLAGS_HAS(phase1_auth_flags, NM_SETTING_802_1X_AUTH_FLAGS_TLS_1_0_DISABLE))
g_string_append_printf(phase1, "%stls_disable_tlsv1_0=1", (phase1->len ? " " : ""));
if (NM_FLAGS_HAS(phase1_auth_flags, NM_SETTING_802_1X_AUTH_FLAGS_TLS_1_1_ENABLE))
g_string_append_printf(phase1, "%stls_disable_tlsv1_1=0", (phase1->len ? " " : ""));
else if (NM_FLAGS_HAS(phase1_auth_flags, NM_SETTING_802_1X_AUTH_FLAGS_TLS_1_1_DISABLE))
g_string_append_printf(phase1, "%stls_disable_tlsv1_1=1", (phase1->len ? " " : ""));
if (NM_FLAGS_HAS(phase1_auth_flags, NM_SETTING_802_1X_AUTH_FLAGS_TLS_1_2_ENABLE))
g_string_append_printf(phase1, "%stls_disable_tlsv1_2=0", (phase1->len ? " " : ""));
else if (NM_FLAGS_HAS(phase1_auth_flags, NM_SETTING_802_1X_AUTH_FLAGS_TLS_1_2_DISABLE))
g_string_append_printf(phase1, "%stls_disable_tlsv1_2=1", (phase1->len ? " " : ""));
if (NM_FLAGS_HAS(phase1_auth_flags, NM_SETTING_802_1X_AUTH_FLAGS_TLS_1_3_ENABLE))
g_string_append_printf(phase1, "%stls_disable_tlsv1_3=0", (phase1->len ? " " : ""));
else if (NM_FLAGS_HAS(phase1_auth_flags, NM_SETTING_802_1X_AUTH_FLAGS_TLS_1_3_DISABLE))
g_string_append_printf(phase1, "%stls_disable_tlsv1_3=1", (phase1->len ? " " : ""));
if (NM_FLAGS_HAS(phase1_auth_flags, NM_SETTING_802_1X_AUTH_FLAGS_TLS_DISABLE_TIME_CHECKS))
g_string_append_printf(phase1, "%stls_disable_time_checks=1", (phase1->len ? " " : ""));
if (phase1->len) {
if (!add_string_val(self, phase1->str, "phase1", FALSE, NULL, error)) {
g_string_free(phase1, TRUE);
return FALSE;
}
}
g_string_free(phase1, TRUE);
phase2 = g_string_new(NULL);
if (nm_setting_802_1x_get_phase2_auth(setting) && !fast_provisoning_allowed) {
tmp = g_ascii_strup(nm_setting_802_1x_get_phase2_auth(setting), -1);
g_string_append_printf(phase2, "auth=%s", tmp);
g_free(tmp);
}
if (nm_setting_802_1x_get_phase2_autheap(setting)) {
if (phase2->len)
g_string_append_c(phase2, ' ');
tmp = g_ascii_strup(nm_setting_802_1x_get_phase2_autheap(setting), -1);
g_string_append_printf(phase2, "autheap=%s", tmp);
g_free(tmp);
}
if (phase2->len) {
if (!add_string_val(self, phase2->str, "phase2", FALSE, NULL, error)) {
g_string_free(phase2, TRUE);
return FALSE;
}
}
g_string_free(phase2, TRUE);
/* PAC file */
path = nm_setting_802_1x_get_pac_file(setting);
if (path) {
if (!add_string_val(self, path, "pac_file", FALSE, NULL, error))
return FALSE;
} else {
/* PAC file is not specified.
* If provisioning is allowed, use an blob format.
*/
if (fast_provisoning_allowed) {
gs_free char *blob_name = NULL;
blob_name = g_strdup_printf("blob://pac-blob-%s", con_uuid);
if (!add_string_val(self, blob_name, "pac_file", FALSE, NULL, error))
return FALSE;
} else {
/* This is only error for EAP-FAST; don't disturb other methods. */
if (fast) {
g_set_error(error,
NM_SUPPLICANT_ERROR,
NM_SUPPLICANT_ERROR_CONFIG,
"EAP-FAST error: no PAC file provided and "
"automatic PAC provisioning is disabled");
return FALSE;
}
}
}
/* If user wants to use system CA certs, either populate ca_path (if the path
* is a directory) or ca_cert (the path is a file name) */
if (nm_setting_802_1x_get_system_ca_certs(setting)) {
if (g_file_test(SYSTEM_CA_PATH, G_FILE_TEST_IS_DIR))
ca_path_override = SYSTEM_CA_PATH;
else
ca_cert_override = SYSTEM_CA_PATH;
}
/* CA path */
path = nm_setting_802_1x_get_ca_path(setting);
path = ca_path_override ?: path;
if (path) {
if (!add_string_val(self, path, "ca_path", FALSE, NULL, error))
return FALSE;
}
/* Phase2 CA path */
path = nm_setting_802_1x_get_phase2_ca_path(setting);
path = ca_path_override ?: path;
if (path) {
if (!add_string_val(self, path, "ca_path2", FALSE, NULL, error))
return FALSE;
}
/* CA certificate */
path = NULL;
bytes = NULL;
if (ca_cert_override) {
/* This is a build-time-configured system-wide file path, no need to pass
* it as a blob */
path = ca_cert_override;
} else {
switch (nm_setting_802_1x_get_ca_cert_scheme(setting)) {
case NM_SETTING_802_1X_CK_SCHEME_BLOB:
bytes = nm_setting_802_1x_get_ca_cert_blob(setting);
break;
case NM_SETTING_802_1X_CK_SCHEME_PATH:
path = nm_setting_802_1x_get_ca_cert_path(setting);
if (priv->private_user)
bytes = nm_g_hash_table_lookup(files, path);
break;
case NM_SETTING_802_1X_CK_SCHEME_PKCS11:
if (!add_pkcs11_uri_with_pin(self,
"ca_cert",
nm_setting_802_1x_get_ca_cert_uri(setting),
nm_setting_802_1x_get_ca_cert_password(setting),
nm_setting_802_1x_get_ca_cert_password_flags(setting),
error)) {
return FALSE;
}
break;
default:
break;
}
}
if (bytes) {
if (!nm_supplicant_config_add_blob_for_connection(self, bytes, "ca_cert", con_uuid, error))
return FALSE;
} else if (path) {
/* Private connections cannot use paths other than the system CA store */
g_return_val_if_fail(ca_cert_override || !priv->private_user, FALSE);
if (!add_string_val(self, path, "ca_cert", FALSE, NULL, error))
return FALSE;
}
/* Phase 2 CA certificate */
path = NULL;
bytes = NULL;
if (ca_cert_override) {
/* This is a build-time-configured system-wide file path, no need to pass
* it as a blob */
path = ca_cert_override;
} else {
switch (nm_setting_802_1x_get_phase2_ca_cert_scheme(setting)) {
case NM_SETTING_802_1X_CK_SCHEME_BLOB:
bytes = nm_setting_802_1x_get_phase2_ca_cert_blob(setting);
break;
case NM_SETTING_802_1X_CK_SCHEME_PATH:
path = nm_setting_802_1x_get_phase2_ca_cert_path(setting);
if (priv->private_user)
bytes = nm_g_hash_table_lookup(files, path);
break;
case NM_SETTING_802_1X_CK_SCHEME_PKCS11:
if (!add_pkcs11_uri_with_pin(
self,
"ca_cert2",
nm_setting_802_1x_get_phase2_ca_cert_uri(setting),
nm_setting_802_1x_get_phase2_ca_cert_password(setting),
nm_setting_802_1x_get_phase2_ca_cert_password_flags(setting),
error)) {
return FALSE;
}
break;
default:
break;
}
}
if (bytes) {
if (!nm_supplicant_config_add_blob_for_connection(self, bytes, "ca_cert2", con_uuid, error))
return FALSE;
} else if (path) {
/* Private connections cannot use paths other than the system CA store */
g_return_val_if_fail(ca_cert_override || !priv->private_user, FALSE);
if (!add_string_val(self, path, "ca_cert2", FALSE, NULL, error))
return FALSE;
}
/* Subject match */
value = nm_setting_802_1x_get_subject_match(setting);
if (!add_string_val(self, value, "subject_match", FALSE, NULL, error))
return FALSE;
value = nm_setting_802_1x_get_phase2_subject_match(setting);
if (!add_string_val(self, value, "subject_match2", FALSE, NULL, error))
return FALSE;
/* altSubjectName match */
if (!ADD_STRING_LIST_VAL(self,
setting,
802_1x,
altsubject_match,
altsubject_matches,
"altsubject_match",
';',
FALSE,
NULL,
error))
return FALSE;
if (!ADD_STRING_LIST_VAL(self,
setting,
802_1x,
phase2_altsubject_match,
phase2_altsubject_matches,
"altsubject_match2",
';',
FALSE,
NULL,
error))
return FALSE;
/* Domain suffix match */
value = nm_setting_802_1x_get_domain_suffix_match(setting);
if (!add_string_val(self, value, "domain_suffix_match", FALSE, NULL, error))
return FALSE;
value = nm_setting_802_1x_get_phase2_domain_suffix_match(setting);
if (!add_string_val(self, value, "domain_suffix_match2", FALSE, NULL, error))
return FALSE;
/* domain match */
value = nm_setting_802_1x_get_domain_match(setting);
if (!add_string_val(self, value, "domain_match", FALSE, NULL, error))
return FALSE;
value = nm_setting_802_1x_get_phase2_domain_match(setting);
if (!add_string_val(self, value, "domain_match2", FALSE, NULL, error))
return FALSE;
/* Private key */
added = FALSE;
path = NULL;
bytes = NULL;
switch (nm_setting_802_1x_get_private_key_scheme(setting)) {
case NM_SETTING_802_1X_CK_SCHEME_BLOB:
bytes = nm_setting_802_1x_get_private_key_blob(setting);
added = TRUE;
break;
case NM_SETTING_802_1X_CK_SCHEME_PATH:
path = nm_setting_802_1x_get_private_key_path(setting);
if (priv->private_user)
bytes = nm_g_hash_table_lookup(files, path);
added = TRUE;
break;
case NM_SETTING_802_1X_CK_SCHEME_PKCS11:
if (!add_pkcs11_uri_with_pin(self,
"private_key",
nm_setting_802_1x_get_private_key_uri(setting),
nm_setting_802_1x_get_private_key_password(setting),
nm_setting_802_1x_get_private_key_password_flags(setting),
error)) {
return FALSE;
}
added = TRUE;
break;
default:
break;
}
if (bytes) {
if (!nm_supplicant_config_add_blob_for_connection(self,
bytes,
"private_key",
con_uuid,
error))
return FALSE;
} else if (path) {
/* Private connections cannot use paths */
g_return_val_if_fail(!priv->private_user, FALSE);
if (!add_string_val(self, path, "private_key", FALSE, NULL, error))
return FALSE;
}
if (added) {
NMSetting8021xCKFormat format;
NMSetting8021xCKScheme scheme;
format = nm_setting_802_1x_get_private_key_format(setting);
scheme = nm_setting_802_1x_get_private_key_scheme(setting);
if (scheme == NM_SETTING_802_1X_CK_SCHEME_PATH
|| format == NM_SETTING_802_1X_CK_FORMAT_PKCS12) {
/* Only add the private key password for PKCS#12 blobs and
* all path schemes, since in both of these cases the private key
* isn't decrypted at all.
*/
value = nm_setting_802_1x_get_private_key_password(setting);
if (!add_string_val(self, value, "private_key_passwd", FALSE, "<hidden>", error))
return FALSE;
}
if (format != NM_SETTING_802_1X_CK_FORMAT_PKCS12) {
/* Only add the client cert if the private key is not PKCS#12, as
* wpa_supplicant configuration directs us to do.
*/
path = NULL;
bytes = NULL;
switch (nm_setting_802_1x_get_client_cert_scheme(setting)) {
case NM_SETTING_802_1X_CK_SCHEME_BLOB:
bytes = nm_setting_802_1x_get_client_cert_blob(setting);
break;
case NM_SETTING_802_1X_CK_SCHEME_PATH:
path = nm_setting_802_1x_get_client_cert_path(setting);
if (priv->private_user)
bytes = nm_g_hash_table_lookup(files, path);
break;
case NM_SETTING_802_1X_CK_SCHEME_PKCS11:
if (!add_pkcs11_uri_with_pin(
self,
"client_cert",
nm_setting_802_1x_get_client_cert_uri(setting),
nm_setting_802_1x_get_client_cert_password(setting),
nm_setting_802_1x_get_client_cert_password_flags(setting),
error)) {
return FALSE;
}
break;
default:
break;
}
if (bytes) {
if (!nm_supplicant_config_add_blob_for_connection(self,
bytes,
"client_cert",
con_uuid,
error))
return FALSE;
} else if (path) {
/* Private connections cannot use paths */
g_return_val_if_fail(!priv->private_user, FALSE);
if (!add_string_val(self, path, "client_cert", FALSE, NULL, error))
return FALSE;
}
}
}
/* Phase 2 private key */
added = FALSE;
path = NULL;
bytes = NULL;
switch (nm_setting_802_1x_get_phase2_private_key_scheme(setting)) {
case NM_SETTING_802_1X_CK_SCHEME_BLOB:
bytes = nm_setting_802_1x_get_phase2_private_key_blob(setting);
added = TRUE;
break;
case NM_SETTING_802_1X_CK_SCHEME_PATH:
path = nm_setting_802_1x_get_phase2_private_key_path(setting);
if (priv->private_user)
bytes = nm_g_hash_table_lookup(files, path);
added = TRUE;
break;
case NM_SETTING_802_1X_CK_SCHEME_PKCS11:
if (!add_pkcs11_uri_with_pin(
self,
"private_key2",
nm_setting_802_1x_get_phase2_private_key_uri(setting),
nm_setting_802_1x_get_phase2_private_key_password(setting),
nm_setting_802_1x_get_phase2_private_key_password_flags(setting),
error)) {
return FALSE;
}
added = TRUE;
break;
default:
break;
}
if (bytes) {
if (!nm_supplicant_config_add_blob_for_connection(self,
bytes,
"private_key2",
con_uuid,
error))
return FALSE;
} else if (path) {
/* Private connections cannot use paths */
g_return_val_if_fail(!priv->private_user, FALSE);
if (!add_string_val(self, path, "private_key2", FALSE, NULL, error))
return FALSE;
}
if (added) {
NMSetting8021xCKFormat format;
NMSetting8021xCKScheme scheme;
format = nm_setting_802_1x_get_phase2_private_key_format(setting);
scheme = nm_setting_802_1x_get_phase2_private_key_scheme(setting);
if (scheme == NM_SETTING_802_1X_CK_SCHEME_PATH
|| format == NM_SETTING_802_1X_CK_FORMAT_PKCS12) {
/* Only add the private key password for PKCS#12 blobs and
* all path schemes, since in both of these cases the private key
* isn't decrypted at all.
*/
value = nm_setting_802_1x_get_phase2_private_key_password(setting);
if (!add_string_val(self, value, "private_key2_passwd", FALSE, "<hidden>", error))
return FALSE;
}
if (format != NM_SETTING_802_1X_CK_FORMAT_PKCS12) {
/* Only add the client cert if the private key is not PKCS#12, as
* wpa_supplicant configuration directs us to do.
*/
path = NULL;
bytes = NULL;
switch (nm_setting_802_1x_get_phase2_client_cert_scheme(setting)) {
case NM_SETTING_802_1X_CK_SCHEME_BLOB:
bytes = nm_setting_802_1x_get_phase2_client_cert_blob(setting);
break;
case NM_SETTING_802_1X_CK_SCHEME_PATH:
path = nm_setting_802_1x_get_phase2_client_cert_path(setting);
if (priv->private_user)
bytes = nm_g_hash_table_lookup(files, path);
break;
case NM_SETTING_802_1X_CK_SCHEME_PKCS11:
if (!add_pkcs11_uri_with_pin(
self,
"client_cert2",
nm_setting_802_1x_get_phase2_client_cert_uri(setting),
nm_setting_802_1x_get_phase2_client_cert_password(setting),
nm_setting_802_1x_get_phase2_client_cert_password_flags(setting),
error)) {
return FALSE;
}
break;
default:
break;
}
if (bytes) {
if (!nm_supplicant_config_add_blob_for_connection(self,
bytes,
"client_cert2",
con_uuid,
error))
return FALSE;
} else if (path) {
/* Private connections cannot use paths */
g_return_val_if_fail(!priv->private_user, FALSE);
if (!add_string_val(self, path, "client_cert2", FALSE, NULL, error))
return FALSE;
}
}
}
value = nm_setting_802_1x_get_identity(setting);
if (!add_string_val(self, value, "identity", FALSE, NULL, error))
return FALSE;
value = nm_setting_802_1x_get_anonymous_identity(setting);
if (!add_string_val(self, value, "anonymous_identity", FALSE, NULL, error))
return FALSE;
value = nm_setting_802_1x_get_openssl_ciphers(setting);
if (value && !add_string_val(self, value, "openssl_ciphers", FALSE, NULL, error))
return FALSE;
return TRUE;
}
gboolean
nm_supplicant_config_add_no_security(NMSupplicantConfig *self, GError **error)
{
return nm_supplicant_config_add_option(self, "key_mgmt", "NONE", -1, NULL, error);
}
gboolean
nm_supplicant_config_get_ap_isolation(NMSupplicantConfig *self)
{
return self->_priv.ap_isolation;
}
void
nm_supplicant_config_set_ap_isolation(NMSupplicantConfig *self, gboolean ap_isolation)
{
self->_priv.ap_isolation = ap_isolation;
}