From e74cf8fcc49d5f602a22014cb88c29b9ff5e9bee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=8D=C3=B1igo=20Huguet?= Date: Tue, 18 Nov 2025 10:39:49 +0100 Subject: [PATCH 01/56] libnm: move hsr symbols to the right version These symbols has been added to the 1.54.2 stable branch, so they are actually available since then. (cherry picked from commit d687768c610a9574a00813a564aaf382fd5e46fa) --- src/libnm-client-impl/libnm.ver | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/libnm-client-impl/libnm.ver b/src/libnm-client-impl/libnm.ver index ca7a4c300e..1fb3282d82 100644 --- a/src/libnm-client-impl/libnm.ver +++ b/src/libnm-client-impl/libnm.ver @@ -2077,13 +2077,17 @@ global: nm_sriov_preserve_on_down_get_type; } libnm_1_52_0; +libnm_1_54_2 { +global: + nm_setting_hsr_get_interlink; + nm_setting_hsr_get_protocol_version; + nm_setting_hsr_protocol_version_get_type; +} libnm_1_54_0; + libnm_1_56_0 { global: nm_dns_server_validate; nm_setting_gsm_get_device_uid; - nm_setting_hsr_get_interlink; - nm_setting_hsr_get_protocol_version; - nm_setting_hsr_protocol_version_get_type; nm_setting_connection_get_dnssec; nm_setting_connection_dnssec_get_type; } libnm_1_54_0; From f0cdf16e1d9d299baceecb071e2b2b00ab27c5a9 Mon Sep 17 00:00:00 2001 From: Beniamino Galvani Date: Tue, 18 Nov 2025 13:51:26 +0100 Subject: [PATCH 02/56] core: fix rate-limit test failures It's possible that the first timeout gets delayed; therefore the interval between the first and the second callback can be less than one second, and the budget doesn't refill completely. Schedule the second timeout from the first callback to guarantee that at least one second passes between the callbacks. Fixes: ff0c4346fc0c ('core: add rate-limiting helper') (cherry picked from commit 3b10b88290da6a5e3835849baffcc847b7693c08) --- src/core/tests/test-utils.c | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/src/core/tests/test-utils.c b/src/core/tests/test-utils.c index ec760cf171..0418d3ae89 100644 --- a/src/core/tests/test-utils.c +++ b/src/core/tests/test-utils.c @@ -264,8 +264,7 @@ test_shorten_hostname(void) typedef struct { NMRateLimit ratelimit; GMainLoop *loop; - GSource *source1; - GSource *source2; + GSource *source; guint num; } RateLimitData; @@ -283,10 +282,9 @@ rate_limit_window_expire_cb(gpointer user_data) g_assert(!nm_rate_limit_check(&data->ratelimit, 1, 5)); g_assert(!nm_rate_limit_check(&data->ratelimit, 1, 5)); + nm_clear_g_source_inst(&data->source); g_main_loop_quit(data->loop); - nm_clear_g_source_inst(&data->source1); - return G_SOURCE_CONTINUE; } @@ -304,7 +302,8 @@ rate_limit_check_cb(gpointer user_data) g_assert(!nm_rate_limit_check(&data->ratelimit, 1, 5)); g_assert(!nm_rate_limit_check(&data->ratelimit, 1, 5)); - nm_clear_g_source_inst(&data->source2); + nm_clear_g_source_inst(&data->source); + data->source = nm_g_timeout_add_source(1000, rate_limit_window_expire_cb, data); return G_SOURCE_CONTINUE; } @@ -317,12 +316,10 @@ test_rate_limit_check(void) data = (RateLimitData) { .loop = g_main_loop_new(NULL, FALSE), .ratelimit = {}, + .source = nm_g_timeout_add_source(1, rate_limit_check_cb, &data), .num = 0, }; - data.source1 = nm_g_timeout_add_source(1100, rate_limit_window_expire_cb, &data); - data.source2 = nm_g_timeout_add_source(10, rate_limit_check_cb, &data); - g_main_loop_run(data.loop); g_main_loop_unref(data.loop); } From e3f20ecf95475050f816cdee405d808e091cc7e0 Mon Sep 17 00:00:00 2001 From: "Matthieu Baerts (NGI0)" Date: Thu, 13 Nov 2025 19:19:07 +0100 Subject: [PATCH 03/56] mptcp: add 'laminar' endpoint support This new endpoint type has been recently added to the kernel in v6.18 [1]. It will be used to create new subflows from the associated address to additional addresses announced by the other peer. This will be done if allowed by the MPTCP limits, and if the associated address is not already being used by another subflow from the same MPTCP connection. Note that the fullmesh flag takes precedence over the laminar one. Without any of these two flags, the path-manager will create new subflows to additional addresses announced by the other peer by selecting the source address from the routing tables, which is harder to configure if the announced address is not known in advance. The support of the new flag is easy: simply by declaring a new flag for NM, and adding it in the related helpers and existing checks looking at the different MPTCP endpoint. The documentation now references the new endpoint type. Note that only the new 'define' has been added in the Linux header file: this file has changed a bit since the last sync, now split in two files. Only this new line is needed, so the minimum has been modified here. Link: https://git.kernel.org/torvalds/c/539f6b9de39e [1] Signed-off-by: Matthieu Baerts (NGI0) (cherry picked from commit 2b03057de0dac8182c2537d9fd9af9a9015fb631) --- src/core/nm-l3cfg.c | 4 ++-- src/libnm-core-aux-intern/nm-libnm-core-utils.h | 11 ++++++----- src/libnm-core-impl/nm-setting-connection.c | 5 +++-- src/libnm-core-public/nm-dbus-interface.h | 11 +++++++++++ src/libnm-platform/nm-linux-platform.c | 1 + src/libnm-platform/nm-platform.c | 3 ++- src/libnm-platform/nm-platform.h | 1 + src/libnmc-setting/settings-docs.h.in | 2 +- src/linux-headers/mptcp.h | 1 + src/nmcli/gen-metadata-nm-settings-nmcli.xml.in | 4 ++-- 10 files changed, 30 insertions(+), 13 deletions(-) diff --git a/src/core/nm-l3cfg.c b/src/core/nm-l3cfg.c index d2ec113a53..0d93f76bc2 100644 --- a/src/core/nm-l3cfg.c +++ b/src/core/nm-l3cfg.c @@ -5054,8 +5054,8 @@ _l3_commit_mptcp_af(NML3Cfg *self, (NM_FLAGS_HAS(mptcp_flags, NM_MPTCP_FLAGS_SIGNAL) ? MPTCP_PM_ADDR_FLAG_SIGNAL : 0) | (NM_FLAGS_HAS(mptcp_flags, NM_MPTCP_FLAGS_SUBFLOW) ? MPTCP_PM_ADDR_FLAG_SUBFLOW : 0) | (NM_FLAGS_HAS(mptcp_flags, NM_MPTCP_FLAGS_BACKUP) ? MPTCP_PM_ADDR_FLAG_BACKUP : 0) - | (NM_FLAGS_HAS(mptcp_flags, NM_MPTCP_FLAGS_FULLMESH) ? MPTCP_PM_ADDR_FLAG_FULLMESH - : 0); + | (NM_FLAGS_HAS(mptcp_flags, NM_MPTCP_FLAGS_FULLMESH) ? MPTCP_PM_ADDR_FLAG_FULLMESH : 0) + | (NM_FLAGS_HAS(mptcp_flags, NM_MPTCP_FLAGS_LAMINAR) ? MPTCP_PM_ADDR_FLAG_LAMINAR : 0); NMPlatformMptcpAddr a = { .ifindex = self->priv.ifindex, .id = 0, diff --git a/src/libnm-core-aux-intern/nm-libnm-core-utils.h b/src/libnm-core-aux-intern/nm-libnm-core-utils.h index 8f30b95820..432a21409f 100644 --- a/src/libnm-core-aux-intern/nm-libnm-core-utils.h +++ b/src/libnm-core-aux-intern/nm-libnm-core-utils.h @@ -300,11 +300,12 @@ gpointer _nm_connection_new_setting(NMConnection *connection, GType gtype); /*****************************************************************************/ -#define _NM_MPTCP_FLAGS_ALL \ - ((NMMptcpFlags) (NM_MPTCP_FLAGS_DISABLED | NM_MPTCP_FLAGS_ENABLED \ - | NM_MPTCP_FLAGS_ALSO_WITHOUT_SYSCTL \ - | NM_MPTCP_FLAGS_ALSO_WITHOUT_DEFAULT_ROUTE | NM_MPTCP_FLAGS_SIGNAL \ - | NM_MPTCP_FLAGS_SUBFLOW | NM_MPTCP_FLAGS_BACKUP | NM_MPTCP_FLAGS_FULLMESH)) +#define _NM_MPTCP_FLAGS_ALL \ + ((NMMptcpFlags) (NM_MPTCP_FLAGS_DISABLED | NM_MPTCP_FLAGS_ENABLED \ + | NM_MPTCP_FLAGS_ALSO_WITHOUT_SYSCTL \ + | NM_MPTCP_FLAGS_ALSO_WITHOUT_DEFAULT_ROUTE | NM_MPTCP_FLAGS_SIGNAL \ + | NM_MPTCP_FLAGS_SUBFLOW | NM_MPTCP_FLAGS_BACKUP | NM_MPTCP_FLAGS_FULLMESH \ + | NM_MPTCP_FLAGS_LAMINAR)) #define _NM_MPTCP_FLAGS_DEFAULT ((NMMptcpFlags) (NM_MPTCP_FLAGS_ENABLED | NM_MPTCP_FLAGS_SUBFLOW)) diff --git a/src/libnm-core-impl/nm-setting-connection.c b/src/libnm-core-impl/nm-setting-connection.c index f0b1c96512..4f25533c07 100644 --- a/src/libnm-core-impl/nm-setting-connection.c +++ b/src/libnm-core-impl/nm-setting-connection.c @@ -3458,7 +3458,7 @@ nm_setting_connection_class_init(NMSettingConnectionClass *klass) * - "disabled", "disabled-on-local-iface", "enable": whether MPTCP handling * is enabled. The flag "disabled-on-local-iface" enables it based on whether * the interface has a default route. - * - "signal", "subflow", "backup", "fullmesh": the endpoint flags + * - "signal", "subflow", "backup", "fullmesh", "laminar": the endpoint flags * that are used. * * The reason is, that it is useful to have one "connection.mptcp-flags" @@ -3518,7 +3518,8 @@ nm_setting_connection_class_init(NMSettingConnectionClass *klass) * * When MPTCP handling is enabled then endpoints are configured with * the specified address flags "signal" (0x10), "subflow" (0x20), "backup" (0x40), - * "fullmesh" (0x80). See ip-mptcp(8) manual for additional information about the flags. + * "fullmesh" (0x80), "laminar" (0x100). See ip-mptcp(8) manual for + * additional information about the flags. * * If the flags are zero (0x0), the global connection default from NetworkManager.conf is * honored. If still unspecified, the fallback is "enabled,subflow". diff --git a/src/libnm-core-public/nm-dbus-interface.h b/src/libnm-core-public/nm-dbus-interface.h index 8db76db1f4..42bff04ae0 100644 --- a/src/libnm-core-public/nm-dbus-interface.h +++ b/src/libnm-core-public/nm-dbus-interface.h @@ -1460,6 +1460,16 @@ typedef enum /*< flags >*/ { * any additional addresses using the MPTCP ADD_ADDR sub-option, this will behave the same * as a plain subflow endpoint. When the peer does announce addresses, each received ADD_ADDR * sub-option will trigger creation of an additional subflow to generate a full mesh topology. + * @NM_MPTCP_FLAGS_LAMINAR: Flag for the MPTCP endpoint. The endpoint will be + * used to create new subflows from the associated address to additional + * addresses announced by the other peer. This will be done if allowed by the + * MPTCP limits, and if the associated address is not already being used by + * another subflow from the same MPTCP connection. Note that the 'fullmesh' + * flag takes precedence over the 'laminar' one. Without any of these two + * flags, the path-manager will create new subflows to additional addresses + * announced by the other peer by selecting the source address from the + * routing tables, which is harder to configure if the announced address is + * not known in advance. Since: 1.56 * * Since: 1.40 */ @@ -1476,6 +1486,7 @@ typedef enum /*< flags >*/ { NM_MPTCP_FLAGS_SUBFLOW = 0x20, NM_MPTCP_FLAGS_BACKUP = 0x40, NM_MPTCP_FLAGS_FULLMESH = 0x80, + NM_MPTCP_FLAGS_LAMINAR = 0x100, } NMMptcpFlags; /* For secrets requests, hints starting with "x-vpn-message:" are a message to show, not diff --git a/src/libnm-platform/nm-linux-platform.c b/src/libnm-platform/nm-linux-platform.c index e703774388..cc5b99e095 100644 --- a/src/libnm-platform/nm-linux-platform.c +++ b/src/libnm-platform/nm-linux-platform.c @@ -59,6 +59,7 @@ G_STATIC_ASSERT(NM_MPTCP_PM_ADDR_FLAG_SUBFLOW == MPTCP_PM_ADDR_FLAG_SUBFLOW); G_STATIC_ASSERT(NM_MPTCP_PM_ADDR_FLAG_BACKUP == MPTCP_PM_ADDR_FLAG_BACKUP); G_STATIC_ASSERT(NM_MPTCP_PM_ADDR_FLAG_FULLMESH == MPTCP_PM_ADDR_FLAG_FULLMESH); G_STATIC_ASSERT(NM_MPTCP_PM_ADDR_FLAG_IMPLICIT == MPTCP_PM_ADDR_FLAG_IMPLICIT); +G_STATIC_ASSERT(NM_MPTCP_PM_ADDR_FLAG_LAMINAR == MPTCP_PM_ADDR_FLAG_LAMINAR); /*****************************************************************************/ diff --git a/src/libnm-platform/nm-platform.c b/src/libnm-platform/nm-platform.c index d7fb59b079..853157d204 100644 --- a/src/libnm-platform/nm-platform.c +++ b/src/libnm-platform/nm-platform.c @@ -7982,7 +7982,8 @@ static NM_UTILS_FLAGS2STR_DEFINE(_mptcp_flags_to_string, NM_UTILS_FLAGS2STR(NM_MPTCP_PM_ADDR_FLAG_SIGNAL, "signal"), NM_UTILS_FLAGS2STR(NM_MPTCP_PM_ADDR_FLAG_SUBFLOW, "subflow"), NM_UTILS_FLAGS2STR(NM_MPTCP_PM_ADDR_FLAG_BACKUP, "backup"), - NM_UTILS_FLAGS2STR(NM_MPTCP_PM_ADDR_FLAG_FULLMESH, "fullmesh")); + NM_UTILS_FLAGS2STR(NM_MPTCP_PM_ADDR_FLAG_FULLMESH, "fullmesh"), + NM_UTILS_FLAGS2STR(NM_MPTCP_PM_ADDR_FLAG_LAMINAR, "laminar")); const char * nm_platform_mptcp_addr_to_string(const NMPlatformMptcpAddr *mptcp_addr, char *buf, gsize len) diff --git a/src/libnm-platform/nm-platform.h b/src/libnm-platform/nm-platform.h index 9c8425b883..033dc51541 100644 --- a/src/libnm-platform/nm-platform.h +++ b/src/libnm-platform/nm-platform.h @@ -46,6 +46,7 @@ typedef gboolean (*NMPObjectPredicateFunc)(const NMPObject *obj, gpointer user_d #define NM_MPTCP_PM_ADDR_FLAG_BACKUP ((guint32) (1 << 2)) #define NM_MPTCP_PM_ADDR_FLAG_FULLMESH ((guint32) (1 << 3)) #define NM_MPTCP_PM_ADDR_FLAG_IMPLICIT ((guint32) (1 << 4)) +#define NM_MPTCP_PM_ADDR_FLAG_LAMINAR ((guint32) (1 << 5)) /* Redefine this in host's endianness */ #define NM_GRE_KEY 0x2000 diff --git a/src/libnmc-setting/settings-docs.h.in b/src/libnmc-setting/settings-docs.h.in index e4ed5f6ba9..4e8978cb20 100644 --- a/src/libnmc-setting/settings-docs.h.in +++ b/src/libnmc-setting/settings-docs.h.in @@ -21,7 +21,7 @@ #define DESCRIBE_DOC_NM_SETTING_CONNECTION_MASTER N_("Interface name of the controller device or UUID of the controller connection. Deprecated 1.46. Use \"controller\" instead, this is just an alias.") #define DESCRIBE_DOC_NM_SETTING_CONNECTION_MDNS N_("Whether mDNS is enabled for the connection. The permitted values are: \"yes\" (2) register hostname and resolving for the connection, \"no\" (0) disable mDNS for the interface, \"resolve\" (1) do not register hostname but allow resolving of mDNS host names and \"default\" (-1) to allow lookup of a global default in NetworkManager.conf. If unspecified, \"default\" ultimately depends on the DNS plugin. This feature requires a plugin which supports mDNS. Otherwise, the setting has no effect. Currently the only supported DNS plugin is systemd-resolved. For systemd-resolved, the default is configurable via MulticastDNS= setting in resolved.conf.") #define DESCRIBE_DOC_NM_SETTING_CONNECTION_METERED N_("Whether the connection is metered. When updating this property on a currently activated connection, the change takes effect immediately.") -#define DESCRIBE_DOC_NM_SETTING_CONNECTION_MPTCP_FLAGS N_("Whether to configure MPTCP endpoints and the address flags. If MPTCP is enabled in NetworkManager, it will configure the addresses of the interface as MPTCP endpoints. Note that IPv4 loopback addresses (127.0.0.0/8), IPv4 link local addresses (169.254.0.0/16), the IPv6 loopback address (::1), IPv6 link local addresses (fe80::/10), IPv6 unique local addresses (ULA, fc00::/7) and IPv6 privacy extension addresses (rfc3041, ipv6.ip6-privacy) will be excluded from being configured as endpoints. If \"disabled\" (0x1), MPTCP handling for the interface is disabled and no endpoints are registered. The \"enabled\" (0x2) flag means that MPTCP handling is enabled. This flag can also be implied from the presence of other flags. Even when enabled, MPTCP handling will by default still be disabled unless \"/proc/sys/net/mptcp/enabled\" sysctl is on. NetworkManager does not change the sysctl and this is up to the administrator or distribution. To configure endpoints even if the sysctl is disabled, \"also-without-sysctl\" (0x4) flag can be used. In that case, NetworkManager doesn't look at the sysctl and configures endpoints regardless. Even when enabled, NetworkManager will only configure MPTCP endpoints for a certain address family, if there is a unicast default route (0.0.0.0/0 or ::/0) in the main routing table. The flag \"also-without-default-route\" (0x8) can override that. When MPTCP handling is enabled then endpoints are configured with the specified address flags \"signal\" (0x10), \"subflow\" (0x20), \"backup\" (0x40), \"fullmesh\" (0x80). See ip-mptcp(8) manual for additional information about the flags. If the flags are zero (0x0), the global connection default from NetworkManager.conf is honored. If still unspecified, the fallback is \"enabled,subflow\". Note that this means that MPTCP is by default done depending on the \"/proc/sys/net/mptcp/enabled\" sysctl. NetworkManager does not change the MPTCP limits nor enable MPTCP via \"/proc/sys/net/mptcp/enabled\". That is a host configuration which the admin can change via sysctl and ip-mptcp. Strict reverse path filtering (rp_filter) breaks many MPTCP use cases, so when MPTCP handling for IPv4 addresses on the interface is enabled, NetworkManager would loosen the strict reverse path filtering (1) to the loose setting (2).") +#define DESCRIBE_DOC_NM_SETTING_CONNECTION_MPTCP_FLAGS N_("Whether to configure MPTCP endpoints and the address flags. If MPTCP is enabled in NetworkManager, it will configure the addresses of the interface as MPTCP endpoints. Note that IPv4 loopback addresses (127.0.0.0/8), IPv4 link local addresses (169.254.0.0/16), the IPv6 loopback address (::1), IPv6 link local addresses (fe80::/10), IPv6 unique local addresses (ULA, fc00::/7) and IPv6 privacy extension addresses (rfc3041, ipv6.ip6-privacy) will be excluded from being configured as endpoints. If \"disabled\" (0x1), MPTCP handling for the interface is disabled and no endpoints are registered. The \"enabled\" (0x2) flag means that MPTCP handling is enabled. This flag can also be implied from the presence of other flags. Even when enabled, MPTCP handling will by default still be disabled unless \"/proc/sys/net/mptcp/enabled\" sysctl is on. NetworkManager does not change the sysctl and this is up to the administrator or distribution. To configure endpoints even if the sysctl is disabled, \"also-without-sysctl\" (0x4) flag can be used. In that case, NetworkManager doesn't look at the sysctl and configures endpoints regardless. Even when enabled, NetworkManager will only configure MPTCP endpoints for a certain address family, if there is a unicast default route (0.0.0.0/0 or ::/0) in the main routing table. The flag \"also-without-default-route\" (0x8) can override that. When MPTCP handling is enabled then endpoints are configured with the specified address flags \"signal\" (0x10), \"subflow\" (0x20), \"backup\" (0x40), \"fullmesh\" (0x80), \"laminar\" (0x100). See ip-mptcp(8) manual for additional information about the flags. If the flags are zero (0x0), the global connection default from NetworkManager.conf is honored. If still unspecified, the fallback is \"enabled,subflow\". Note that this means that MPTCP is by default done depending on the \"/proc/sys/net/mptcp/enabled\" sysctl. NetworkManager does not change the MPTCP limits nor enable MPTCP via \"/proc/sys/net/mptcp/enabled\". That is a host configuration which the admin can change via sysctl and ip-mptcp. Strict reverse path filtering (rp_filter) breaks many MPTCP use cases, so when MPTCP handling for IPv4 addresses on the interface is enabled, NetworkManager would loosen the strict reverse path filtering (1) to the loose setting (2).") #define DESCRIBE_DOC_NM_SETTING_CONNECTION_MUD_URL N_("If configured, set to a Manufacturer Usage Description (MUD) URL that points to manufacturer-recommended network policies for IoT devices. It is transmitted as a DHCPv4 or DHCPv6 option. The value must be a valid URL starting with \"https://\". The special value \"none\" is allowed to indicate that no MUD URL is used. If the per-profile value is unspecified (the default), a global connection default gets consulted. If still unspecified, the ultimate default is \"none\".") #define DESCRIBE_DOC_NM_SETTING_CONNECTION_MULTI_CONNECT N_("Specifies whether the profile can be active multiple times at a particular moment. The value is of type NMConnectionMultiConnect.") #define DESCRIBE_DOC_NM_SETTING_CONNECTION_PERMISSIONS N_("An array of strings defining what access a given user has to this connection. If this is NULL or empty, all users are allowed to access this connection; otherwise users are allowed if and only if they are in this list. When this is not empty, the connection can be active only when one of the specified users is logged into an active session. Each entry is of the form \"[type]:[id]:[reserved]\"; for example, \"user:dcbw:blah\". At this time only the \"user\" [type] is allowed. Any other values are ignored and reserved for future use. [id] is the username that this permission refers to, which may not contain the \":\" character. Any [reserved] information present must be ignored and is reserved for future use. All of [type], [id], and [reserved] must be valid UTF-8.") diff --git a/src/linux-headers/mptcp.h b/src/linux-headers/mptcp.h index ca502f1edc..ed44b9509b 100644 --- a/src/linux-headers/mptcp.h +++ b/src/linux-headers/mptcp.h @@ -80,6 +80,7 @@ enum { #define MPTCP_PM_ADDR_FLAG_BACKUP (1 << 2) #define MPTCP_PM_ADDR_FLAG_FULLMESH (1 << 3) #define MPTCP_PM_ADDR_FLAG_IMPLICIT (1 << 4) +#define MPTCP_PM_ADDR_FLAG_LAMINAR (1 << 5) enum { MPTCP_PM_CMD_UNSPEC, diff --git a/src/nmcli/gen-metadata-nm-settings-nmcli.xml.in b/src/nmcli/gen-metadata-nm-settings-nmcli.xml.in index 9f3f0ef383..949bc6804e 100644 --- a/src/nmcli/gen-metadata-nm-settings-nmcli.xml.in +++ b/src/nmcli/gen-metadata-nm-settings-nmcli.xml.in @@ -738,9 +738,9 @@ format="choice (NMSettingConnectionDnssec)" values="default (-1), no (0), allow-downgrade (1), yes (2)" /> + values="none/default (0x0), disabled (0x1), enabled (0x2), also-without-sysctl (0x4), also-without-default-route (0x8), signal (0x10), subflow (0x20), backup (0x40), fullmesh (0x80), laminar (0x100)" /> From 12a4696229a7e6cc33a28fd0d6376a4547cdc93b Mon Sep 17 00:00:00 2001 From: "Matthieu Baerts (NGI0)" Date: Thu, 13 Nov 2025 19:19:22 +0100 Subject: [PATCH 04/56] mptcp: set the laminar flag by default By default, the MPTCP limits have 'add_addr_accepted' set to 0. It means that when the other peer announces an additional address it can be reached from, the receiver will not try to establish any new subflows to this address. If this limit is increased, and without the new 'laminar' flag, the MPTCP in-kernel path-manager will select the source address by looking at the routing tables to establish this new subflow. This is not ideal: very likely, the source address will be the one linked to the default route and a new subflow from the same interface as the initial one will be created instead of using another path. This is especially problematic when the other peer has set the 'C-flag' in the MPTCP connection request (MP_CAPABLE). This flag can be set to tell the other side that the peer will not accept extra subflows requests sent to its initial IP address and port: typically set by a server using an anycast address, behind a legacy Layer 4 load balancer. It sounds better to add the 'laminar' flag by default to pick the source address from well-defined MPTCP endpoints, rather than relying on routing rules which will likely not pick the most interesting solution. Note that older kernels will accept unsupported flags, and ignore them. So it is fine to have the new flag added by default even if it is not supported. Signed-off-by: Matthieu Baerts (NGI0) (cherry picked from commit 8caa781270e0427575381d9815dfb424ee5d0221) --- man/NetworkManager.conf.xml | 2 +- src/libnm-core-aux-intern/nm-libnm-core-utils.h | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/man/NetworkManager.conf.xml b/man/NetworkManager.conf.xml index 65cad02cf9..6c055e9d2e 100644 --- a/man/NetworkManager.conf.xml +++ b/man/NetworkManager.conf.xml @@ -895,7 +895,7 @@ ipv6.ip6-privacy=0 connection.mptcp-flags - If unspecified, the fallback is 0x22 ("enabled,subflow"). Note that if sysctl /proc/sys/net/mptcp/enabled is disabled, NetworkManager will still not configure endpoints. + If unspecified, the fallback is 0x122 ("enabled,subflow,laminar"). Note that if sysctl /proc/sys/net/mptcp/enabled is disabled, NetworkManager will still not configure endpoints. connection.dns-over-tls diff --git a/src/libnm-core-aux-intern/nm-libnm-core-utils.h b/src/libnm-core-aux-intern/nm-libnm-core-utils.h index 432a21409f..0e75982ecc 100644 --- a/src/libnm-core-aux-intern/nm-libnm-core-utils.h +++ b/src/libnm-core-aux-intern/nm-libnm-core-utils.h @@ -307,7 +307,8 @@ gpointer _nm_connection_new_setting(NMConnection *connection, GType gtype); | NM_MPTCP_FLAGS_SUBFLOW | NM_MPTCP_FLAGS_BACKUP | NM_MPTCP_FLAGS_FULLMESH \ | NM_MPTCP_FLAGS_LAMINAR)) -#define _NM_MPTCP_FLAGS_DEFAULT ((NMMptcpFlags) (NM_MPTCP_FLAGS_ENABLED | NM_MPTCP_FLAGS_SUBFLOW)) +#define _NM_MPTCP_FLAGS_DEFAULT \ + ((NMMptcpFlags) (NM_MPTCP_FLAGS_ENABLED | NM_MPTCP_FLAGS_SUBFLOW | NM_MPTCP_FLAGS_LAMINAR)) NMMptcpFlags nm_mptcp_flags_normalize(NMMptcpFlags flags); From 380cd0d24896553e0ed8596ca069f043537d87d1 Mon Sep 17 00:00:00 2001 From: "Matthieu Baerts (NGI0)" Date: Tue, 18 Nov 2025 14:38:28 +0100 Subject: [PATCH 05/56] NEWS: new MPTCP 'laminar' endpoint & default A summary linked to the last two commits. Signed-off-by: Matthieu Baerts (NGI0) (cherry picked from commit 3ce1da1fd22bc50d3aea8e451c90159a94292874) --- NEWS | 2 ++ 1 file changed, 2 insertions(+) diff --git a/NEWS b/NEWS index b8b9212eb3..9e2d0cc15d 100644 --- a/NEWS +++ b/NEWS @@ -34,6 +34,8 @@ USE AT YOUR OWN RISK. NOT RECOMMENDED FOR PRODUCTION USE! connection.mptcp-flags, ipv6.ip6-privacy) * Update n-acd to always compile with eBPF enabled, as support for eBPF is now detected at run time. +* Add new MPTCP 'laminar' endpoint type, and set it by default alongside + the 'subflow' one. ============================================= NetworkManager-1.54 From 4610511bcd0596ec9d944729e916e0a6b350a8bc Mon Sep 17 00:00:00 2001 From: Popax21 Date: Mon, 17 Nov 2025 04:52:23 +0100 Subject: [PATCH 06/56] core: restrict connectivity check lookups to per-link DNS if available Restrict connectivity check DNS lookups to just the relevant link if the link has a per-link DNS resolver configured. This change was previously discussed as part of issue https://gitlab.freedesktop.org/NetworkManager/NetworkManager/-/issues/1836, and brings NM's behavior back in line with the behavior documented in the man page. The connectivity check checks for a per-link DNS resolver by querying systemd-resolved's `ScopeMask` for the link; this involves a small D-Bus roundtrip, but is ultimately the more flexible solution since it is also capable of dealing with per-link DNS configuration stemming from other sources. Fixes: e6dac4f0b67e ('core: don't restrict DNS interface when performing connectivity check') (cherry picked from commit 6e2de1d2b3118e817f930e19e0ea15cde50e3858) --- src/core/nm-connectivity.c | 134 ++++++++++++++++++++++++++++++++----- 1 file changed, 118 insertions(+), 16 deletions(-) diff --git a/src/core/nm-connectivity.c b/src/core/nm-connectivity.c index 2aa22331ea..c7915fd962 100644 --- a/src/core/nm-connectivity.c +++ b/src/core/nm-connectivity.c @@ -77,6 +77,8 @@ struct _NMConnectivityCheckHandle { ConConfig *con_config; GCancellable *resolve_cancellable; + int resolve_ifindex; + GDBusConnection *dbus_connection; CURLM *curl_mhandle; CURL *curl_ehandle; struct curl_slist *request_headers; @@ -953,6 +955,113 @@ systemd_resolved_resolve_cb(GObject *object, GAsyncResult *res, gpointer user_da do_curl_request(cb_data, nm_str_buf_get_str(&strbuf_hosts)); } +static void +systemd_resolved_resolve(NMConnectivityCheckHandle *cb_data) +{ + _LOG2D("start request to '%s' (try resolving '%s' using systemd-resolved with ifindex %d)", + cb_data->concheck.con_config->uri, + cb_data->concheck.con_config->host, + cb_data->concheck.resolve_ifindex); + + g_dbus_connection_call(cb_data->concheck.dbus_connection, + "org.freedesktop.resolve1", + "/org/freedesktop/resolve1", + "org.freedesktop.resolve1.Manager", + "ResolveHostname", + g_variant_new("(isit)", + (gint32) cb_data->concheck.resolve_ifindex, + cb_data->concheck.con_config->host, + (gint32) cb_data->addr_family, + SD_RESOLVED_DNS), + G_VARIANT_TYPE("(a(iiay)st)"), + G_DBUS_CALL_FLAGS_NONE, + -1, + cb_data->concheck.resolve_cancellable, + systemd_resolved_resolve_cb, + cb_data); +} + +static void +systemd_resolved_link_scopes_cb(GObject *object, GAsyncResult *res, gpointer user_data) +{ + NMConnectivityCheckHandle *cb_data; + gs_unref_variant GVariant *result = NULL; + gs_unref_variant GVariant *value = NULL; + gs_free_error GError *error = NULL; + guint64 scope_mask = 0; + + result = g_dbus_connection_call_finish(G_DBUS_CONNECTION(object), res, &error); + if (nm_utils_error_is_cancelled(error)) + return; + + cb_data = user_data; + + if (!result) { + _LOG2D("unable to obtain systemd-resolved link ScopesMask for interface %d: %s", + cb_data->concheck.resolve_ifindex, + error->message); + + cb_data->concheck.resolve_ifindex = 0; + systemd_resolved_resolve(cb_data); + return; + } + + g_variant_get(result, "(v)", &value); + g_variant_get(value, "t", &scope_mask); + + if (!(scope_mask & SD_RESOLVED_DNS)) { + /* there is no per-link DNS configured / active; query all available / + * system DNS resolvers instead of restricting the lookup to just this + * one, which would turn up no results. */ + _LOG2D("no per-link DNS available (scope mask %" G_GUINT64_FORMAT + "); falling back to system-wide lookups", + scope_mask); + cb_data->concheck.resolve_ifindex = 0; + } + + systemd_resolved_resolve(cb_data); +} + +static void +systemd_resolved_get_link_cb(GObject *object, GAsyncResult *res, gpointer user_data) +{ + NMConnectivityCheckHandle *cb_data; + gs_unref_variant GVariant *result = NULL; + gs_free char *link_path = NULL; + gs_free_error GError *error = NULL; + + result = g_dbus_connection_call_finish(G_DBUS_CONNECTION(object), res, &error); + if (nm_utils_error_is_cancelled(error)) + return; + + cb_data = user_data; + + if (!result) { + _LOG2D("unable to obtain systemd-resolved link D-Bus object for interface %d: %s", + cb_data->concheck.resolve_ifindex, + error->message); + + cb_data->concheck.resolve_ifindex = 0; + systemd_resolved_resolve(cb_data); + return; + } + + g_variant_get(result, "(o)", &link_path); + + g_dbus_connection_call(cb_data->concheck.dbus_connection, + "org.freedesktop.resolve1", + link_path, + "org.freedesktop.DBus.Properties", + "Get", + g_variant_new("(ss)", "org.freedesktop.resolve1.Link", "ScopesMask"), + G_VARIANT_TYPE("(v)"), + G_DBUS_CALL_FLAGS_NONE, + -1, + cb_data->concheck.resolve_cancellable, + systemd_resolved_link_scopes_cb, + cb_data); +} + static NMConnectivityState check_platform_config(NMConnectivity *self, NMPlatform *platform, @@ -1067,6 +1176,7 @@ nm_connectivity_check_start(NMConnectivity *self, } cb_data->concheck.resolve_cancellable = g_cancellable_new(); + cb_data->concheck.resolve_ifindex = ifindex; /* note that we pick up support for systemd-resolved right away when we need it. * We don't need to remember the setting, because we can (cheaply) check anew @@ -1089,10 +1199,8 @@ nm_connectivity_check_start(NMConnectivity *self, has_systemd_resolved = !!nm_dns_manager_get_systemd_resolved(nm_dns_manager_get()); if (has_systemd_resolved) { - GDBusConnection *dbus_connection; - - dbus_connection = NM_MAIN_DBUS_CONNECTION_GET; - if (!dbus_connection) { + cb_data->concheck.dbus_connection = NM_MAIN_DBUS_CONNECTION_GET; + if (!cb_data->concheck.dbus_connection) { /* we have no D-Bus connection? That might happen in configure and quit mode. * * Anyway, something is very odd, just fail connectivity check. */ @@ -1103,25 +1211,19 @@ nm_connectivity_check_start(NMConnectivity *self, return cb_data; } - g_dbus_connection_call(dbus_connection, + /* first check whether there has been a per-link DNS configured */ + g_dbus_connection_call(cb_data->concheck.dbus_connection, "org.freedesktop.resolve1", "/org/freedesktop/resolve1", "org.freedesktop.resolve1.Manager", - "ResolveHostname", - g_variant_new("(isit)", - 0, - cb_data->concheck.con_config->host, - (gint32) cb_data->addr_family, - SD_RESOLVED_DNS), - G_VARIANT_TYPE("(a(iiay)st)"), + "GetLink", + g_variant_new("(i)", ifindex), + G_VARIANT_TYPE("(o)"), G_DBUS_CALL_FLAGS_NONE, -1, cb_data->concheck.resolve_cancellable, - systemd_resolved_resolve_cb, + systemd_resolved_get_link_cb, cb_data); - _LOG2D("start request to '%s' (try resolving '%s' using systemd-resolved)", - cb_data->concheck.con_config->uri, - cb_data->concheck.con_config->host); return cb_data; } From 2fc662cc712e9e1a1992cbbc187f257f57476e53 Mon Sep 17 00:00:00 2001 From: Beniamino Galvani Date: Fri, 10 Oct 2025 11:44:55 +0200 Subject: [PATCH 07/56] libnm-core, core: add permission helpers Add utility functions to get the number of users and the first user from the connection.permissions property of a connection. (cherry picked from commit 59543620dcf7bb3e4b1316536f0330ab4a752e3e) --- src/core/nm-core-utils.c | 11 ++++++ src/core/nm-core-utils.h | 4 ++ src/libnm-core-impl/nm-setting-connection.c | 41 +++++++++++++++++++++ src/libnm-core-intern/nm-core-internal.h | 5 +++ 4 files changed, 61 insertions(+) diff --git a/src/core/nm-core-utils.c b/src/core/nm-core-utils.c index b68b5b7390..32312e8e9d 100644 --- a/src/core/nm-core-utils.c +++ b/src/core/nm-core-utils.c @@ -5624,3 +5624,14 @@ nm_rate_limit_check(NMRateLimit *rate_limit, gint32 window_sec, gint32 burst) return FALSE; } + +const char * +nm_utils_get_connection_first_permissions_user(NMConnection *connection) +{ + NMSettingConnection *s_con; + + s_con = nm_connection_get_setting_connection(connection); + nm_assert(s_con); + + return _nm_setting_connection_get_first_permissions_user(s_con); +} diff --git a/src/core/nm-core-utils.h b/src/core/nm-core-utils.h index 841616a414..e30d6ce657 100644 --- a/src/core/nm-core-utils.h +++ b/src/core/nm-core-utils.h @@ -503,4 +503,8 @@ typedef struct { gboolean nm_rate_limit_check(NMRateLimit *rate_limit, gint32 window_sec, gint32 burst); +/*****************************************************************************/ + +const char *nm_utils_get_connection_first_permissions_user(NMConnection *connection); + #endif /* __NM_CORE_UTILS_H__ */ diff --git a/src/libnm-core-impl/nm-setting-connection.c b/src/libnm-core-impl/nm-setting-connection.c index 4f25533c07..0ad97846df 100644 --- a/src/libnm-core-impl/nm-setting-connection.c +++ b/src/libnm-core-impl/nm-setting-connection.c @@ -433,6 +433,47 @@ nm_setting_connection_permissions_user_allowed_by_uid(NMSettingConnection *setti return _permissions_user_allowed(setting, NULL, uid); } +guint +_nm_setting_connection_get_num_permissions_users(NMSettingConnection *setting) +{ + NMSettingConnectionPrivate *priv; + guint i; + guint count = 0; + + nm_assert(NM_IS_SETTING_CONNECTION(setting)); + priv = NM_SETTING_CONNECTION_GET_PRIVATE(setting); + + for (i = 0; priv->permissions && i < priv->permissions->len; i++) { + const Permission *permission = &nm_g_array_index(priv->permissions, Permission, i); + + if (permission->ptype == PERM_TYPE_USER) { + count++; + } + } + + return count; +} + +const char * +_nm_setting_connection_get_first_permissions_user(NMSettingConnection *setting) +{ + NMSettingConnectionPrivate *priv; + guint i; + + nm_assert(NM_IS_SETTING_CONNECTION(setting)); + priv = NM_SETTING_CONNECTION_GET_PRIVATE(setting); + + for (i = 0; priv->permissions && i < priv->permissions->len; i++) { + const Permission *permission = &nm_g_array_index(priv->permissions, Permission, i); + + if (permission->ptype == PERM_TYPE_USER) { + return permission->item; + } + } + + return NULL; +} + /** * nm_setting_connection_add_permission: * @setting: the #NMSettingConnection diff --git a/src/libnm-core-intern/nm-core-internal.h b/src/libnm-core-intern/nm-core-internal.h index 8b23dd44cc..6991185d39 100644 --- a/src/libnm-core-intern/nm-core-internal.h +++ b/src/libnm-core-intern/nm-core-internal.h @@ -1187,4 +1187,9 @@ gboolean nm_connection_need_secrets_for_rerequest(NMConnection *connection); const GPtrArray *_nm_setting_ovs_port_get_trunks_arr(NMSettingOvsPort *self); +/*****************************************************************************/ + +guint _nm_setting_connection_get_num_permissions_users(NMSettingConnection *setting); +const char *_nm_setting_connection_get_first_permissions_user(NMSettingConnection *setting); + #endif From afa6fc951b4a19b55d76fb365446a5cf8896a1d3 Mon Sep 17 00:00:00 2001 From: Beniamino Galvani Date: Thu, 2 Oct 2025 16:29:35 +0200 Subject: [PATCH 08/56] helpers: move helper programs to the same directory Create a new 'nm-helpers' directory for all the helper programs, to avoid having too many subdirs in the src directory. (cherry picked from commit 3d76d12eee88b667d1a385b861c54fcdd4e490ed) --- src/meson.build | 3 +-- src/nm-daemon-helper/README.md | 11 ---------- src/nm-daemon-helper/meson.build | 15 -------------- src/{nm-priv-helper => nm-helpers}/README.md | 20 ++++++++++++++++++- .../meson.build | 20 +++++++++++++++++++ .../nm-daemon-helper.c | 0 .../nm-priv-helper.c | 0 .../nm-priv-helper.conf | 0 .../org.freedesktop.nm_priv_helper.service.in | 0 9 files changed, 40 insertions(+), 29 deletions(-) delete mode 100644 src/nm-daemon-helper/README.md delete mode 100644 src/nm-daemon-helper/meson.build rename src/{nm-priv-helper => nm-helpers}/README.md (64%) rename src/{nm-priv-helper => nm-helpers}/meson.build (67%) rename src/{nm-daemon-helper => nm-helpers}/nm-daemon-helper.c (100%) rename src/{nm-priv-helper => nm-helpers}/nm-priv-helper.c (100%) rename src/{nm-priv-helper => nm-helpers}/nm-priv-helper.conf (100%) rename src/{nm-priv-helper => nm-helpers}/org.freedesktop.nm_priv_helper.service.in (100%) diff --git a/src/meson.build b/src/meson.build index a5b3441ba7..7c02080952 100644 --- a/src/meson.build +++ b/src/meson.build @@ -103,8 +103,7 @@ if enable_nmtui endif subdir('nmcli') subdir('nm-dispatcher') -subdir('nm-priv-helper') -subdir('nm-daemon-helper') +subdir('nm-helpers') subdir('nm-online') if enable_nmtui subdir('nmtui') diff --git a/src/nm-daemon-helper/README.md b/src/nm-daemon-helper/README.md deleted file mode 100644 index 695f533553..0000000000 --- a/src/nm-daemon-helper/README.md +++ /dev/null @@ -1,11 +0,0 @@ -nm-daemon-helper -================ - -A internal helper application that is spawned by NetworkManager -to perform certain actions. - -Currently all it does is doing a reverse DNS lookup, which -cannot be done by NetworkManager because the operation requires -to reconfigure the libc resolver (which is a process-wide operation). - -This is not directly useful to the user. diff --git a/src/nm-daemon-helper/meson.build b/src/nm-daemon-helper/meson.build deleted file mode 100644 index da0d6571e1..0000000000 --- a/src/nm-daemon-helper/meson.build +++ /dev/null @@ -1,15 +0,0 @@ -executable( - 'nm-daemon-helper', - 'nm-daemon-helper.c', - include_directories : [ - src_inc, - top_inc, - ], - link_with: [ - libnm_std_aux, - ], - link_args: ldflags_linker_script_binary, - link_depends: linker_script_binary, - install: true, - install_dir: nm_libexecdir, -) diff --git a/src/nm-priv-helper/README.md b/src/nm-helpers/README.md similarity index 64% rename from src/nm-priv-helper/README.md rename to src/nm-helpers/README.md index 576da7a70b..ccaa5bf5cd 100644 --- a/src/nm-priv-helper/README.md +++ b/src/nm-helpers/README.md @@ -1,5 +1,23 @@ +nm-helpers +========== + +This directory contains stand-alone helper programs used by various +components. + +nm-daemon-helper +---------------- + +A internal helper application that is spawned by NetworkManager +to perform certain actions. + +Currently all it does is doing a reverse DNS lookup, which +cannot be done by NetworkManager because the operation requires +to reconfigure the libc resolver (which is a process-wide operation). + +This is not directly useful to the user. + nm-priv-helper -============== +-------------- This is a D-Bus activatable, exit-on-idle service, which provides an internal API to NetworkManager daemon. diff --git a/src/nm-priv-helper/meson.build b/src/nm-helpers/meson.build similarity index 67% rename from src/nm-priv-helper/meson.build rename to src/nm-helpers/meson.build index 6141e0e207..5f330cbc94 100644 --- a/src/nm-priv-helper/meson.build +++ b/src/nm-helpers/meson.build @@ -1,5 +1,25 @@ # SPDX-License-Identifier: LGPL-2.1-or-later +# nm-daemon-helper + +executable( + 'nm-daemon-helper', + 'nm-daemon-helper.c', + include_directories : [ + src_inc, + top_inc, + ], + link_with: [ + libnm_std_aux, + ], + link_args: ldflags_linker_script_binary, + link_depends: linker_script_binary, + install: true, + install_dir: nm_libexecdir, +) + +# nm-priv-helper + configure_file( input: 'org.freedesktop.nm_priv_helper.service.in', output: '@BASENAME@', diff --git a/src/nm-daemon-helper/nm-daemon-helper.c b/src/nm-helpers/nm-daemon-helper.c similarity index 100% rename from src/nm-daemon-helper/nm-daemon-helper.c rename to src/nm-helpers/nm-daemon-helper.c diff --git a/src/nm-priv-helper/nm-priv-helper.c b/src/nm-helpers/nm-priv-helper.c similarity index 100% rename from src/nm-priv-helper/nm-priv-helper.c rename to src/nm-helpers/nm-priv-helper.c diff --git a/src/nm-priv-helper/nm-priv-helper.conf b/src/nm-helpers/nm-priv-helper.conf similarity index 100% rename from src/nm-priv-helper/nm-priv-helper.conf rename to src/nm-helpers/nm-priv-helper.conf diff --git a/src/nm-priv-helper/org.freedesktop.nm_priv_helper.service.in b/src/nm-helpers/org.freedesktop.nm_priv_helper.service.in similarity index 100% rename from src/nm-priv-helper/org.freedesktop.nm_priv_helper.service.in rename to src/nm-helpers/org.freedesktop.nm_priv_helper.service.in From 022b992846712ecff6454524eb3934ff0800cf54 Mon Sep 17 00:00:00 2001 From: Beniamino Galvani Date: Mon, 29 Sep 2025 09:52:51 +0200 Subject: [PATCH 09/56] daemon-helper: add read-file-as-user Add a new command to read the content of a file after switching to the given user. This command can be used to enforce Unix filesystem permissions when accessing a file on behalf of a user. (cherry picked from commit 285457a5f8284f21387753d7f245e3f51ce29248) --- src/libnm-std-aux/nm-std-utils.c | 114 +++++++++++++++++++++++++++++- src/libnm-std-aux/nm-std-utils.h | 4 ++ src/nm-helpers/README.md | 11 +-- src/nm-helpers/nm-daemon-helper.c | 33 +++++++++ 4 files changed, 156 insertions(+), 6 deletions(-) diff --git a/src/libnm-std-aux/nm-std-utils.c b/src/libnm-std-aux/nm-std-utils.c index 8bc109f2e2..6c909dfab3 100644 --- a/src/libnm-std-aux/nm-std-utils.c +++ b/src/libnm-std-aux/nm-std-utils.c @@ -4,10 +4,14 @@ #include "nm-std-utils.h" -#include #include +#include +#include #include #include +#include +#include +#include /*****************************************************************************/ @@ -95,6 +99,114 @@ out_huge: /*****************************************************************************/ +bool +nm_utils_set_effective_user(const char *user, char *errbuf, size_t errbuf_len) +{ + struct passwd *pwentry; + int errsv; + char error[1024]; + + errno = 0; + pwentry = getpwnam(user); + if (!pwentry) { + errsv = errno; + if (errsv == 0) { + snprintf(errbuf, errbuf_len, "user not found"); + } else { + snprintf(errbuf, + errbuf_len, + "error getting user entry: %d (%s)\n", + errsv, + strerror_r(errsv, error, sizeof(error))); + } + return false; + } + + if (setgid(pwentry->pw_gid) != 0) { + errsv = errno; + snprintf(errbuf, + errbuf_len, + "failed to change group to %u: %d (%s)\n", + pwentry->pw_gid, + errsv, + strerror_r(errsv, error, sizeof(error))); + return false; + } + + if (initgroups(user, pwentry->pw_gid) != 0) { + errsv = errno; + snprintf(errbuf, + errbuf_len, + "failed to reset supplementary group list to %u: %d (%s)\n", + pwentry->pw_gid, + errsv, + strerror_r(errsv, error, sizeof(error))); + return false; + } + + if (setuid(pwentry->pw_uid) != 0) { + errsv = errno; + snprintf(errbuf, + errbuf_len, + "failed to change user to %u: %d (%s)\n", + pwentry->pw_uid, + errsv, + strerror_r(errsv, error, sizeof(error))); + return false; + } + + return true; +} + +/*****************************************************************************/ + +bool +nm_utils_read_file_to_stdout(const char *filename, char *errbuf, size_t errbuf_len) +{ + nm_auto_close int fd = -1; + char buffer[4096]; + char error[1024]; + ssize_t bytes_read; + int errsv; + + fd = open(filename, O_RDONLY); + if (fd == -1) { + errsv = errno; + snprintf(errbuf, + errbuf_len, + "error opening the file: %d (%s)", + errsv, + strerror_r(errsv, error, sizeof(error))); + return false; + } + + while ((bytes_read = read(fd, buffer, sizeof(buffer))) > 0) { + if (fwrite(buffer, 1, bytes_read, stdout) != (size_t) bytes_read) { + errsv = errno; + snprintf(errbuf, + errbuf_len, + "error writing to stdout: %d (%s)", + errsv, + strerror_r(errsv, error, sizeof(error))); + return false; + } + } + + if (bytes_read < 0) { + errsv = errno; + snprintf(errbuf, + errbuf_len, + "error reading the file: %d (%s)", + errsv, + strerror_r(errsv, error, sizeof(error))); + return false; + } + + return true; +} + +/*****************************************************************************/ + /** * _nm_strerror_r: * @errsv: the errno passed to strerror_r() diff --git a/src/libnm-std-aux/nm-std-utils.h b/src/libnm-std-aux/nm-std-utils.h index 015444c93d..04c61c30f5 100644 --- a/src/libnm-std-aux/nm-std-utils.h +++ b/src/libnm-std-aux/nm-std-utils.h @@ -37,4 +37,8 @@ size_t nm_utils_get_next_realloc_size(bool true_realloc, size_t requested); const char *_nm_strerror_r(int errsv, char *buf, size_t buf_size); +bool nm_utils_set_effective_user(const char *user, char *errbuf, size_t errbuf_size); + +bool nm_utils_read_file_to_stdout(const char *filename, char *errbuf, size_t errbuf_len); + #endif /* __NM_STD_UTILS_H__ */ diff --git a/src/nm-helpers/README.md b/src/nm-helpers/README.md index ccaa5bf5cd..ab0ea02444 100644 --- a/src/nm-helpers/README.md +++ b/src/nm-helpers/README.md @@ -7,12 +7,13 @@ components. nm-daemon-helper ---------------- -A internal helper application that is spawned by NetworkManager -to perform certain actions. +A internal helper application that is spawned by NetworkManager to +perform certain actions which can't be done in the daemon. -Currently all it does is doing a reverse DNS lookup, which -cannot be done by NetworkManager because the operation requires -to reconfigure the libc resolver (which is a process-wide operation). +Currently it's used to do a reverse DNS lookup after reconfiguring the +libc resolver (which is a process-wide operation), and to read files +on behalf of unprivileged users (which requires a seteuid that affects +all the threads of the process). This is not directly useful to the user. diff --git a/src/nm-helpers/nm-daemon-helper.c b/src/nm-helpers/nm-daemon-helper.c index 32be93a4ef..759993a451 100644 --- a/src/nm-helpers/nm-daemon-helper.c +++ b/src/nm-helpers/nm-daemon-helper.c @@ -137,6 +137,37 @@ cmd_resolve_address(void) return RETURN_ERROR; } +static int +cmd_read_file_as_user(void) +{ + nm_auto_free char *user = NULL; + nm_auto_free char *filename = NULL; + char error[1024]; + + user = read_arg(); + if (!user) + return RETURN_INVALID_ARGS; + + filename = read_arg(); + if (!filename) + return RETURN_INVALID_ARGS; + + if (more_args()) + return RETURN_INVALID_ARGS; + + if (!nm_utils_set_effective_user(user, error, sizeof(error))) { + fprintf(stderr, "Failed to set effective user '%s': %s", user, error); + return RETURN_ERROR; + } + + if (!nm_utils_read_file_to_stdout(filename, error, sizeof(error))) { + fprintf(stderr, "Failed to read file '%s' as user '%s': %s", filename, user, error); + return RETURN_ERROR; + } + + return RETURN_SUCCESS; +} + int main(int argc, char **argv) { @@ -150,6 +181,8 @@ main(int argc, char **argv) return cmd_version(); if (nm_streq(cmd, "resolve-address")) return cmd_resolve_address(); + if (nm_streq(cmd, "read-file-as-user")) + return cmd_read_file_as_user(); return RETURN_INVALID_CMD; } From ce3ebf6d3e5c511f872872bf51bee7f08db6f045 Mon Sep 17 00:00:00 2001 From: Beniamino Galvani Date: Tue, 16 Sep 2025 16:56:00 +0200 Subject: [PATCH 10/56] supplicant: remove blobs before adding new ones When connecting, we add the blobs to the Interface object of the supplicant. Those blobs are not removed on disconnect and so when we try to add blobs with the same id, the supplicant returns an error. Make sure we start from a clean slate on each connection attempt, by deleting all existing blobs. Probably we should also delete the added blobs on disconnect, but that's left for a future improvement. (cherry picked from commit 0093bbd9507df3b16eaa08cd3a6b799b678c7599) --- src/core/supplicant/nm-supplicant-interface.c | 191 +++++++++++++++--- 1 file changed, 159 insertions(+), 32 deletions(-) diff --git a/src/core/supplicant/nm-supplicant-interface.c b/src/core/supplicant/nm-supplicant-interface.c index 43926d4228..5c60a7b6d6 100644 --- a/src/core/supplicant/nm-supplicant-interface.c +++ b/src/core/supplicant/nm-supplicant-interface.c @@ -46,6 +46,7 @@ typedef struct { gpointer user_data; guint fail_on_idle_id; guint blobs_left; + guint remove_blobs_left; guint calls_left; struct _AddNetworkData *add_network_data; } AssocData; @@ -2265,6 +2266,7 @@ assoc_add_blob_cb(GObject *source, GAsyncResult *result, gpointer user_data) return; } + nm_assert(priv->assoc_data->blobs_left > 0); priv->assoc_data->blobs_left--; _LOGT("assoc[" NM_HASH_OBFUSCATE_PTR_FMT "]: blob added (%u left)", NM_HASH_OBFUSCATE_PTR(priv->assoc_data), @@ -2273,6 +2275,148 @@ assoc_add_blob_cb(GObject *source, GAsyncResult *result, gpointer user_data) assoc_call_select_network(self); } +static void +assoc_add_blobs(NMSupplicantInterface *self) +{ + NMSupplicantInterfacePrivate *priv = NM_SUPPLICANT_INTERFACE_GET_PRIVATE(self); + GHashTable *blobs; + GHashTableIter iter; + const char *blob_name; + GBytes *blob_data; + + blobs = nm_supplicant_config_get_blobs(priv->assoc_data->cfg); + priv->assoc_data->blobs_left = nm_g_hash_table_size(blobs); + + _LOGT("assoc[" NM_HASH_OBFUSCATE_PTR_FMT "]: need to add %u blobs", + NM_HASH_OBFUSCATE_PTR(priv->assoc_data), + priv->assoc_data->blobs_left); + + if (priv->assoc_data->blobs_left == 0) { + assoc_call_select_network(self); + return; + } + + g_hash_table_iter_init(&iter, blobs); + while (g_hash_table_iter_next(&iter, (gpointer) &blob_name, (gpointer) &blob_data)) { + _LOGT("assoc[" NM_HASH_OBFUSCATE_PTR_FMT "]: adding blob '%s'", + NM_HASH_OBFUSCATE_PTR(priv->assoc_data), + blob_name); + _dbus_connection_call( + self, + NM_WPAS_DBUS_IFACE_INTERFACE, + "AddBlob", + g_variant_new("(s@ay)", blob_name, nm_g_bytes_to_variant_ay(blob_data)), + G_VARIANT_TYPE("()"), + G_DBUS_CALL_FLAGS_NONE, + DBUS_TIMEOUT_MSEC, + priv->assoc_data->cancellable, + assoc_add_blob_cb, + self); + } +} + +static void +assoc_remove_blob_cb(GObject *source, GAsyncResult *result, gpointer user_data) +{ + NMSupplicantInterface *self; + NMSupplicantInterfacePrivate *priv; + gs_free_error GError *error = NULL; + gs_unref_variant GVariant *res = NULL; + + res = g_dbus_connection_call_finish(G_DBUS_CONNECTION(source), result, &error); + if (nm_utils_error_is_cancelled(error)) + return; + + self = NM_SUPPLICANT_INTERFACE(user_data); + priv = NM_SUPPLICANT_INTERFACE_GET_PRIVATE(self); + + /* We don't consider a failure fatal. The new association might be able + * to proceed even with the existing blobs, if they don't conflict with new + * ones. */ + + nm_assert(priv->assoc_data->remove_blobs_left > 0); + priv->assoc_data->remove_blobs_left--; + + if (error) { + g_dbus_error_strip_remote_error(error); + _LOGD("assoc[" NM_HASH_OBFUSCATE_PTR_FMT "]: failed to delete blob: %s", + NM_HASH_OBFUSCATE_PTR(priv->assoc_data), + error->message); + } else { + _LOGT("assoc[" NM_HASH_OBFUSCATE_PTR_FMT "]: blob removed (%u left)", + NM_HASH_OBFUSCATE_PTR(priv->assoc_data), + priv->assoc_data->remove_blobs_left); + } + + if (priv->assoc_data->remove_blobs_left == 0) + assoc_add_blobs(self); +} + +static void +assoc_get_blobs_cb(GObject *source, GAsyncResult *result, gpointer user_data) +{ + NMSupplicantInterface *self; + NMSupplicantInterfacePrivate *priv; + gs_free_error GError *error = NULL; + gs_unref_variant GVariant *res = NULL; + gs_unref_variant GVariant *value = NULL; + GVariantIter iter; + const char *blob_name; + GVariant *blob_data; + + res = g_dbus_connection_call_finish(G_DBUS_CONNECTION(source), result, &error); + if (nm_utils_error_is_cancelled(error)) + return; + + self = NM_SUPPLICANT_INTERFACE(user_data); + priv = NM_SUPPLICANT_INTERFACE_GET_PRIVATE(self); + + if (error) { + _LOGD("assoc[" NM_HASH_OBFUSCATE_PTR_FMT "]: failed to get blob list: %s", + NM_HASH_OBFUSCATE_PTR(priv->assoc_data), + error->message); + assoc_add_blobs(self); + return; + } + + g_variant_get(res, "(v)", &value); + + /* While the "Blobs" property is documented as type "as", it is actually "a{say}" */ + if (!value || !g_variant_is_of_type(value, G_VARIANT_TYPE("a{say}"))) { + _LOGD("assoc[" NM_HASH_OBFUSCATE_PTR_FMT "]: failed to get blob list: wrong return type %s", + NM_HASH_OBFUSCATE_PTR(priv->assoc_data), + value ? g_variant_get_type_string(value) : "NULL"); + assoc_add_blobs(self); + return; + } + + g_variant_iter_init(&iter, value); + priv->assoc_data->remove_blobs_left = g_variant_iter_n_children(&iter); + _LOGT("assoc[" NM_HASH_OBFUSCATE_PTR_FMT "]: need to delete %u blobs", + NM_HASH_OBFUSCATE_PTR(priv->assoc_data), + priv->assoc_data->remove_blobs_left); + + if (priv->assoc_data->remove_blobs_left == 0) { + assoc_add_blobs(self); + } else { + while (g_variant_iter_loop(&iter, "{&s@ay}", &blob_name, &blob_data)) { + _LOGT("assoc[" NM_HASH_OBFUSCATE_PTR_FMT "]: removing blob '%s'", + NM_HASH_OBFUSCATE_PTR(priv->assoc_data), + blob_name); + _dbus_connection_call(self, + NM_WPAS_DBUS_IFACE_INTERFACE, + "RemoveBlob", + g_variant_new("(s)", blob_name), + G_VARIANT_TYPE("()"), + G_DBUS_CALL_FLAGS_NONE, + DBUS_TIMEOUT_MSEC, + priv->assoc_data->cancellable, + assoc_remove_blob_cb, + self); + } + } +} + static void assoc_add_network_cb(GObject *source, GAsyncResult *result, gpointer user_data) { @@ -2280,12 +2424,8 @@ assoc_add_network_cb(GObject *source, GAsyncResult *result, gpointer user_data) AssocData *assoc_data; NMSupplicantInterface *self; NMSupplicantInterfacePrivate *priv; - gs_unref_variant GVariant *res = NULL; - gs_free_error GError *error = NULL; - GHashTable *blobs; - GHashTableIter iter; - const char *blob_name; - GBytes *blob_data; + gs_unref_variant GVariant *res = NULL; + gs_free_error GError *error = NULL; nm_auto_ref_string NMRefString *name_owner = NULL; nm_auto_ref_string NMRefString *object_path = NULL; @@ -2337,34 +2477,21 @@ assoc_add_network_cb(GObject *source, GAsyncResult *result, gpointer user_data) nm_assert(!priv->net_path); g_variant_get(res, "(o)", &priv->net_path); - /* Send blobs first; otherwise jump to selecting the network */ - blobs = nm_supplicant_config_get_blobs(priv->assoc_data->cfg); - priv->assoc_data->blobs_left = blobs ? g_hash_table_size(blobs) : 0u; - - _LOGT("assoc[" NM_HASH_OBFUSCATE_PTR_FMT "]: network added (%s) (%u blobs left)", + _LOGT("assoc[" NM_HASH_OBFUSCATE_PTR_FMT "]: network added (%s)", NM_HASH_OBFUSCATE_PTR(priv->assoc_data), - priv->net_path, - priv->assoc_data->blobs_left); + priv->net_path); - if (priv->assoc_data->blobs_left == 0) { - assoc_call_select_network(self); - return; - } - - g_hash_table_iter_init(&iter, blobs); - while (g_hash_table_iter_next(&iter, (gpointer) &blob_name, (gpointer) &blob_data)) { - _dbus_connection_call( - self, - NM_WPAS_DBUS_IFACE_INTERFACE, - "AddBlob", - g_variant_new("(s@ay)", blob_name, nm_g_bytes_to_variant_ay(blob_data)), - G_VARIANT_TYPE("()"), - G_DBUS_CALL_FLAGS_NONE, - DBUS_TIMEOUT_MSEC, - priv->assoc_data->cancellable, - assoc_add_blob_cb, - self); - } + /* Delete any existing blobs before adding new ones */ + _dbus_connection_call(self, + DBUS_INTERFACE_PROPERTIES, + "Get", + g_variant_new("(ss)", NM_WPAS_DBUS_IFACE_INTERFACE, "Blobs"), + G_VARIANT_TYPE("(v)"), + G_DBUS_CALL_FLAGS_NONE, + DBUS_TIMEOUT_MSEC, + priv->assoc_data->cancellable, + assoc_get_blobs_cb, + self); } static void From 59df5fc93fc30c9b8c9ceca3c42b173f831f53f7 Mon Sep 17 00:00:00 2001 From: Beniamino Galvani Date: Tue, 16 Sep 2025 16:57:19 +0200 Subject: [PATCH 11/56] core: support returning binary output from the daemon helper The full output of the daemon helper is added to a NMStrBuf, without interpreting it as a string (that is, without stopping at the first NUL character). However, when we retrieve the content from the NMStrBuf we assume it's a string. This is fine for certain commands that expect a string output, but it's not for other commands as the read-file-as-user one. Add a new argument to nm_utils_spawn_helper() to specify whether the output is binary or not. Also have different finish functions depending on the return type. (cherry picked from commit 1d90d50fc6e8c167581c6831c2511bc4148f234b) --- src/core/devices/nm-device-utils.c | 3 ++- src/core/nm-core-utils.c | 39 ++++++++++++++++++++++++++---- src/core/nm-core-utils.h | 4 ++- 3 files changed, 39 insertions(+), 7 deletions(-) diff --git a/src/core/devices/nm-device-utils.c b/src/core/devices/nm-device-utils.c index 3b4c6b2b63..be1de3ea87 100644 --- a/src/core/devices/nm-device-utils.c +++ b/src/core/devices/nm-device-utils.c @@ -239,7 +239,7 @@ resolve_addr_helper_cb(GObject *source, GAsyncResult *result, gpointer user_data gs_free_error GError *error = NULL; gs_free char *output = NULL; - output = nm_utils_spawn_helper_finish(result, &error); + output = nm_utils_spawn_helper_finish_string(result, &error); if (nm_utils_error_is_cancelled(error)) return; @@ -278,6 +278,7 @@ resolve_addr_spawn_helper(ResolveAddrInfo *info, ResolveAddrService services) nm_inet_ntop(info->addr_family, &info->address, addr_str); _LOG2D(info, "start lookup via nm-daemon-helper using services: %s", str); nm_utils_spawn_helper(NM_MAKE_STRV("resolve-address", addr_str, str), + FALSE, g_task_get_cancellable(info->task), resolve_addr_helper_cb, info); diff --git a/src/core/nm-core-utils.c b/src/core/nm-core-utils.c index 32312e8e9d..7e1c1674b9 100644 --- a/src/core/nm-core-utils.c +++ b/src/core/nm-core-utils.c @@ -5012,6 +5012,7 @@ typedef struct { int child_stdin; int child_stdout; int child_stderr; + gboolean binary_output; GSource *input_source; GSource *output_source; GSource *error_source; @@ -5091,9 +5092,17 @@ helper_complete(HelperInfo *info, GError *error) } nm_clear_g_cancellable_disconnect(g_task_get_cancellable(info->task), &info->cancellable_id); - g_task_return_pointer(info->task, - nm_str_buf_finalize(&info->in_buffer, NULL) ?: g_new0(char, 1), - g_free); + + if (info->binary_output) { + g_task_return_pointer( + info->task, + g_bytes_new(nm_str_buf_get_str_unsafe(&info->in_buffer), info->in_buffer.len), + (GDestroyNotify) (g_bytes_unref)); + } else { + g_task_return_pointer(info->task, + nm_str_buf_finalize(&info->in_buffer, NULL) ?: g_new0(char, 1), + g_free); + } helper_info_free(info); } @@ -5236,6 +5245,7 @@ helper_cancelled(GObject *object, gpointer user_data) void nm_utils_spawn_helper(const char *const *args, + gboolean binary_output, GCancellable *cancellable, GAsyncReadyCallback callback, gpointer cb_data) @@ -5251,9 +5261,14 @@ nm_utils_spawn_helper(const char *const *args, info = g_new(HelperInfo, 1); *info = (HelperInfo) { - .task = nm_g_task_new(NULL, cancellable, nm_utils_spawn_helper, callback, cb_data), + .task = nm_g_task_new(NULL, cancellable, nm_utils_spawn_helper, callback, cb_data), + .binary_output = binary_output, }; + /* Store if the caller requested binary output so that we can check later + * that the right result function is called. */ + g_task_set_task_data(info->task, GINT_TO_POINTER(binary_output), NULL); + if (!g_spawn_async_with_pipes("/", (char **) NM_MAKE_STRV(LIBEXECDIR "/nm-daemon-helper"), (char **) NM_MAKE_STRV(), @@ -5364,11 +5379,25 @@ nm_utils_spawn_helper(const char *const *args, } char * -nm_utils_spawn_helper_finish(GAsyncResult *result, GError **error) +nm_utils_spawn_helper_finish_string(GAsyncResult *result, GError **error) { GTask *task = G_TASK(result); nm_assert(nm_g_task_is_valid(result, NULL, nm_utils_spawn_helper)); + /* Check binary_output */ + nm_assert(GPOINTER_TO_INT(g_task_get_task_data(task)) == FALSE); + + return g_task_propagate_pointer(task, error); +} + +GBytes * +nm_utils_spawn_helper_finish_binary(GAsyncResult *result, GError **error) +{ + GTask *task = G_TASK(result); + + nm_assert(nm_g_task_is_valid(result, NULL, nm_utils_spawn_helper)); + /* Check binary_output */ + nm_assert(GPOINTER_TO_INT(g_task_get_task_data(task)) == TRUE); return g_task_propagate_pointer(task, error); } diff --git a/src/core/nm-core-utils.h b/src/core/nm-core-utils.h index e30d6ce657..be5490ce8d 100644 --- a/src/core/nm-core-utils.h +++ b/src/core/nm-core-utils.h @@ -478,11 +478,13 @@ guint8 nm_wifi_utils_level_to_quality(int val); /*****************************************************************************/ void nm_utils_spawn_helper(const char *const *args, + gboolean binary_output, GCancellable *cancellable, GAsyncReadyCallback callback, gpointer cb_data); -char *nm_utils_spawn_helper_finish(GAsyncResult *result, GError **error); +char *nm_utils_spawn_helper_finish_string(GAsyncResult *result, GError **error); +GBytes *nm_utils_spawn_helper_finish_binary(GAsyncResult *result, GError **error); /*****************************************************************************/ From a17f51fe156ed63882d5dc49e594e23913f883fd Mon Sep 17 00:00:00 2001 From: Beniamino Galvani Date: Tue, 16 Sep 2025 16:58:31 +0200 Subject: [PATCH 12/56] supplicant: rename variables Rename uid to to blob_id, and con_uid to con_uuid. (cherry picked from commit 586f7700b8ad6b4b4cffdb4cdb2bed2e4726ef5c) --- src/core/supplicant/nm-supplicant-config.c | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/core/supplicant/nm-supplicant-config.c b/src/core/supplicant/nm-supplicant-config.c index 38294e89a3..635174bdf0 100644 --- a/src/core/supplicant/nm-supplicant-config.c +++ b/src/core/supplicant/nm-supplicant-config.c @@ -258,19 +258,19 @@ static gboolean nm_supplicant_config_add_blob_for_connection(NMSupplicantConfig *self, GBytes *field, const char *name, - const char *con_uid, + const char *con_uuid, GError **error) { if (field && g_bytes_get_size(field)) { - gs_free char *uid = NULL; + gs_free char *blob_id = NULL; char *p; - uid = g_strdup_printf("%s-%s", con_uid, name); - for (p = uid; *p; 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, uid, error)) + if (!nm_supplicant_config_add_blob(self, name, field, blob_id, error)) return FALSE; } return TRUE; From 9432822f3460975520aeba4b3367108d5322b28a Mon Sep 17 00:00:00 2001 From: Beniamino Galvani Date: Tue, 23 Sep 2025 17:00:53 +0200 Subject: [PATCH 13/56] core: add functions to read private files of connections Add function nm_utils_read_private_files(). It can be used to read a list of paths as the given user. It spawns the daemon-helper to read each path and returns asynchronously a hash table containing the files content. Also add nm_utils_get_connection_private_files_paths() to return a list of file paths referenced in a connection. The function currently returns only 802.1x file paths for certificates and keys. (cherry picked from commit de4eb64253d493364d676b509f63f2e8d1810061) --- src/core/nm-core-utils.c | 199 +++++++++++++++++++++++++++++++++++++++ src/core/nm-core-utils.h | 11 +++ 2 files changed, 210 insertions(+) diff --git a/src/core/nm-core-utils.c b/src/core/nm-core-utils.c index 7e1c1674b9..ee50de5a81 100644 --- a/src/core/nm-core-utils.c +++ b/src/core/nm-core-utils.c @@ -5664,3 +5664,202 @@ nm_utils_get_connection_first_permissions_user(NMConnection *connection) return _nm_setting_connection_get_first_permissions_user(s_con); } + +/*****************************************************************************/ + +static void +get_8021x_private_files(NMConnection *connection, GPtrArray *files) +{ + const struct { + NMSetting8021xCKScheme (*get_scheme_func)(NMSetting8021x *); + const char *(*get_path_func)(NMSetting8021x *); + } funcs[] = { + {nm_setting_802_1x_get_ca_cert_scheme, nm_setting_802_1x_get_ca_cert_path}, + {nm_setting_802_1x_get_client_cert_scheme, nm_setting_802_1x_get_client_cert_path}, + {nm_setting_802_1x_get_private_key_scheme, nm_setting_802_1x_get_private_key_path}, + {nm_setting_802_1x_get_phase2_ca_cert_scheme, nm_setting_802_1x_get_phase2_ca_cert_path}, + {nm_setting_802_1x_get_phase2_client_cert_scheme, + nm_setting_802_1x_get_phase2_client_cert_path}, + {nm_setting_802_1x_get_phase2_private_key_scheme, + nm_setting_802_1x_get_phase2_private_key_path}, + }; + NMSetting8021x *s_8021x; + const char *path; + guint i; + + s_8021x = nm_connection_get_setting_802_1x(connection); + if (!s_8021x) + return; + + for (i = 0; i < G_N_ELEMENTS(funcs); i++) { + if (funcs[i].get_scheme_func(s_8021x) == NM_SETTING_802_1X_CK_SCHEME_PATH) { + path = funcs[i].get_path_func(s_8021x); + if (path) { + g_ptr_array_add(files, (gpointer) path); + } + } + } +} + +const char ** +nm_utils_get_connection_private_files_paths(NMConnection *connection) +{ + GPtrArray *files; + + files = g_ptr_array_new(); + get_8021x_private_files(connection, files); + g_ptr_array_add(files, NULL); + + return (const char **) g_ptr_array_free(files, files->len == 1); +} + +typedef struct _ReadInfo ReadInfo; + +typedef struct { + char *path; + ReadInfo *read_info; +} FileInfo; + +struct _ReadInfo { + GTask *task; + GHashTable *table; + GPtrArray *file_infos; /* of FileInfo */ + GError *first_error; + guint num_pending; +}; + +static void +read_file_helper_cb(GObject *source, GAsyncResult *result, gpointer user_data) +{ + FileInfo *file_info = user_data; + ReadInfo *read_info = file_info->read_info; + gs_unref_bytes GBytes *output = NULL; + gs_free_error GError *error = NULL; + + output = nm_utils_spawn_helper_finish_binary(result, &error); + + nm_assert(read_info->num_pending > 0); + read_info->num_pending--; + + if (nm_utils_error_is_cancelled(error)) { + /* nop */ + } else if (error) { + nm_log_dbg(LOGD_CORE, + "read-private-files: failed to read file '%s': %s", + file_info->path, + error->message); + if (!read_info->first_error) { + /* @error just says "helper process exited with status X". + * Return a more human-friendly one. */ + read_info->first_error = g_error_new(NM_UTILS_ERROR, + NM_UTILS_ERROR_UNKNOWN, + "error reading file '%s'", + file_info->path); + } + } else { + nm_log_dbg(LOGD_SUPPLICANT, + "read-private-files: successfully read file '%s'", + file_info->path); + + /* Store the file contents in the hash table */ + if (!read_info->table) { + read_info->table = g_hash_table_new_full(nm_str_hash, + g_str_equal, + g_free, + (GDestroyNotify) g_bytes_unref); + } + g_hash_table_insert(read_info->table, + g_steal_pointer(&file_info->path), + g_steal_pointer(&output)); + } + + g_clear_pointer(&file_info->path, g_free); + + /* If all operations are completed, return */ + if (read_info->num_pending == 0) { + if (read_info->first_error) { + g_task_return_error(read_info->task, g_steal_pointer(&read_info->first_error)); + } else { + g_task_return_pointer(read_info->task, + g_steal_pointer(&read_info->table), + (GDestroyNotify) g_hash_table_unref); + } + + if (read_info->table) + g_hash_table_unref(read_info->table); + if (read_info->file_infos) + g_ptr_array_unref(read_info->file_infos); + + g_object_unref(read_info->task); + g_free(read_info); + } +} + +/** + * nm_utils_read_private_files: + * @paths: array of file paths to be read + * @user: name of the user to impersonate when reading the files + * @cancellable: cancellable to cancel the operation + * @callback: callback to invoke on completion + * @cb_data: data for @callback + * + * Reads the given list of files @paths on behalf of user @user. Invokes + * @callback asynchronously on completion. The callback must use + * nm_utils_read_private_files_finish() to obtain the result. + */ +void +nm_utils_read_private_files(const char *const *paths, + const char *user, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer cb_data) +{ + ReadInfo *read_info; + FileInfo *file_info; + guint i; + + g_return_if_fail(paths && paths[0]); + g_return_if_fail(cancellable); + g_return_if_fail(callback); + g_return_if_fail(cb_data); + + read_info = g_new(ReadInfo, 1); + *read_info = (ReadInfo) { + .task = nm_g_task_new(NULL, cancellable, nm_utils_read_private_files, callback, cb_data), + .file_infos = g_ptr_array_new_with_free_func(g_free), + }; + + for (i = 0; paths[i]; i++) { + file_info = g_new(FileInfo, 1); + *file_info = (FileInfo) { + .path = g_strdup(paths[i]), + .read_info = read_info, + }; + g_ptr_array_add(read_info->file_infos, file_info); + read_info->num_pending++; + + nm_utils_spawn_helper(NM_MAKE_STRV("read-file-as-user", user, paths[i]), + TRUE, + cancellable, + read_file_helper_cb, + file_info); + } +} + +/** + * nm_utils_read_private_files_finish: + * @result: the GAsyncResult + * @error: on return, the error + * + * Returns the files read by nm_utils_read_private_files(). The return value + * is a hash table {char * -> GBytes *}. Free it with g_hash_table_unref(). + */ +GHashTable * +nm_utils_read_private_files_finish(GAsyncResult *result, GError **error) +{ + GTask *task = G_TASK(result); + + nm_assert(nm_g_task_is_valid(result, NULL, nm_utils_read_private_files)); + + return g_task_propagate_pointer(task, error); +} diff --git a/src/core/nm-core-utils.h b/src/core/nm-core-utils.h index be5490ce8d..cccccae636 100644 --- a/src/core/nm-core-utils.h +++ b/src/core/nm-core-utils.h @@ -509,4 +509,15 @@ gboolean nm_rate_limit_check(NMRateLimit *rate_limit, gint32 window_sec, gint32 const char *nm_utils_get_connection_first_permissions_user(NMConnection *connection); +/*****************************************************************************/ + +const char **nm_utils_get_connection_private_files_paths(NMConnection *connection); + +void nm_utils_read_private_files(const char *const *paths, + const char *user, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer cb_data); +GHashTable *nm_utils_read_private_files_finish(GAsyncResult *result, GError **error); + #endif /* __NM_CORE_UTILS_H__ */ From a417df34847ae7cd1eb0d77af8b70beb6619cfbe Mon Sep 17 00:00:00 2001 From: Beniamino Galvani Date: Tue, 23 Sep 2025 17:04:02 +0200 Subject: [PATCH 14/56] device: read private files in stage2 During stage2 (prepare) of an activation, check if the connection is private and if it contains any certificate/key path. If so, start reading the files and delay stage2. Once done, store the files' content into priv->private_files.table and continue the activation. (cherry picked from commit 98e6dbdf21e5b165bae498ab2a29bb14f331ccd1) --- src/core/devices/nm-device-private.h | 2 + src/core/devices/nm-device.c | 127 ++++++++++++++++++++++++++- 2 files changed, 128 insertions(+), 1 deletion(-) diff --git a/src/core/devices/nm-device-private.h b/src/core/devices/nm-device-private.h index 2b4793eb38..6d82859757 100644 --- a/src/core/devices/nm-device-private.h +++ b/src/core/devices/nm-device-private.h @@ -176,4 +176,6 @@ void nm_device_auth_request(NMDevice *self, void nm_device_link_properties_set(NMDevice *self, gboolean reapply); +GHashTable *nm_device_get_private_files(NMDevice *self); + #endif /* NM_DEVICE_PRIVATE_H */ diff --git a/src/core/devices/nm-device.c b/src/core/devices/nm-device.c index 04850fbecf..46fb233974 100644 --- a/src/core/devices/nm-device.c +++ b/src/core/devices/nm-device.c @@ -348,6 +348,12 @@ typedef struct { int addr_family; } HostnameResolver; +typedef enum { + PRIVATE_FILES_STATE_UNKNOWN = 0, + PRIVATE_FILES_STATE_READING, + PRIVATE_FILES_STATE_DONE, +} PrivateFilesState; + /*****************************************************************************/ enum { @@ -784,6 +790,13 @@ typedef struct _NMDevicePrivate { guint64 rx_bytes; } stats; + struct { + GHashTable *table; + GCancellable *cancellable; + char *user; + PrivateFilesState state; + } private_files; + bool mtu_force_set_done : 1; bool needs_ip6_subnet : 1; @@ -10831,6 +10844,49 @@ tc_commit(NMDevice *self) return TRUE; } +static void +read_private_files_cb(GObject *source_object, GAsyncResult *result, gpointer data) +{ + gs_unref_hashtable GHashTable *table = NULL; + gs_free_error GError *error = NULL; + NMDevice *self; + NMDevicePrivate *priv; + + table = nm_utils_read_private_files_finish(result, &error); + if (nm_utils_error_is_cancelled(error)) + return; + + self = NM_DEVICE(data); + priv = NM_DEVICE_GET_PRIVATE(self); + + if (error) { + NMConnection *connection = nm_device_get_applied_connection(self); + + _LOGW(LOGD_DEVICE, + "could not read files for private connection %s owned by user '%s': %s", + connection ? nm_connection_get_uuid(connection) : NULL, + priv->private_files.user, + error->message); + nm_device_state_changed(self, NM_DEVICE_STATE_FAILED, NM_DEVICE_STATE_REASON_CONFIG_FAILED); + return; + } + + _LOGD(LOGD_DEVICE, "private files successfully read"); + + priv->private_files.state = PRIVATE_FILES_STATE_DONE; + priv->private_files.table = g_steal_pointer(&table); + g_clear_pointer(&priv->private_files.user, g_free); + g_clear_object(&priv->private_files.cancellable); + + nm_device_activate_schedule_stage2_device_config(self, FALSE); +} + +GHashTable * +nm_device_get_private_files(NMDevice *self) +{ + return NM_DEVICE_GET_PRIVATE(self)->private_files.table; +} + /* * activate_stage2_device_config * @@ -10843,6 +10899,7 @@ activate_stage2_device_config(NMDevice *self) { NMDevicePrivate *priv = NM_DEVICE_GET_PRIVATE(self); NMDeviceClass *klass = NM_DEVICE_GET_CLASS(self); + NMConnection *applied; NMActStageReturn ret; NMSettingWired *s_wired; gboolean no_firmware = FALSE; @@ -10851,6 +10908,68 @@ activate_stage2_device_config(NMDevice *self) nm_device_state_changed(self, NM_DEVICE_STATE_CONFIG, NM_DEVICE_STATE_REASON_NONE); + applied = nm_device_get_applied_connection(self); + + /* If the connection is private (owned by a specific user), we need to + * verify that the user has permission to access any files specified in + * the connection, such as certificates and keys. We do that by calling + * nm_utils_read_private_files() and saving the file contents in a hash + * table that can be accessed later during the activation. It is important + * to never access the files again to avoid TOCTOU bugs. + */ + switch (priv->private_files.state) { + case PRIVATE_FILES_STATE_UNKNOWN: + { + gs_free const char **paths = NULL; + NMSettingConnection *s_con; + const char *user; + + s_con = nm_connection_get_setting_connection(applied); + nm_assert(s_con); + user = _nm_setting_connection_get_first_permissions_user(s_con); + + priv->private_files.user = g_strdup(user); + if (!priv->private_files.user) { + priv->private_files.state = PRIVATE_FILES_STATE_DONE; + break; + } + + paths = nm_utils_get_connection_private_files_paths(applied); + if (!paths) { + priv->private_files.state = PRIVATE_FILES_STATE_DONE; + break; + } + + if (_nm_setting_connection_get_num_permissions_users(s_con) > 1) { + _LOGW(LOGD_DEVICE, + "private connections with multiple users are not allowed to reference " + "certificates and keys on the filesystem. Specify only one user in the " + "connection.permissions property."); + nm_device_state_changed(self, + NM_DEVICE_STATE_FAILED, + NM_DEVICE_STATE_REASON_CONFIG_FAILED); + return; + } + + priv->private_files.state = PRIVATE_FILES_STATE_READING; + priv->private_files.cancellable = g_cancellable_new(); + + _LOGD(LOGD_DEVICE, "reading private files"); + nm_utils_read_private_files(paths, + priv->private_files.user, + priv->private_files.cancellable, + read_private_files_cb, + self); + return; + } + case PRIVATE_FILES_STATE_READING: + /* wait */ + return; + case PRIVATE_FILES_STATE_DONE: + /* proceed */ + break; + } + if (!nm_device_managed_type_is_external(self)) { _ethtool_state_set(self); nm_device_link_properties_set(self, FALSE); @@ -10867,7 +10986,7 @@ activate_stage2_device_config(NMDevice *self) priv->tc_committed = TRUE; } - nm_routing_rules_sync(nm_device_get_applied_connection(self), + nm_routing_rules_sync(applied, NM_TERNARY_TRUE, klass->get_extra_rules, self, @@ -17180,6 +17299,12 @@ nm_device_cleanup(NMDevice *self, NMDeviceStateReason reason, CleanupType cleanu if (klass->deactivate) klass->deactivate(self); + /* Clean up private files */ + nm_clear_g_cancellable(&priv->private_files.cancellable); + g_clear_pointer(&priv->private_files.table, g_hash_table_unref); + g_clear_pointer(&priv->private_files.user, g_free); + priv->private_files.state = PRIVATE_FILES_STATE_UNKNOWN; + ifindex = nm_device_get_ip_ifindex(self); if (cleanup_type == CLEANUP_TYPE_DECONFIGURE) { From aac5b80fcad34489e737b6eb1c5389bd32169d23 Mon Sep 17 00:00:00 2001 From: Beniamino Galvani Date: Tue, 23 Sep 2025 17:06:39 +0200 Subject: [PATCH 15/56] core: pass certificates as blobs to supplicant for private connections In case of private connections, the device has already read the certificates and keys content from disk, validating that the owner of the connection has access to them. Pass those files as blobs to the supplicant so that it doesn't have to read them again from the filesystem, creating the opportunity for TOCTOU bugs. (cherry picked from commit 36ea70c0993cb48d3155c2de6d6c8e48a2b08c60) --- NEWS | 14 +- src/core/devices/nm-device-ethernet.c | 11 +- src/core/devices/nm-device-macsec.c | 11 +- src/core/devices/wifi/nm-device-wifi.c | 4 +- src/core/supplicant/nm-supplicant-config.c | 160 ++++++++++++------ src/core/supplicant/nm-supplicant-config.h | 4 +- .../supplicant/tests/test-supplicant-config.c | 4 +- 7 files changed, 143 insertions(+), 65 deletions(-) diff --git a/NEWS b/NEWS index 9e2d0cc15d..563927584c 100644 --- a/NEWS +++ b/NEWS @@ -1,13 +1,17 @@ +=============================================== +NetworkManager-1.56.1 +Overview of changes since NetworkManager-1.56.0 +=============================================== + +* For private connections (the ones that specify a user in the + "connection.permissions" property), verify that the user can access + the 802.1X certificates and keys set in the connection. + ============================================= NetworkManager-1.56 Overview of changes since NetworkManager-1.54 ============================================= -This is a snapshot of NetworkManager development. The API is -subject to change and not guaranteed to be compatible with -the later release. -USE AT YOUR OWN RISK. NOT RECOMMENDED FOR PRODUCTION USE! - * nmcli now supports viewing and managing WireGuard peers. * Support reapplying the "sriov.vfs" property as long as "sriov.total-vfs" is not changed. diff --git a/src/core/devices/nm-device-ethernet.c b/src/core/devices/nm-device-ethernet.c index 5396914e82..11f691ded9 100644 --- a/src/core/devices/nm-device-ethernet.c +++ b/src/core/devices/nm-device-ethernet.c @@ -629,10 +629,17 @@ build_supplicant_config(NMDeviceEthernet *self, GError **error) mtu = nm_platform_link_get_mtu(nm_device_get_platform(NM_DEVICE(self)), nm_device_get_ifindex(NM_DEVICE(self))); - config = nm_supplicant_config_new(NM_SUPPL_CAP_MASK_NONE); + config = nm_supplicant_config_new(NM_SUPPL_CAP_MASK_NONE, + nm_utils_get_connection_first_permissions_user(connection)); security = nm_connection_get_setting_802_1x(connection); - if (!nm_supplicant_config_add_setting_8021x(config, security, con_uuid, mtu, TRUE, error)) { + if (!nm_supplicant_config_add_setting_8021x(config, + security, + con_uuid, + mtu, + TRUE, + nm_device_get_private_files(NM_DEVICE(self)), + error)) { g_prefix_error(error, "802-1x-setting: "); g_clear_object(&config); } diff --git a/src/core/devices/nm-device-macsec.c b/src/core/devices/nm-device-macsec.c index 5d67081c77..eb39cb2ab0 100644 --- a/src/core/devices/nm-device-macsec.c +++ b/src/core/devices/nm-device-macsec.c @@ -201,7 +201,8 @@ build_supplicant_config(NMDeviceMacsec *self, GError **error) mtu = nm_platform_link_get_mtu(nm_device_get_platform(NM_DEVICE(self)), nm_device_get_ifindex(NM_DEVICE(self))); - config = nm_supplicant_config_new(NM_SUPPL_CAP_MASK_NONE); + config = nm_supplicant_config_new(NM_SUPPL_CAP_MASK_NONE, + nm_utils_get_connection_first_permissions_user(connection)); s_macsec = nm_device_get_applied_setting(NM_DEVICE(self), NM_TYPE_SETTING_MACSEC); @@ -227,7 +228,13 @@ build_supplicant_config(NMDeviceMacsec *self, GError **error) if (nm_setting_macsec_get_mode(s_macsec) == NM_SETTING_MACSEC_MODE_EAP) { s_8021x = nm_connection_get_setting_802_1x(connection); - if (!nm_supplicant_config_add_setting_8021x(config, s_8021x, con_uuid, mtu, TRUE, error)) { + if (!nm_supplicant_config_add_setting_8021x(config, + s_8021x, + con_uuid, + mtu, + TRUE, + nm_device_get_private_files(NM_DEVICE(self)), + error)) { g_prefix_error(error, "802-1x-setting: "); return NULL; } diff --git a/src/core/devices/wifi/nm-device-wifi.c b/src/core/devices/wifi/nm-device-wifi.c index 395e0c8e5f..b41ed5e118 100644 --- a/src/core/devices/wifi/nm-device-wifi.c +++ b/src/core/devices/wifi/nm-device-wifi.c @@ -2984,7 +2984,8 @@ build_supplicant_config(NMDeviceWifi *self, s_wireless = nm_connection_get_setting_wireless(connection); g_return_val_if_fail(s_wireless != NULL, NULL); - config = nm_supplicant_config_new(nm_supplicant_interface_get_capabilities(priv->sup_iface)); + config = nm_supplicant_config_new(nm_supplicant_interface_get_capabilities(priv->sup_iface), + nm_utils_get_connection_first_permissions_user(connection)); /* Warn if AP mode may not be supported */ if (nm_streq0(nm_setting_wireless_get_mode(s_wireless), NM_SETTING_WIRELESS_MODE_AP) @@ -3060,6 +3061,7 @@ build_supplicant_config(NMDeviceWifi *self, mtu, pmf, fils, + nm_device_get_private_files(NM_DEVICE(self)), error)) { g_prefix_error(error, "802-11-wireless-security: "); goto error; diff --git a/src/core/supplicant/nm-supplicant-config.c b/src/core/supplicant/nm-supplicant-config.c index 635174bdf0..fd360e7238 100644 --- a/src/core/supplicant/nm-supplicant-config.c +++ b/src/core/supplicant/nm-supplicant-config.c @@ -30,6 +30,7 @@ typedef struct { typedef struct { GHashTable *config; GHashTable *blobs; + char *private_user; NMSupplCapMask capabilities; guint32 ap_scan; bool fast_required : 1; @@ -60,7 +61,7 @@ _get_capability(NMSupplicantConfigPrivate *priv, NMSupplCapType type) } NMSupplicantConfig * -nm_supplicant_config_new(NMSupplCapMask capabilities) +nm_supplicant_config_new(NMSupplCapMask capabilities, const char *private_user) { NMSupplicantConfigPrivate *priv; NMSupplicantConfig *self; @@ -69,6 +70,7 @@ nm_supplicant_config_new(NMSupplCapMask capabilities) priv = NM_SUPPLICANT_CONFIG_GET_PRIVATE(self); priv->capabilities = capabilities; + priv->private_user = g_strdup(private_user); return self; } @@ -283,6 +285,7 @@ nm_supplicant_config_finalize(GObject *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); } @@ -930,6 +933,7 @@ nm_supplicant_config_add_setting_wireless_security(NMSupplicantConfig guint32 mtu, NMSettingWirelessSecurityPmf pmf, NMSettingWirelessSecurityFils fils, + GHashTable *files, GError **error) { NMSupplicantConfigPrivate *priv = NM_SUPPLICANT_CONFIG_GET_PRIVATE(self); @@ -1284,6 +1288,7 @@ nm_supplicant_config_add_setting_wireless_security(NMSupplicantConfig con_uuid, mtu, FALSE, + files, error)) return FALSE; } @@ -1365,6 +1370,7 @@ nm_supplicant_config_add_setting_8021x(NMSupplicantConfig *self, const char *con_uuid, guint32 mtu, gboolean wired, + GHashTable *files, GError **error) { NMSupplicantConfigPrivate *priv; @@ -1594,24 +1600,21 @@ nm_supplicant_config_add_setting_8021x(NMSupplicantConfig *self, } /* CA certificate */ + path = NULL; + bytes = NULL; if (ca_cert_override) { - if (!add_string_val(self, ca_cert_override, "ca_cert", FALSE, NULL, error)) - return FALSE; + /* 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); - if (!nm_supplicant_config_add_blob_for_connection(self, - bytes, - "ca_cert", - con_uuid, - error)) - return FALSE; break; case NM_SETTING_802_1X_CK_SCHEME_PATH: path = nm_setting_802_1x_get_ca_cert_path(setting); - if (!add_string_val(self, path, "ca_cert", FALSE, NULL, error)) - return FALSE; + 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, @@ -1627,26 +1630,32 @@ nm_supplicant_config_add_setting_8021x(NMSupplicantConfig *self, 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) { - if (!add_string_val(self, ca_cert_override, "ca_cert2", FALSE, NULL, error)) - return FALSE; + /* 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); - if (!nm_supplicant_config_add_blob_for_connection(self, - bytes, - "ca_cert2", - con_uuid, - error)) - return FALSE; break; case NM_SETTING_802_1X_CK_SCHEME_PATH: path = nm_setting_802_1x_get_phase2_ca_cert_path(setting); - if (!add_string_val(self, path, "ca_cert2", FALSE, NULL, error)) - return FALSE; + 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( @@ -1663,6 +1672,15 @@ nm_supplicant_config_add_setting_8021x(NMSupplicantConfig *self, 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); @@ -1714,21 +1732,17 @@ nm_supplicant_config_add_setting_8021x(NMSupplicantConfig *self, /* 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); - if (!nm_supplicant_config_add_blob_for_connection(self, - bytes, - "private_key", - con_uuid, - error)) - return FALSE; added = TRUE; break; case NM_SETTING_802_1X_CK_SCHEME_PATH: path = nm_setting_802_1x_get_private_key_path(setting); - if (!add_string_val(self, path, "private_key", FALSE, NULL, error)) - return FALSE; + if (priv->private_user) + bytes = nm_g_hash_table_lookup(files, path); added = TRUE; break; case NM_SETTING_802_1X_CK_SCHEME_PKCS11: @@ -1745,6 +1759,19 @@ nm_supplicant_config_add_setting_8021x(NMSupplicantConfig *self, 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; @@ -1768,20 +1795,16 @@ nm_supplicant_config_add_setting_8021x(NMSupplicantConfig *self, /* 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); - if (!nm_supplicant_config_add_blob_for_connection(self, - bytes, - "client_cert", - con_uuid, - error)) - return FALSE; break; case NM_SETTING_802_1X_CK_SCHEME_PATH: path = nm_setting_802_1x_get_client_cert_path(setting); - if (!add_string_val(self, path, "client_cert", FALSE, NULL, error)) - return FALSE; + 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( @@ -1797,26 +1820,35 @@ nm_supplicant_config_add_setting_8021x(NMSupplicantConfig *self, 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); - if (!nm_supplicant_config_add_blob_for_connection(self, - bytes, - "private_key2", - con_uuid, - error)) - return FALSE; added = TRUE; break; case NM_SETTING_802_1X_CK_SCHEME_PATH: path = nm_setting_802_1x_get_phase2_private_key_path(setting); - if (!add_string_val(self, path, "private_key2", FALSE, NULL, error)) - return FALSE; + if (priv->private_user) + bytes = nm_g_hash_table_lookup(files, path); added = TRUE; break; case NM_SETTING_802_1X_CK_SCHEME_PKCS11: @@ -1834,6 +1866,19 @@ nm_supplicant_config_add_setting_8021x(NMSupplicantConfig *self, 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; @@ -1857,20 +1902,16 @@ nm_supplicant_config_add_setting_8021x(NMSupplicantConfig *self, /* 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); - if (!nm_supplicant_config_add_blob_for_connection(self, - bytes, - "client_cert2", - con_uuid, - error)) - return FALSE; break; case NM_SETTING_802_1X_CK_SCHEME_PATH: path = nm_setting_802_1x_get_phase2_client_cert_path(setting); - if (!add_string_val(self, path, "client_cert2", FALSE, NULL, error)) - return FALSE; + 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( @@ -1886,6 +1927,19 @@ nm_supplicant_config_add_setting_8021x(NMSupplicantConfig *self, 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; + } } } diff --git a/src/core/supplicant/nm-supplicant-config.h b/src/core/supplicant/nm-supplicant-config.h index c52b756e78..96460b86c7 100644 --- a/src/core/supplicant/nm-supplicant-config.h +++ b/src/core/supplicant/nm-supplicant-config.h @@ -29,7 +29,7 @@ typedef struct _NMSupplicantConfigClass NMSupplicantConfigClass; GType nm_supplicant_config_get_type(void); -NMSupplicantConfig *nm_supplicant_config_new(NMSupplCapMask capabilities); +NMSupplicantConfig *nm_supplicant_config_new(NMSupplCapMask capabilities, const char *private_user); guint32 nm_supplicant_config_get_ap_scan(NMSupplicantConfig *self); @@ -57,6 +57,7 @@ gboolean nm_supplicant_config_add_setting_wireless_security(NMSupplicantConfig guint32 mtu, NMSettingWirelessSecurityPmf pmf, NMSettingWirelessSecurityFils fils, + GHashTable *files, GError **error); gboolean nm_supplicant_config_add_no_security(NMSupplicantConfig *self, GError **error); @@ -66,6 +67,7 @@ gboolean nm_supplicant_config_add_setting_8021x(NMSupplicantConfig *self, const char *con_uuid, guint32 mtu, gboolean wired, + GHashTable *files, GError **error); gboolean nm_supplicant_config_add_setting_macsec(NMSupplicantConfig *self, diff --git a/src/core/supplicant/tests/test-supplicant-config.c b/src/core/supplicant/tests/test-supplicant-config.c index 1ca5b26e56..416fe0054f 100644 --- a/src/core/supplicant/tests/test-supplicant-config.c +++ b/src/core/supplicant/tests/test-supplicant-config.c @@ -98,7 +98,8 @@ build_supplicant_config(NMConnection *connection, NMSetting8021x *s_8021x; gboolean success; - config = nm_supplicant_config_new(capabilities); + config = nm_supplicant_config_new(capabilities, + nm_utils_get_connection_first_permissions_user(connection)); s_wifi = nm_connection_get_setting_wireless(connection); g_assert(s_wifi); @@ -120,6 +121,7 @@ build_supplicant_config(NMConnection *connection, mtu, pmf, fils, + NULL, &error); } else { success = nm_supplicant_config_add_no_security(config, &error); From e3c27f2a22b75c98c300c5ba6249193b9047eaaf Mon Sep 17 00:00:00 2001 From: Beniamino Galvani Date: Mon, 27 Oct 2025 17:40:14 +0100 Subject: [PATCH 16/56] core,libnm-core: introduce property flag for certificate and keys If we add a new property in the future and it references a certificate or key stored on disk, we need to also implement the logic to verify the access to the file for private connections. Add a new property flag NM_SETTING_PARAM_CERT_KEY_FILE to existing certificate and key properties, so that it's easier to see that they need special treatment. Also add some assertions to verify that the properties with the flag are handled properly. While at it, move the enumeration of private-files to the settings. (cherry picked from commit acbfae5e051af8647e32d14ccc6be05419dcca77) --- src/core/nm-core-utils.c | 46 +++-------- src/libnm-core-impl/nm-setting-8021x.c | 97 ++++++++++++++++++++++-- src/libnm-core-impl/nm-setting-private.h | 17 ++++- src/libnm-core-impl/nm-setting.c | 29 +++++++ src/libnm-core-intern/nm-core-internal.h | 2 + 5 files changed, 143 insertions(+), 48 deletions(-) diff --git a/src/core/nm-core-utils.c b/src/core/nm-core-utils.c index ee50de5a81..5404ecb9ce 100644 --- a/src/core/nm-core-utils.c +++ b/src/core/nm-core-utils.c @@ -5667,47 +5667,19 @@ nm_utils_get_connection_first_permissions_user(NMConnection *connection) /*****************************************************************************/ -static void -get_8021x_private_files(NMConnection *connection, GPtrArray *files) -{ - const struct { - NMSetting8021xCKScheme (*get_scheme_func)(NMSetting8021x *); - const char *(*get_path_func)(NMSetting8021x *); - } funcs[] = { - {nm_setting_802_1x_get_ca_cert_scheme, nm_setting_802_1x_get_ca_cert_path}, - {nm_setting_802_1x_get_client_cert_scheme, nm_setting_802_1x_get_client_cert_path}, - {nm_setting_802_1x_get_private_key_scheme, nm_setting_802_1x_get_private_key_path}, - {nm_setting_802_1x_get_phase2_ca_cert_scheme, nm_setting_802_1x_get_phase2_ca_cert_path}, - {nm_setting_802_1x_get_phase2_client_cert_scheme, - nm_setting_802_1x_get_phase2_client_cert_path}, - {nm_setting_802_1x_get_phase2_private_key_scheme, - nm_setting_802_1x_get_phase2_private_key_path}, - }; - NMSetting8021x *s_8021x; - const char *path; - guint i; - - s_8021x = nm_connection_get_setting_802_1x(connection); - if (!s_8021x) - return; - - for (i = 0; i < G_N_ELEMENTS(funcs); i++) { - if (funcs[i].get_scheme_func(s_8021x) == NM_SETTING_802_1X_CK_SCHEME_PATH) { - path = funcs[i].get_path_func(s_8021x); - if (path) { - g_ptr_array_add(files, (gpointer) path); - } - } - } -} - const char ** nm_utils_get_connection_private_files_paths(NMConnection *connection) { - GPtrArray *files; + GPtrArray *files; + gs_free NMSetting **settings = NULL; + guint num_settings; + guint i; - files = g_ptr_array_new(); - get_8021x_private_files(connection, files); + files = g_ptr_array_new(); + settings = nm_connection_get_settings(connection, &num_settings); + for (i = 0; i < num_settings; i++) { + _nm_setting_get_private_files(settings[i], files); + } g_ptr_array_add(files, NULL); return (const char **) g_ptr_array_free(files, files->len == 1); diff --git a/src/libnm-core-impl/nm-setting-8021x.c b/src/libnm-core-impl/nm-setting-8021x.c index 4ea6072966..f933380333 100644 --- a/src/libnm-core-impl/nm-setting-8021x.c +++ b/src/libnm-core-impl/nm-setting-8021x.c @@ -3133,6 +3133,86 @@ need_secrets(NMSetting *setting, gboolean check_rerequest) /*****************************************************************************/ +static void +get_private_files(NMSetting *setting, GPtrArray *files) +{ + const struct { + const char *property; + NMSetting8021xCKScheme (*get_scheme_func)(NMSetting8021x *); + const char *(*get_path_func)(NMSetting8021x *); + } cert_props[] = { + {NM_SETTING_802_1X_CA_CERT, + nm_setting_802_1x_get_ca_cert_scheme, + nm_setting_802_1x_get_ca_cert_path}, + {NM_SETTING_802_1X_CLIENT_CERT, + nm_setting_802_1x_get_client_cert_scheme, + nm_setting_802_1x_get_client_cert_path}, + {NM_SETTING_802_1X_PRIVATE_KEY, + nm_setting_802_1x_get_private_key_scheme, + nm_setting_802_1x_get_private_key_path}, + {NM_SETTING_802_1X_PHASE2_CA_CERT, + nm_setting_802_1x_get_phase2_ca_cert_scheme, + nm_setting_802_1x_get_phase2_ca_cert_path}, + {NM_SETTING_802_1X_PHASE2_CLIENT_CERT, + nm_setting_802_1x_get_phase2_client_cert_scheme, + nm_setting_802_1x_get_phase2_client_cert_path}, + {NM_SETTING_802_1X_PHASE2_PRIVATE_KEY, + nm_setting_802_1x_get_phase2_private_key_scheme, + nm_setting_802_1x_get_phase2_private_key_path}, + }; + NMSetting8021x *s_8021x = NM_SETTING_802_1X(setting); + const char *path; + guint i; + + if (NM_MORE_ASSERT_ONCE(5)) { + GObjectClass *klass; + gs_free GParamSpec **properties = NULL; + guint n_properties; + gboolean found; + guint j; + + /* Check that all the properties in the setting with flag CERT_KEY_FILE + * are listed in the table, and vice versa. */ + + klass = G_OBJECT_GET_CLASS(setting); + + properties = g_object_class_list_properties(klass, &n_properties); + for (i = 0; i < n_properties; i++) { + if (!(properties[i]->flags & NM_SETTING_PARAM_CERT_KEY_FILE)) + continue; + + found = FALSE; + for (j = 0; j < G_N_ELEMENTS(cert_props); j++) { + if (nm_streq0(properties[i]->name, cert_props[j].property)) { + found = TRUE; + break; + } + } + + nm_assert(found); + } + + for (i = 0; i < G_N_ELEMENTS(cert_props); i++) { + GParamSpec *prop; + + prop = g_object_class_find_property(klass, cert_props[i].property); + nm_assert(prop); + nm_assert(prop->flags & NM_SETTING_PARAM_CERT_KEY_FILE); + } + } + + for (i = 0; i < G_N_ELEMENTS(cert_props); i++) { + if (cert_props[i].get_scheme_func(s_8021x) == NM_SETTING_802_1X_CK_SCHEME_PATH) { + path = cert_props[i].get_path_func(s_8021x); + if (path) { + g_ptr_array_add(files, (gpointer) path); + } + } + } +} + +/*****************************************************************************/ + static void get_property(GObject *object, guint prop_id, GValue *value, GParamSpec *pspec) { @@ -3223,8 +3303,9 @@ nm_setting_802_1x_class_init(NMSetting8021xClass *klass) object_class->set_property = set_property; object_class->finalize = finalize; - setting_class->verify = verify; - setting_class->need_secrets = need_secrets; + setting_class->verify = verify; + setting_class->need_secrets = need_secrets; + setting_class->get_private_files = get_private_files; /** * NMSetting8021x:eap: @@ -3359,7 +3440,7 @@ nm_setting_802_1x_class_init(NMSetting8021xClass *klass) obj_properties, NM_SETTING_802_1X_CA_CERT, PROP_CA_CERT, - NM_SETTING_PARAM_NONE, + NM_SETTING_PARAM_CERT_KEY_FILE, NMSetting8021xPrivate, ca_cert); @@ -3556,7 +3637,7 @@ nm_setting_802_1x_class_init(NMSetting8021xClass *klass) obj_properties, NM_SETTING_802_1X_CLIENT_CERT, PROP_CLIENT_CERT, - NM_SETTING_PARAM_NONE, + NM_SETTING_PARAM_CERT_KEY_FILE, NMSetting8021xPrivate, client_cert); @@ -3803,7 +3884,7 @@ nm_setting_802_1x_class_init(NMSetting8021xClass *klass) obj_properties, NM_SETTING_802_1X_PHASE2_CA_CERT, PROP_PHASE2_CA_CERT, - NM_SETTING_PARAM_NONE, + NM_SETTING_PARAM_CERT_KEY_FILE, NMSetting8021xPrivate, phase2_ca_cert); @@ -4006,7 +4087,7 @@ nm_setting_802_1x_class_init(NMSetting8021xClass *klass) obj_properties, NM_SETTING_802_1X_PHASE2_CLIENT_CERT, PROP_PHASE2_CLIENT_CERT, - NM_SETTING_PARAM_NONE, + NM_SETTING_PARAM_CERT_KEY_FILE, NMSetting8021xPrivate, phase2_client_cert); @@ -4175,7 +4256,7 @@ nm_setting_802_1x_class_init(NMSetting8021xClass *klass) obj_properties, NM_SETTING_802_1X_PRIVATE_KEY, PROP_PRIVATE_KEY, - NM_SETTING_PARAM_NONE, + NM_SETTING_PARAM_CERT_KEY_FILE, NMSetting8021xPrivate, private_key); @@ -4276,7 +4357,7 @@ nm_setting_802_1x_class_init(NMSetting8021xClass *klass) obj_properties, NM_SETTING_802_1X_PHASE2_PRIVATE_KEY, PROP_PHASE2_PRIVATE_KEY, - NM_SETTING_PARAM_NONE, + NM_SETTING_PARAM_CERT_KEY_FILE, NMSetting8021xPrivate, phase2_private_key); diff --git a/src/libnm-core-impl/nm-setting-private.h b/src/libnm-core-impl/nm-setting-private.h index 8ee770f471..61f9678936 100644 --- a/src/libnm-core-impl/nm-setting-private.h +++ b/src/libnm-core-impl/nm-setting-private.h @@ -154,6 +154,11 @@ struct _NMSettingClass { guint /* NMSettingParseFlags */ parse_flags, GError **error); + /* returns a list of certificate/key files referenced in the connection. + * When the connection is private, we need to verify that the owner of + * the connection has access to them. */ + void (*get_private_files)(NMSetting *setting, GPtrArray *files); + const struct _NMMetaSettingInfo *setting_info; }; @@ -334,6 +339,11 @@ struct _NMRange { */ #define NM_SETTING_PARAM_TO_DBUS_IGNORE_FLAGS (1 << (7 + G_PARAM_USER_SHIFT)) +/* The property can refer to a certificate or key stored on disk. As such, + * special care is needed when accessing the file for private connections. + */ +#define NM_SETTING_PARAM_CERT_KEY_FILE (1 << (8 + G_PARAM_USER_SHIFT)) + extern const NMSettInfoPropertType nm_sett_info_propert_type_setting_name; extern const NMSettInfoPropertType nm_sett_info_propert_type_deprecated_interface_name; extern const NMSettInfoPropertType nm_sett_info_propert_type_deprecated_ignore_i; @@ -859,9 +869,10 @@ _nm_properties_override(GArray *properties_override, const NMSettInfoProperty *p { \ GParamSpec *_param_spec; \ \ - G_STATIC_ASSERT(!NM_FLAGS_ANY((param_flags), \ - ~(NM_SETTING_PARAM_SECRET | NM_SETTING_PARAM_INFERRABLE \ - | NM_SETTING_PARAM_FUZZY_IGNORE))); \ + G_STATIC_ASSERT( \ + !NM_FLAGS_ANY((param_flags), \ + ~(NM_SETTING_PARAM_SECRET | NM_SETTING_PARAM_INFERRABLE \ + | NM_SETTING_PARAM_FUZZY_IGNORE | NM_SETTING_PARAM_CERT_KEY_FILE))); \ \ _param_spec = g_param_spec_boxed("" prop_name "", \ "", \ diff --git a/src/libnm-core-impl/nm-setting.c b/src/libnm-core-impl/nm-setting.c index 98424c76fc..295eabe7a6 100644 --- a/src/libnm-core-impl/nm-setting.c +++ b/src/libnm-core-impl/nm-setting.c @@ -2262,6 +2262,34 @@ init_from_dbus(NMSetting *setting, return TRUE; } +static void +get_private_files(NMSetting *setting, GPtrArray *files) +{ + if (NM_MORE_ASSERTS) { + GParamSpec **properties; + guint n_properties; + int i; + + properties = g_object_class_list_properties(G_OBJECT_GET_CLASS(setting), &n_properties); + for (i = 0; i < n_properties; i++) { + if (properties[i]->flags & NM_SETTING_PARAM_CERT_KEY_FILE) { + /* Certificates and keys needs special handling, see setting 802.1X */ + nm_assert_not_reached(); + } + } + g_free(properties); + } +} + +void +_nm_setting_get_private_files(NMSetting *setting, GPtrArray *files) +{ + g_return_if_fail(NM_IS_SETTING(setting)); + g_return_if_fail(files); + + NM_SETTING_GET_CLASS(setting)->get_private_files(setting, files); +} + /** * nm_setting_get_dbus_property_type: * @setting: an #NMSetting @@ -4672,6 +4700,7 @@ nm_setting_class_init(NMSettingClass *setting_class) setting_class->enumerate_values = enumerate_values; setting_class->aggregate = aggregate; setting_class->init_from_dbus = init_from_dbus; + setting_class->get_private_files = get_private_files; /** * NMSetting:name: diff --git a/src/libnm-core-intern/nm-core-internal.h b/src/libnm-core-intern/nm-core-internal.h index 6991185d39..b8df4d2e9d 100644 --- a/src/libnm-core-intern/nm-core-internal.h +++ b/src/libnm-core-intern/nm-core-internal.h @@ -1192,4 +1192,6 @@ const GPtrArray *_nm_setting_ovs_port_get_trunks_arr(NMSettingOvsPort *self); guint _nm_setting_connection_get_num_permissions_users(NMSettingConnection *setting); const char *_nm_setting_connection_get_first_permissions_user(NMSettingConnection *setting); +void _nm_setting_get_private_files(NMSetting *setting, GPtrArray *files); + #endif From 8437e14758d1d70de2c01b43685f47101967b3e5 Mon Sep 17 00:00:00 2001 From: Beniamino Galvani Date: Fri, 10 Oct 2025 15:08:34 +0200 Subject: [PATCH 17/56] vpn: add nm_vpn_plugin_info_supports_safe_private_file_access() The new API indicates that the VPN plugin supports reading files (certificates, keys) of private connections in a safe way (i.e. checking user permissions), or that it doesn't need to read any file from disk. (cherry picked from commit 10db4baeb6d3eef76cf036b2f342ab61caa29764) --- src/libnm-client-impl/libnm.ver | 1 + src/libnm-core-impl/nm-vpn-plugin-info.c | 23 ++++++++++++++++++++++ src/libnm-core-public/nm-vpn-plugin-info.h | 2 ++ 3 files changed, 26 insertions(+) diff --git a/src/libnm-client-impl/libnm.ver b/src/libnm-client-impl/libnm.ver index 1fb3282d82..92183be2b3 100644 --- a/src/libnm-client-impl/libnm.ver +++ b/src/libnm-client-impl/libnm.ver @@ -2090,4 +2090,5 @@ global: nm_setting_gsm_get_device_uid; nm_setting_connection_get_dnssec; nm_setting_connection_dnssec_get_type; + nm_vpn_plugin_info_supports_safe_private_file_access; } libnm_1_54_0; diff --git a/src/libnm-core-impl/nm-vpn-plugin-info.c b/src/libnm-core-impl/nm-vpn-plugin-info.c index 223d8ab33b..47dc9e3b5a 100644 --- a/src/libnm-core-impl/nm-vpn-plugin-info.c +++ b/src/libnm-core-impl/nm-vpn-plugin-info.c @@ -913,6 +913,29 @@ nm_vpn_plugin_info_supports_multiple(NMVpnPluginInfo *self) return _nm_utils_ascii_str_to_bool(s, FALSE); } +/** + * nm_vpn_plugin_info_supports_safe_private_file_access: + * @self: plugin info instance + * + * Returns: %TRUE if the service supports reading files (certificates, keys) of + * private connections in a safe way (i.e. checking user permissions), or + if the service doesn't need to read any file from disk. + * + * Since: 1.56 + */ +gboolean +nm_vpn_plugin_info_supports_safe_private_file_access(NMVpnPluginInfo *self) +{ + const char *s; + + g_return_val_if_fail(NM_IS_VPN_PLUGIN_INFO(self), FALSE); + + s = nm_vpn_plugin_info_lookup_property(self, + NM_VPN_PLUGIN_INFO_KF_GROUP_CONNECTION, + "supports-safe-private-file-access"); + return _nm_utils_ascii_str_to_bool(s, FALSE); +} + /** * nm_vpn_plugin_info_get_aliases: * @self: plugin info instance diff --git a/src/libnm-core-public/nm-vpn-plugin-info.h b/src/libnm-core-public/nm-vpn-plugin-info.h index c045daa03d..0f2618ac9a 100644 --- a/src/libnm-core-public/nm-vpn-plugin-info.h +++ b/src/libnm-core-public/nm-vpn-plugin-info.h @@ -64,6 +64,8 @@ NM_AVAILABLE_IN_1_4 gboolean nm_vpn_plugin_info_supports_hints(NMVpnPluginInfo *self); NM_AVAILABLE_IN_1_42 gboolean nm_vpn_plugin_info_supports_multiple(NMVpnPluginInfo *self); +NM_AVAILABLE_IN_1_56 +gboolean nm_vpn_plugin_info_supports_safe_private_file_access(NMVpnPluginInfo *self); NM_AVAILABLE_IN_1_4 const char *const *nm_vpn_plugin_info_get_aliases(NMVpnPluginInfo *self); NM_AVAILABLE_IN_1_2 From 3d85bace3dcd8aaf9773db90fa412a7cdc131e4b Mon Sep 17 00:00:00 2001 From: Beniamino Galvani Date: Fri, 26 Sep 2025 21:04:04 +0200 Subject: [PATCH 18/56] libnm: add function to copy a certificate or key as user Add a new public function nm_utils_copy_cert_as_user() to libnm. It reads a certificate or key file on behalf of the given user and writes it to a directory in /run/NetworkManager. It is useful for VPN plugins that run as root and need to verify that the user owning the connection (the one listed in the connection.permissions property) can access the file. (cherry picked from commit 1a52bbe7c9dcabc066d8930dfd7b7cfe74dabf78) --- NEWS | 2 + contrib/fedora/rpm/NetworkManager.spec | 1 + src/libnm-client-impl/libnm.ver | 1 + src/libnm-client-impl/tests/meson.build | 16 +- .../tests/test-copy-cert-as-user.c | 32 +++ src/libnm-core-impl/nm-utils.c | 256 ++++++++++++++++++ src/libnm-core-public/nm-utils.h | 3 + src/nm-helpers/README.md | 8 + src/nm-helpers/meson.build | 20 ++ src/nm-helpers/nm-libnm-helper.c | 45 +++ 10 files changed, 378 insertions(+), 6 deletions(-) create mode 100644 src/libnm-client-impl/tests/test-copy-cert-as-user.c create mode 100644 src/nm-helpers/nm-libnm-helper.c diff --git a/NEWS b/NEWS index 563927584c..7d8d9abddc 100644 --- a/NEWS +++ b/NEWS @@ -6,6 +6,8 @@ Overview of changes since NetworkManager-1.56.0 * For private connections (the ones that specify a user in the "connection.permissions" property), verify that the user can access the 802.1X certificates and keys set in the connection. +* Introduce a libnm function that can be used by VPN plugins to check + user permissions on certificate and keys. ============================================= NetworkManager-1.56 diff --git a/contrib/fedora/rpm/NetworkManager.spec b/contrib/fedora/rpm/NetworkManager.spec index 0732fb495f..ca22591a64 100644 --- a/contrib/fedora/rpm/NetworkManager.spec +++ b/contrib/fedora/rpm/NetworkManager.spec @@ -880,6 +880,7 @@ fi %{_libexecdir}/nm-dispatcher %{_libexecdir}/nm-initrd-generator %{_libexecdir}/nm-daemon-helper +%{_libexecdir}/nm-libnm-helper %{_libexecdir}/nm-priv-helper %dir %{_libdir}/%{name} %dir %{nmplugindir} diff --git a/src/libnm-client-impl/libnm.ver b/src/libnm-client-impl/libnm.ver index 92183be2b3..ed5901d79f 100644 --- a/src/libnm-client-impl/libnm.ver +++ b/src/libnm-client-impl/libnm.ver @@ -2090,5 +2090,6 @@ global: nm_setting_gsm_get_device_uid; nm_setting_connection_get_dnssec; nm_setting_connection_dnssec_get_type; + nm_utils_copy_cert_as_user; nm_vpn_plugin_info_supports_safe_private_file_access; } libnm_1_54_0; diff --git a/src/libnm-client-impl/tests/meson.build b/src/libnm-client-impl/tests/meson.build index 42e9883e77..500504db6e 100644 --- a/src/libnm-client-impl/tests/meson.build +++ b/src/libnm-client-impl/tests/meson.build @@ -5,6 +5,7 @@ test_units = [ 'test-nm-client', 'test-remote-settings-client', 'test-secret-agent', + 'test-copy-cert-as-user' ] foreach test_unit: test_units @@ -37,12 +38,15 @@ foreach test_unit: test_units ], ) - test( - 'src/libnm-client-impl/tests/' + test_unit, - test_script, - timeout: 90, - args: test_args + [exe.full_path()], - ) + # test-copy-cert-as-user is a manual test, don't run it automatically + if test_unit != 'test-copy-cert-as-user' + test( + 'src/libnm-client-impl/tests/' + test_unit, + test_script, + timeout: 90, + args: test_args + [exe.full_path()], + ) + endif endforeach if enable_introspection diff --git a/src/libnm-client-impl/tests/test-copy-cert-as-user.c b/src/libnm-client-impl/tests/test-copy-cert-as-user.c new file mode 100644 index 0000000000..b2ef9de67d --- /dev/null +++ b/src/libnm-client-impl/tests/test-copy-cert-as-user.c @@ -0,0 +1,32 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +/* + * This is a program to manually test the + * nm_utils_copy_cert_as_user() libnm function. + */ + +#include "libnm-client-impl/nm-default-libnm.h" + +#include "nm-utils.h" + +int +main(int argc, char **argv) +{ + gs_free_error GError *error = NULL; + gs_free char *filename = NULL; + + if (argc != 3) { + g_printerr("Usage: %s \n", argv[0]); + return 1; + } + + filename = nm_utils_copy_cert_as_user(argv[1], argv[2], &error); + if (!filename) { + g_printerr("Error: %s\n", error->message); + return 1; + } + + g_print("%s\n", filename); + + return 0; +} diff --git a/src/libnm-core-impl/nm-utils.c b/src/libnm-core-impl/nm-utils.c index 6d5df98ccc..1bf00831f9 100644 --- a/src/libnm-core-impl/nm-utils.c +++ b/src/libnm-core-impl/nm-utils.c @@ -17,6 +17,7 @@ #include #include +#include "libnm-glib-aux/nm-io-utils.h" #include "libnm-glib-aux/nm-uuid.h" #include "libnm-glib-aux/nm-json-aux.h" #include "libnm-glib-aux/nm-str-buf.h" @@ -6195,3 +6196,258 @@ nm_utils_ensure_gtypes(void) for (meta_type = 0; meta_type < _NM_META_SETTING_TYPE_NUM; meta_type++) nm_meta_setting_infos[meta_type].get_setting_gtype(); } + +/*****************************************************************************/ + +typedef struct { + GPid pid; + GSource *child_watch_source; + GMainLoop *loop; + GError *error; + + int child_stdout; + int child_stderr; + + GSource *output_source; + GSource *error_source; + + NMStrBuf output_buffer; + NMStrBuf error_buffer; +} HelperInfo; + +static void +helper_complete(HelperInfo *info, GError *error_take) +{ + if (error_take) { + if (!info->error) + info->error = error_take; + else + g_error_free(error_take); + } + + if (info->output_source || info->error_source || info->pid != -1) { + /* Wait that the pipe is closed and process has terminated */ + return; + } + + if (info->error && info->error_buffer.len > 0) { + /* Prefer the message from stderr as it's more informative */ + g_error_free(info->error); + info->error = g_error_new(NM_CONNECTION_ERROR, + NM_CONNECTION_ERROR_FAILED, + "%s", + nm_str_buf_get_str(&info->error_buffer)); + } + + g_main_loop_quit(info->loop); +} + +static gboolean +helper_have_err_data(int fd, GIOCondition condition, gpointer user_data) +{ + HelperInfo *info = user_data; + gssize n_read; + GError *error = NULL; + + n_read = nm_utils_fd_read(fd, &info->error_buffer); + + if (n_read > 0) + return G_SOURCE_CONTINUE; + + nm_clear_g_source_inst(&info->error_source); + nm_clear_fd(&info->child_stderr); + + if (n_read < 0) { + error = g_error_new(NM_UTILS_ERROR, + NM_UTILS_ERROR_UNKNOWN, + "read from process returned %d (%s)", + (int) -n_read, + nm_strerror_native((int) -n_read)); + } + + helper_complete(info, error); + return G_SOURCE_CONTINUE; +} + +static gboolean +helper_have_data(int fd, GIOCondition condition, gpointer user_data) +{ + HelperInfo *info = user_data; + gssize n_read; + GError *error = NULL; + + n_read = nm_utils_fd_read(fd, &info->output_buffer); + + if (n_read > 0) + return G_SOURCE_CONTINUE; + + nm_clear_g_source_inst(&info->output_source); + nm_clear_fd(&info->child_stdout); + + if (n_read < 0) { + error = g_error_new(NM_UTILS_ERROR, + NM_UTILS_ERROR_UNKNOWN, + "read from process returned %d (%s)", + (int) -n_read, + nm_strerror_native((int) -n_read)); + } + + helper_complete(info, error); + return G_SOURCE_CONTINUE; +} + +static void +helper_child_terminated(GPid pid, int status, gpointer user_data) +{ + HelperInfo *info = user_data; + gs_free char *status_desc = NULL; + GError *error = NULL; + + info->pid = -1; + nm_clear_g_source_inst(&info->child_watch_source); + + if (!WIFEXITED(status) || WEXITSTATUS(status) != 0) { + if (!status_desc) + status_desc = nm_utils_get_process_exit_status_desc(status); + error = + g_error_new(NM_UTILS_ERROR, NM_UTILS_ERROR_UNKNOWN, "helper process %s", status_desc); + } + + helper_complete(info, error); +} + +#define RUN_CERT_DIR NMRUNDIR "/cert" + +/** + * nm_utils_copy_cert_as_user: + * @filename: the file name of the certificate or key to copy + * @user: the user to impersonate when reading the file + * @error: (nullable): return location for a #GError, or %NULL + * + * Reads @filename on behalf of user @user and writes the + * content to a new file in /run/NetworkManager/cert/. + * The new file has permission 600 and is owned by root. + * + * This function is useful for VPN plugins that run as root and need + * to verify that the user owning the connection (the one listed in the + * connection.permissions property) can access the file. + * + * Returns: (transfer full): the name of the new temporary file. Or %NULL + * if an error occurred, including when the given user can't access the + * file. + * + * Since: 1.56 + */ +char * +nm_utils_copy_cert_as_user(const char *filename, const char *user, GError **error) +{ + gs_unref_bytes GBytes *bytes = NULL; + char dst_path[] = RUN_CERT_DIR "/XXXXXX"; + HelperInfo info = { + .child_stdout = -1, + .child_stderr = -1, + }; + GMainContext *context; + int fd = -1; + + g_return_val_if_fail(filename, NULL); + g_return_val_if_fail(user, NULL); + g_return_val_if_fail(!error || !*error, NULL); + + if (geteuid() != 0) { + g_set_error_literal(error, + NM_CONNECTION_ERROR, + NM_CONNECTION_ERROR_INVALID_PROPERTY, + _("This function needs to be called by root")); + return NULL; + } + + if (!g_spawn_async_with_pipes( + "/", + (char **) + NM_MAKE_STRV(LIBEXECDIR "/nm-libnm-helper", "read-file-as-user", filename, user), + (char **) NM_MAKE_STRV(), + G_SPAWN_CLOEXEC_PIPES | G_SPAWN_DO_NOT_REAP_CHILD, + NULL, + NULL, + &info.pid, + NULL, + &info.child_stdout, + &info.child_stderr, + error)) { + return NULL; + } + + context = g_main_context_new(); + info.loop = g_main_loop_new(context, FALSE); + + /* Watch process */ + info.child_watch_source = nm_g_child_watch_source_new(info.pid, + G_PRIORITY_DEFAULT, + helper_child_terminated, + &info, + NULL); + g_source_attach(info.child_watch_source, context); + + /* Watch stdout */ + info.output_buffer = NM_STR_BUF_INIT(0, FALSE); + info.output_source = nm_g_unix_fd_source_new(info.child_stdout, + G_IO_IN | G_IO_ERR | G_IO_HUP, + G_PRIORITY_DEFAULT, + helper_have_data, + &info, + NULL); + g_source_attach(info.output_source, context); + + /* Watch stderr */ + info.error_buffer = NM_STR_BUF_INIT(0, FALSE); + info.error_source = nm_g_unix_fd_source_new(info.child_stderr, + G_IO_IN | G_IO_ERR | G_IO_HUP, + G_PRIORITY_DEFAULT, + helper_have_err_data, + &info, + NULL); + g_source_attach(info.error_source, context); + + /* Wait termination */ + g_main_loop_run(info.loop); + g_clear_pointer(&info.loop, g_main_loop_unref); + g_clear_pointer(&context, g_main_context_unref); + + if (info.error) { + nm_str_buf_destroy(&info.output_buffer); + nm_str_buf_destroy(&info.error_buffer); + g_propagate_error(error, g_steal_pointer(&info.error)); + return NULL; + } + + /* Write the data to a new file */ + + bytes = g_bytes_new(nm_str_buf_get_str_unsafe(&info.output_buffer), info.output_buffer.len); + nm_str_buf_destroy(&info.output_buffer); + nm_str_buf_destroy(&info.error_buffer); + + mkdir(RUN_CERT_DIR, 0600); + fd = mkstemp(dst_path); + if (fd < 0) { + g_set_error_literal(error, + NM_CONNECTION_ERROR, + NM_CONNECTION_ERROR_INVALID_PROPERTY, + _("Failure creating the temporary file")); + return NULL; + } + nm_close(fd); + + if (!nm_utils_file_set_contents(dst_path, + g_bytes_get_data(bytes, NULL), + g_bytes_get_size(bytes), + 0600, + NULL, + NULL, + NULL, + error)) { + return NULL; + } + + return g_strdup(dst_path); +} diff --git a/src/libnm-core-public/nm-utils.h b/src/libnm-core-public/nm-utils.h index 35ef580db7..e46bf47280 100644 --- a/src/libnm-core-public/nm-utils.h +++ b/src/libnm-core-public/nm-utils.h @@ -261,6 +261,9 @@ nm_utils_base64secret_decode(const char *base64_key, gsize required_key_len, gui NM_AVAILABLE_IN_1_42 void nm_utils_ensure_gtypes(void); +NM_AVAILABLE_IN_1_56 +char *nm_utils_copy_cert_as_user(const char *filename, const char *user, GError **error); + G_END_DECLS #endif /* __NM_UTILS_H__ */ diff --git a/src/nm-helpers/README.md b/src/nm-helpers/README.md index ab0ea02444..66a9429221 100644 --- a/src/nm-helpers/README.md +++ b/src/nm-helpers/README.md @@ -17,6 +17,14 @@ all the threads of the process). This is not directly useful to the user. +nm-libnm-helper +--------------- + +A internal helper application that is spawned by libnm to perform +certain actions without impacting the calling process. + +This is not directly useful to the user. + nm-priv-helper -------------- diff --git a/src/nm-helpers/meson.build b/src/nm-helpers/meson.build index 5f330cbc94..7c148079d2 100644 --- a/src/nm-helpers/meson.build +++ b/src/nm-helpers/meson.build @@ -18,6 +18,26 @@ executable( install_dir: nm_libexecdir, ) +# nm-libnm-helper + +executable( + 'nm-libnm-helper', + ['nm-libnm-helper.c'], + include_directories : [ + src_inc, + top_inc, + ], + dependencies: [ + glib_dep, + ], + link_with: [ + libnm_glib_aux, + libnm_std_aux, + ], + install: true, + install_dir: nm_libexecdir, +) + # nm-priv-helper configure_file( diff --git a/src/nm-helpers/nm-libnm-helper.c b/src/nm-helpers/nm-libnm-helper.c new file mode 100644 index 0000000000..bd0ba67d94 --- /dev/null +++ b/src/nm-helpers/nm-libnm-helper.c @@ -0,0 +1,45 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include "libnm-std-aux/nm-default-std.h" + +#include + +enum { + RETURN_SUCCESS = 0, + RETURN_INVALID_CMD = 1, + RETURN_INVALID_ARGS = 2, + RETURN_ERROR = 3, +}; + +static int +read_file_as_user(const char *filename, const char *user) +{ + char error[1024]; + + if (!nm_utils_set_effective_user(user, error, sizeof(error))) { + fprintf(stderr, "Failed to set effective user '%s': %s", user, error); + return RETURN_ERROR; + } + + if (!nm_utils_read_file_to_stdout(filename, error, sizeof(error))) { + fprintf(stderr, "Failed to read file '%s' as user '%s': %s", filename, user, error); + return RETURN_ERROR; + } + + return RETURN_SUCCESS; +} + +int +main(int argc, char **argv) +{ + if (argc <= 1) + return RETURN_INVALID_CMD; + + if (nm_streq(argv[1], "read-file-as-user")) { + if (argc != 4) + return RETURN_INVALID_ARGS; + return read_file_as_user(argv[2], argv[3]); + } + + return RETURN_INVALID_CMD; +} \ No newline at end of file From a9d7154fe1b68bb634dba8872e47b96eaef7af62 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=8D=C3=B1igo=20Huguet?= Date: Fri, 12 Dec 2025 12:00:31 +0100 Subject: [PATCH 19/56] nm-version: set API_VERSION with MICRO+1 (temporary) In the past, stable branches used odd micro numbers as development micro version. Because of that, NM_API_VERSION was defined with MICRO+1 so we don't get warnings during development. As we stopped using odd micro=devel it is wrong to set MICRO+1 on odd releases. Final users of 1.52.3 has NM_API_VERSION 1.52.4. However, during development we need to have MICRO+1. For example, if we are working on top of 1.52.3 towards the next 1.52.4, we define new symbols with NM_AVAILABLE_IN_1_52_4. Because of that, we get compilation failures until we finally bump to 1.52.4, just before the release. The CI remains red until then, potentially missing many bugs. For now, just set MICRO+1 all the time. It is wrong, but it was wrong half of the time anyway, and at least we'll have a green CI until we implement a definitive solution. (cherry picked from commit 13bfa44cebf504e88e2ac00ab85145119263d8fe) --- src/libnm-core-public/nm-version-macros.h.in | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/libnm-core-public/nm-version-macros.h.in b/src/libnm-core-public/nm-version-macros.h.in index 49f283f6ea..9384917c3b 100644 --- a/src/libnm-core-public/nm-version-macros.h.in +++ b/src/libnm-core-public/nm-version-macros.h.in @@ -87,10 +87,10 @@ * version, you are already using the future API, even if * it is not yet released. Hence, the currently used API * version is the future one. */ -#define NM_API_VERSION \ - (((NM_MINOR_VERSION % 2) == 1) \ - ? NM_ENCODE_VERSION (NM_MAJOR_VERSION, NM_MINOR_VERSION + 1, 0 ) \ - : NM_ENCODE_VERSION (NM_MAJOR_VERSION, NM_MINOR_VERSION , ((NM_MICRO_VERSION + 1) / 2) * 2)) +#define NM_API_VERSION \ + (((NM_MINOR_VERSION % 2) == 1) \ + ? NM_ENCODE_VERSION(NM_MAJOR_VERSION, NM_MINOR_VERSION + 1, 0) \ + : NM_ENCODE_VERSION(NM_MAJOR_VERSION, NM_MINOR_VERSION, NM_MICRO_VERSION + 1)) /* deprecated. */ #define NM_VERSION_CUR_STABLE NM_API_VERSION From ea759ccf3abc45cc7aeb2982c36a1301b5ea5732 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=8D=C3=B1igo=20Huguet?= Date: Fri, 12 Dec 2025 14:26:31 +0100 Subject: [PATCH 20/56] std-aux: use _nm_strerror_r The function strerror_r returns an int per POSIX spec, but GNU version returns char *. Using it fails the compilation in Alpine, so use _nm_strerror_r instead that handles both cases. Fixes: 41e28b900f59 ('daemon-helper: add read-file-as-user') (cherry picked from commit 599cc1ed1d0306905fa7581eded85c45defe582a) --- src/libnm-std-aux/nm-std-utils.c | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/libnm-std-aux/nm-std-utils.c b/src/libnm-std-aux/nm-std-utils.c index 6c909dfab3..ef0e445d76 100644 --- a/src/libnm-std-aux/nm-std-utils.c +++ b/src/libnm-std-aux/nm-std-utils.c @@ -117,7 +117,7 @@ nm_utils_set_effective_user(const char *user, char *errbuf, size_t errbuf_len) errbuf_len, "error getting user entry: %d (%s)\n", errsv, - strerror_r(errsv, error, sizeof(error))); + _nm_strerror_r(errsv, error, sizeof(error))); } return false; } @@ -129,7 +129,7 @@ nm_utils_set_effective_user(const char *user, char *errbuf, size_t errbuf_len) "failed to change group to %u: %d (%s)\n", pwentry->pw_gid, errsv, - strerror_r(errsv, error, sizeof(error))); + _nm_strerror_r(errsv, error, sizeof(error))); return false; } @@ -140,7 +140,7 @@ nm_utils_set_effective_user(const char *user, char *errbuf, size_t errbuf_len) "failed to reset supplementary group list to %u: %d (%s)\n", pwentry->pw_gid, errsv, - strerror_r(errsv, error, sizeof(error))); + _nm_strerror_r(errsv, error, sizeof(error))); return false; } @@ -151,7 +151,7 @@ nm_utils_set_effective_user(const char *user, char *errbuf, size_t errbuf_len) "failed to change user to %u: %d (%s)\n", pwentry->pw_uid, errsv, - strerror_r(errsv, error, sizeof(error))); + _nm_strerror_r(errsv, error, sizeof(error))); return false; } @@ -176,7 +176,7 @@ nm_utils_read_file_to_stdout(const char *filename, char *errbuf, size_t errbuf_l errbuf_len, "error opening the file: %d (%s)", errsv, - strerror_r(errsv, error, sizeof(error))); + _nm_strerror_r(errsv, error, sizeof(error))); return false; } @@ -187,7 +187,7 @@ nm_utils_read_file_to_stdout(const char *filename, char *errbuf, size_t errbuf_l errbuf_len, "error writing to stdout: %d (%s)", errsv, - strerror_r(errsv, error, sizeof(error))); + _nm_strerror_r(errsv, error, sizeof(error))); return false; } } @@ -198,7 +198,7 @@ nm_utils_read_file_to_stdout(const char *filename, char *errbuf, size_t errbuf_l errbuf_len, "error reading the file: %d (%s)", errsv, - strerror_r(errsv, error, sizeof(error))); + _nm_strerror_r(errsv, error, sizeof(error))); return false; } From c978963ee7939cad1ff8947b3b9a70a59d65d90a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=8D=C3=B1igo=20Huguet?= Date: Fri, 12 Dec 2025 16:23:41 +0100 Subject: [PATCH 21/56] release: bump version to 1.55.91 (1.56-rc2) (development) --- meson.build | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/meson.build b/meson.build index f02ef5ef75..d4354b15f4 100644 --- a/meson.build +++ b/meson.build @@ -5,7 +5,7 @@ project( # NOTE: When incrementing version also add corresponding # NM_VERSION_x_y_z macros in # "src/libnm-core-public/nm-version-macros.h.in" - version: '1.55.90', + version: '1.55.91', license: 'GPL2+', default_options: [ 'buildtype=debugoptimized', From 102c763348f912e4984f4946c97e3ee18705f938 Mon Sep 17 00:00:00 2001 From: Beniamino Galvani Date: Wed, 3 Dec 2025 09:30:15 +0100 Subject: [PATCH 22/56] libnm-core: fix the documentation of the gateway IP property The D-Bus API documentation of the IPv4 and IPv6 settings say: * addresses Deprecated in favor of the 'address-data' and 'gateway' properties, but this can be used for backward-compatibility with older daemons. Note that if you send this property the daemon will ignore 'address-data' and 'gateway'. * gateway The gateway associated with this configuration. This is only meaningful if "addresses" is also set. This documentation wrongly suggests that at D-Bus level "gateway" requires "addresses", while it actually requires "address-data". The reason for the inconsistency is that the gateway documentation is common between nmcli and D-Bus and it refers to the "address" GObject property, not to the D-Bus property. Fix this inconsistency by not explicitly mentioning the property name. Fixes: 36156b70dc06 ('libnm: Override parts of nm-setting-docs.xml') https://gitlab.freedesktop.org/NetworkManager/NetworkManager/-/merge_requests/2319 (cherry picked from commit dad4da06b1e00ea04145f12268e2345af74ba4e3) --- src/libnm-core-impl/nm-setting-ip-config.c | 2 +- src/libnmc-setting/settings-docs.h.in | 4 ++-- src/nmcli/gen-metadata-nm-settings-nmcli.xml.in | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/libnm-core-impl/nm-setting-ip-config.c b/src/libnm-core-impl/nm-setting-ip-config.c index 20aca478d8..1aecc20c83 100644 --- a/src/libnm-core-impl/nm-setting-ip-config.c +++ b/src/libnm-core-impl/nm-setting-ip-config.c @@ -6740,7 +6740,7 @@ nm_setting_ip_config_class_init(NMSettingIPConfigClass *klass) * NMSettingIPConfig:gateway: * * The gateway associated with this configuration. This is only meaningful - * if #NMSettingIPConfig:addresses is also set. + * if addresses are also set on the device. * * Setting the gateway causes NetworkManager to configure a standard default route * with the gateway as next hop. This is ignored if #NMSettingIPConfig:never-default diff --git a/src/libnmc-setting/settings-docs.h.in b/src/libnmc-setting/settings-docs.h.in index 4e8978cb20..89f8c6d178 100644 --- a/src/libnmc-setting/settings-docs.h.in +++ b/src/libnmc-setting/settings-docs.h.in @@ -205,7 +205,7 @@ #define DESCRIBE_DOC_NM_SETTING_IP4_CONFIG_DNS_PRIORITY N_("DNS servers priority. The relative priority for DNS servers specified by this setting. A lower numerical value is better (higher priority). Negative values have the special effect of excluding other configurations with a greater numerical priority value; so in presence of at least one negative priority, only DNS servers from connections with the lowest priority value will be used. To avoid all DNS leaks, set the priority of the profile that should be used to the most negative value of all active connections profiles. Zero selects a globally configured default value. If the latter is missing or zero too, it defaults to 50 for VPNs (including WireGuard) and 100 for other connections. Note that the priority is to order DNS settings for multiple active connections. It does not disambiguate multiple DNS servers within the same connection profile. When multiple devices have configurations with the same priority, VPNs will be considered first, then devices with the best (lowest metric) default route and then all other devices. When using dns=default, servers with higher priority will be on top of resolv.conf. To prioritize a given server over another one within the same connection, just specify them in the desired order. Note that commonly the resolver tries name servers in /etc/resolv.conf in the order listed, proceeding with the next server in the list on failure. See for example the \"rotate\" option of the dns-options setting. If there are any negative DNS priorities, then only name servers from the devices with that lowest priority will be considered. When using a DNS resolver that supports Conditional Forwarding or Split DNS (with dns=dnsmasq or dns=systemd-resolved settings), each connection is used to query domains in its search list. The search domains determine which name servers to ask, and the DNS priority is used to prioritize name servers based on the domain. Queries for domains not present in any search list are routed through connections having the '~.' special wildcard domain, which is added automatically to connections with the default route (or can be added manually). When multiple connections specify the same domain, the one with the best priority (lowest numerical value) wins. If a sub domain is configured on another interface it will be accepted regardless the priority, unless parent domain on the other interface has a negative priority, which causes the sub domain to be shadowed. With Split DNS one can avoid undesired DNS leaks by properly configuring DNS priorities and the search domains, so that only name servers of the desired interface are configured.") #define DESCRIBE_DOC_NM_SETTING_IP4_CONFIG_DNS_SEARCH N_("List of DNS search domains. Domains starting with a tilde ('~') are considered 'routing' domains and are used only to decide the interface over which a query must be forwarded; they are not used to complete unqualified host names. When using a DNS plugin that supports Conditional Forwarding or Split DNS, then the search domains specify which name servers to query. This makes the behavior different from running with plain /etc/resolv.conf. For more information see also the dns-priority setting. When set on a profile that also enabled DHCP, the DNS search list received automatically (option 119 for DHCPv4 and option 24 for DHCPv6) gets merged with the manual list. This can be prevented by setting \"ignore-auto-dns\". Note that if no DNS searches are configured, the fallback will be derived from the domain from DHCP (option 15).") #define DESCRIBE_DOC_NM_SETTING_IP4_CONFIG_FORWARDING N_("Whether to configure sysctl interface-specific forwarding. When enabled, the interface will act as a router to forward the packet from one interface to another. When set to \"default\" (-1), the value from global configuration is used; if no global default is defined, \"auto\" (2) will be used. The \"forwarding\" property is ignored when \"method\" is set to \"shared\", because forwarding is always enabled in this case. The accepted values are: \"default\" (-1): use global default. \"no\" (0): disabled. \"yes\" (1): enabled. \"auto\" (2): enable if any shared connection is active, use kernel default otherwise.") -#define DESCRIBE_DOC_NM_SETTING_IP4_CONFIG_GATEWAY N_("The gateway associated with this configuration. This is only meaningful if \"addresses\" is also set. Setting the gateway causes NetworkManager to configure a standard default route with the gateway as next hop. This is ignored if \"never-default\" is set. An alternative is to configure the default route explicitly with a manual route and /0 as prefix length. Note that the gateway usually conflicts with routing that NetworkManager configures for WireGuard interfaces, so usually it should not be set in that case. See \"ip4-auto-default-route\".") +#define DESCRIBE_DOC_NM_SETTING_IP4_CONFIG_GATEWAY N_("The gateway associated with this configuration. This is only meaningful if addresses are also set on the device. Setting the gateway causes NetworkManager to configure a standard default route with the gateway as next hop. This is ignored if \"never-default\" is set. An alternative is to configure the default route explicitly with a manual route and /0 as prefix length. Note that the gateway usually conflicts with routing that NetworkManager configures for WireGuard interfaces, so usually it should not be set in that case. See \"ip4-auto-default-route\".") #define DESCRIBE_DOC_NM_SETTING_IP4_CONFIG_IGNORE_AUTO_DNS N_("When \"method\" is set to \"auto\" and this property to TRUE, automatically configured name servers and search domains are ignored and only name servers and search domains specified in the \"dns\" and \"dns-search\" properties, if any, are used.") #define DESCRIBE_DOC_NM_SETTING_IP4_CONFIG_IGNORE_AUTO_ROUTES N_("When \"method\" is set to \"auto\" and this property to TRUE, automatically configured routes are ignored and only routes specified in the \"routes\" property, if any, are used.") #define DESCRIBE_DOC_NM_SETTING_IP4_CONFIG_LINK_LOCAL N_("Enable and disable the IPv4 link-local configuration independently of the ipv4.method configuration. This allows a link-local address (169.254.x.y/16) to be obtained in addition to other addresses, such as those manually configured or obtained from a DHCP server. When set to \"auto\", the value is dependent on \"ipv4.method\". When set to \"default\", it honors the global connection default, before falling back to \"auto\". Note that if \"ipv4.method\" is \"disabled\", then link local addressing is always disabled too. The default is \"default\". Since 1.52, when set to \"fallback\", a link-local address is obtained if no other IPv4 address is set.") @@ -241,7 +241,7 @@ #define DESCRIBE_DOC_NM_SETTING_IP6_CONFIG_DNS_PRIORITY N_("DNS servers priority. The relative priority for DNS servers specified by this setting. A lower numerical value is better (higher priority). Negative values have the special effect of excluding other configurations with a greater numerical priority value; so in presence of at least one negative priority, only DNS servers from connections with the lowest priority value will be used. To avoid all DNS leaks, set the priority of the profile that should be used to the most negative value of all active connections profiles. Zero selects a globally configured default value. If the latter is missing or zero too, it defaults to 50 for VPNs (including WireGuard) and 100 for other connections. Note that the priority is to order DNS settings for multiple active connections. It does not disambiguate multiple DNS servers within the same connection profile. When multiple devices have configurations with the same priority, VPNs will be considered first, then devices with the best (lowest metric) default route and then all other devices. When using dns=default, servers with higher priority will be on top of resolv.conf. To prioritize a given server over another one within the same connection, just specify them in the desired order. Note that commonly the resolver tries name servers in /etc/resolv.conf in the order listed, proceeding with the next server in the list on failure. See for example the \"rotate\" option of the dns-options setting. If there are any negative DNS priorities, then only name servers from the devices with that lowest priority will be considered. When using a DNS resolver that supports Conditional Forwarding or Split DNS (with dns=dnsmasq or dns=systemd-resolved settings), each connection is used to query domains in its search list. The search domains determine which name servers to ask, and the DNS priority is used to prioritize name servers based on the domain. Queries for domains not present in any search list are routed through connections having the '~.' special wildcard domain, which is added automatically to connections with the default route (or can be added manually). When multiple connections specify the same domain, the one with the best priority (lowest numerical value) wins. If a sub domain is configured on another interface it will be accepted regardless the priority, unless parent domain on the other interface has a negative priority, which causes the sub domain to be shadowed. With Split DNS one can avoid undesired DNS leaks by properly configuring DNS priorities and the search domains, so that only name servers of the desired interface are configured.") #define DESCRIBE_DOC_NM_SETTING_IP6_CONFIG_DNS_SEARCH N_("List of DNS search domains. Domains starting with a tilde ('~') are considered 'routing' domains and are used only to decide the interface over which a query must be forwarded; they are not used to complete unqualified host names. When using a DNS plugin that supports Conditional Forwarding or Split DNS, then the search domains specify which name servers to query. This makes the behavior different from running with plain /etc/resolv.conf. For more information see also the dns-priority setting. When set on a profile that also enabled DHCP, the DNS search list received automatically (option 119 for DHCPv4 and option 24 for DHCPv6) gets merged with the manual list. This can be prevented by setting \"ignore-auto-dns\". Note that if no DNS searches are configured, the fallback will be derived from the domain from DHCP (option 15).") #define DESCRIBE_DOC_NM_SETTING_IP6_CONFIG_FORWARDING N_("Whether to configure sysctl interface-specific forwarding. When enabled, the interface will act as a router to forward the packet from one interface to another. When set to \"default\" (-1), the value from global configuration is used; if no global default is defined, \"auto\" (2) will be used. The \"forwarding\" property is ignored when \"method\" is set to \"shared\", because forwarding is always enabled in this case. The accepted values are: \"default\" (-1): use global default. \"no\" (0): disabled. \"yes\" (1): enabled. \"auto\" (2): enable if any shared connection is active, use kernel default otherwise.") -#define DESCRIBE_DOC_NM_SETTING_IP6_CONFIG_GATEWAY N_("The gateway associated with this configuration. This is only meaningful if \"addresses\" is also set. Setting the gateway causes NetworkManager to configure a standard default route with the gateway as next hop. This is ignored if \"never-default\" is set. An alternative is to configure the default route explicitly with a manual route and /0 as prefix length. Note that the gateway usually conflicts with routing that NetworkManager configures for WireGuard interfaces, so usually it should not be set in that case. See \"ip4-auto-default-route\".") +#define DESCRIBE_DOC_NM_SETTING_IP6_CONFIG_GATEWAY N_("The gateway associated with this configuration. This is only meaningful if addresses are also set on the device. Setting the gateway causes NetworkManager to configure a standard default route with the gateway as next hop. This is ignored if \"never-default\" is set. An alternative is to configure the default route explicitly with a manual route and /0 as prefix length. Note that the gateway usually conflicts with routing that NetworkManager configures for WireGuard interfaces, so usually it should not be set in that case. See \"ip4-auto-default-route\".") #define DESCRIBE_DOC_NM_SETTING_IP6_CONFIG_IGNORE_AUTO_DNS N_("When \"method\" is set to \"auto\" and this property to TRUE, automatically configured name servers and search domains are ignored and only name servers and search domains specified in the \"dns\" and \"dns-search\" properties, if any, are used.") #define DESCRIBE_DOC_NM_SETTING_IP6_CONFIG_IGNORE_AUTO_ROUTES N_("When \"method\" is set to \"auto\" and this property to TRUE, automatically configured routes are ignored and only routes specified in the \"routes\" property, if any, are used.") #define DESCRIBE_DOC_NM_SETTING_IP6_CONFIG_IP6_PRIVACY N_("Configure IPv6 Privacy Extensions for SLAAC, described in RFC4941. If enabled, it makes the kernel generate a temporary IPv6 address in addition to the public one generated from MAC address via modified EUI-64. This enhances privacy, but could cause problems in some applications, on the other hand. The permitted values are: -1: unknown, 0: disabled, 1: enabled (prefer public address), 2: enabled (prefer temporary addresses). Having a per-connection setting set to \"-1\" (default) means fallback to global configuration \"ipv6.ip6-privacy\". If it's also unspecified or set to \"-1\", fallback to read \"/proc/sys/net/ipv6/conf/default/use_tempaddr\". Note that this setting is distinct from the Stable Privacy addresses that can be enabled with the \"addr-gen-mode\" property's \"stable-privacy\" setting as another way of avoiding host tracking with IPv6 addresses.") diff --git a/src/nmcli/gen-metadata-nm-settings-nmcli.xml.in b/src/nmcli/gen-metadata-nm-settings-nmcli.xml.in index 949bc6804e..881c41cac8 100644 --- a/src/nmcli/gen-metadata-nm-settings-nmcli.xml.in +++ b/src/nmcli/gen-metadata-nm-settings-nmcli.xml.in @@ -1357,7 +1357,7 @@ format="list of ipv4.addresses objects" /> Date: Wed, 3 Dec 2025 18:24:58 +0100 Subject: [PATCH 23/56] nmcli: fix "device wifi connect" command with existing connection Executing this command twice, or when a connection profile already exists for the SSID: nmcli device wifi connect $SSID password $PASSWORD returns error: Error: 802-11-wireless-security.key-mgmt: property is missing. When setting the password nmcli was wiping the existing wireless security setting. Fixes: c8ff1b30fba3 ('nmcli/dev: use secret agent for nmcli d [wifi] connect') https://gitlab.freedesktop.org/NetworkManager/NetworkManager/-/issues/1688 (cherry picked from commit 3a4e18e30205b958ced44382313586e6858cc027) --- src/nmcli/devices.c | 1 + 1 file changed, 1 insertion(+) diff --git a/src/nmcli/devices.c b/src/nmcli/devices.c index 9a0181138b..4831a06e19 100644 --- a/src/nmcli/devices.c +++ b/src/nmcli/devices.c @@ -4041,6 +4041,7 @@ do_device_wifi_connect(const NMCCommand *cmd, NmCli *nmc, int argc, const char * if (password) { if (!connection) connection = nm_simple_connection_new(); + s_wsec = nm_connection_get_setting_wireless_security(connection); if (!s_wsec) { s_wsec = (NMSettingWirelessSecurity *) nm_setting_wireless_security_new(); nm_connection_add_setting(connection, NM_SETTING(s_wsec)); From d399ffbaba325df1b749d23c2f2fcba8ef8646cf Mon Sep 17 00:00:00 2001 From: Beniamino Galvani Date: Thu, 4 Dec 2025 10:58:53 +0100 Subject: [PATCH 24/56] nmcli: start the agent only after updating the connection When connecting to a wifi network and providing the password on the command line, nmcli first looks if there is a compatible connection to reuse. If there is not, it creates and activates a new one via a single call to AddAndActivate(). If there is a compatible connection, nmcli first calls Update() on it to set the new password and then Activate() to bring it up. Before that, it registers a secret agent that can prompt for a new password in case of authentication failure. However, as soon as nmcli registers a secret agent, NM tries to activate again the connection if it was blocked due to a previous authentication failure. This connection attempt is going to fail because it still uses the old password, as new one hasn't been set via Update(). Change the order of operations to register the agent after Update() and before Activate(). Reproducer: nmcli device wifi connect SSID password BAD_PASSWORD nmcli device wifi connect SSID password GOOD_PASSWORD Fixes: c8ff1b30fba3 ('nmcli/dev: use secret agent for nmcli d [wifi] connect') (cherry picked from commit 427a7cf2577868cd70556923a88312a68f939009) --- src/nmcli/devices.c | 52 +++++++++++++++++++++++++++++++++++---------- 1 file changed, 41 insertions(+), 11 deletions(-) diff --git a/src/nmcli/devices.c b/src/nmcli/devices.c index 4831a06e19..e81e710f44 100644 --- a/src/nmcli/devices.c +++ b/src/nmcli/devices.c @@ -2090,6 +2090,7 @@ typedef struct { char *specific_object; bool hotspot : 1; bool create : 1; + bool start_agent : 1; } AddAndActivateInfo; static AddAndActivateInfo * @@ -2097,6 +2098,7 @@ add_and_activate_info_new(NmCli *nmc, NMDevice *device, gboolean hotspot, gboolean create, + gboolean start_agent, const char *specific_object) { AddAndActivateInfo *info; @@ -2107,6 +2109,7 @@ add_and_activate_info_new(NmCli *nmc, .device = g_object_ref(device), .hotspot = hotspot, .create = create, + .start_agent = start_agent, .specific_object = g_strdup(specific_object), }; return info; @@ -2364,7 +2367,7 @@ do_device_connect(const NMCCommand *cmd, NmCli *nmc, int argc, const char *const nmc); } - info = add_and_activate_info_new(nmc, device, FALSE, FALSE, NULL); + info = add_and_activate_info_new(nmc, device, FALSE, FALSE, FALSE, NULL); nm_client_activate_connection_async(nmc->client, NULL, /* let NM find a connection automatically */ @@ -3603,6 +3606,16 @@ activate_update2_cb(GObject *source_object, GAsyncResult *res, gpointer user_dat return; } + if (info->start_agent && !nmc->secret_agent) { + nmc->secret_agent = nm_secret_agent_simple_new("nmcli-connect"); + if (nmc->secret_agent) { + g_signal_connect(nmc->secret_agent, + NM_SECRET_AGENT_SIMPLE_REQUEST_SECRETS, + G_CALLBACK(nmc_secrets_requested), + nmc); + } + } + nm_client_activate_connection_async(nmc->client, NM_CONNECTION(remote_con), info->device, @@ -3617,6 +3630,7 @@ save_and_activate_connection(NmCli *nmc, NMDevice *device, NMConnection *connection, gboolean hotspot, + gboolean start_agent, const char *specific_object) { AddAndActivateInfo *info; @@ -3625,9 +3639,15 @@ save_and_activate_connection(NmCli *nmc, device, hotspot, !NM_IS_REMOTE_CONNECTION(connection), + start_agent, specific_object); if (NM_IS_REMOTE_CONNECTION(connection)) { + /* Don't start the agent immediately. Otherwise the agent registration + * to the daemon will trigger a new activation if the connection was + * blocked due to bad secrets. This new activation would use the old + * secrets. + */ nm_remote_connection_update2(NM_REMOTE_CONNECTION(connection), nm_connection_to_dbus(connection, NM_CONNECTION_SERIALIZE_ALL), NM_SETTINGS_UPDATE2_FLAG_BLOCK_AUTOCONNECT, @@ -3636,6 +3656,16 @@ save_and_activate_connection(NmCli *nmc, activate_update2_cb, info); } else { + if (start_agent) { + nmc->secret_agent = nm_secret_agent_simple_new("nmcli-connect"); + if (nmc->secret_agent) { + g_signal_connect(nmc->secret_agent, + NM_SECRET_AGENT_SIMPLE_REQUEST_SECRETS, + G_CALLBACK(nmc_secrets_requested), + nmc); + } + } + nm_client_add_and_activate_connection_async(nmc->client, connection, info->device, @@ -3665,6 +3695,7 @@ do_device_wifi_connect(const NMCCommand *cmd, NmCli *nmc, int argc, const char * gboolean private = FALSE; gboolean hidden = FALSE; gboolean wep_passphrase = FALSE; + gboolean start_agent = FALSE; GByteArray *bssid1_arr = NULL; GByteArray *bssid2_arr = NULL; gs_free NMDevice **devices = NULL; @@ -4029,14 +4060,8 @@ do_device_wifi_connect(const NMCCommand *cmd, NmCli *nmc, int argc, const char * NM_802_11_AP_SEC_KEY_MGMT_OWE | NM_802_11_AP_SEC_KEY_MGMT_OWE_TM))) { NMSettingWirelessSecurity *s_wsec = NULL; - /* Create secret agent */ - nmc->secret_agent = nm_secret_agent_simple_new("nmcli-connect"); - if (nmc->secret_agent) { - g_signal_connect(nmc->secret_agent, - NM_SECRET_AGENT_SIMPLE_REQUEST_SECRETS, - G_CALLBACK(nmc_secrets_requested), - nmc); - } + /* Start the secret agent just before initiating the activation. */ + start_agent = TRUE; if (password) { if (!connection) @@ -4076,7 +4101,12 @@ do_device_wifi_connect(const NMCCommand *cmd, NmCli *nmc, int argc, const char * nmc->nowait_flag = (nmc->timeout == 0); nmc->should_wait++; - save_and_activate_connection(nmc, device, connection, FALSE, nm_object_get_path(NM_OBJECT(ap))); + save_and_activate_connection(nmc, + device, + connection, + FALSE, + start_agent, + nm_object_get_path(NM_OBJECT(ap))); finish: if (bssid1_arr) @@ -4536,7 +4566,7 @@ do_device_wifi_hotspot(const NMCCommand *cmd, NmCli *nmc, int argc, const char * nmc->nowait_flag = (nmc->timeout == 0); nmc->should_wait++; - save_and_activate_connection(nmc, device, connection, TRUE, NULL); + save_and_activate_connection(nmc, device, connection, TRUE, FALSE, NULL); } static void From 1b1612f064b0e5ef5d9a1650f154a90e0a8afb6d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=8D=C3=B1igo=20Huguet?= Date: Thu, 18 Dec 2025 10:49:33 +0100 Subject: [PATCH 25/56] meson: specify project version with the -dev and -rc suffixes This will create the tarball with names NetworkManager-1.56-rc2.tar.xz or NetworkManager-1.57.1-dev.tar.xz. This way they will match with the name of the Git tag, making easier for users, and specially for tools like Packit, to understand the versioning scheme. The goal is to make that there is only one public versioning scheme, the one with -rc and -dev suffixes. Version numbers with micro>=90 for RC releases is kept only as an internal thing for the C headers. Users of the API can still use it. Bump meson version to 0.56 to use str.substring(). (cherry picked from commit e422b1c3d92052d87358649321732ea80a3a34e3) --- meson.build | 39 +++++++++++++++++++++++++++------ src/tests/client/test-client.py | 14 ++++++++++-- 2 files changed, 44 insertions(+), 9 deletions(-) diff --git a/meson.build b/meson.build index d4354b15f4..58f030dd7d 100644 --- a/meson.build +++ b/meson.build @@ -5,23 +5,48 @@ project( # NOTE: When incrementing version also add corresponding # NM_VERSION_x_y_z macros in # "src/libnm-core-public/nm-version-macros.h.in" - version: '1.55.91', + version: '1.56-rc2', license: 'GPL2+', default_options: [ 'buildtype=debugoptimized', 'c_std=gnu11', 'warning_level=2' # value "2" will add "-Wall" and "-Wextra" to the compiler flags ], - meson_version: '>= 0.53.0', + meson_version: '>= 0.56.0', ) nm_name = meson.project_name() - nm_version = meson.project_version() -version_array = nm_version.split('.') -nm_major_version = version_array[0].to_int() -nm_minor_version = version_array[1].to_int() -nm_micro_version = version_array[2].to_int() + +version_and_suffix = nm_version.split('-') +version_array = version_and_suffix[0].split('.') +if version_and_suffix.length() == 2 + version_suffix = version_and_suffix[1] +else + assert(version_and_suffix.length() == 1) + version_suffix = '' +endif + +# In the C API we encode the version in 90+ scheme (1.56-rc1 = 1.55.90, rc2 = .91, etc) +if version_suffix == '' or version_suffix == 'dev' + assert(version_array.length() == 3) + nm_major_version = version_array[0].to_int() + nm_minor_version = version_array[1].to_int() + nm_micro_version = version_array[2].to_int() +elif version_suffix.startswith('rc') + assert(version_array.length() == 2) + nm_major_version = version_array[0].to_int() + nm_minor_version = version_array[1].to_int() - 1 + nm_micro_version = version_suffix.substring(2).to_int() + 89 +else + error('Invalid suffix: ' + version_suffix) +endif + +if nm_minor_version % 2 == 1 and version_suffix == '' + error('Expected a "-dev" or "-rc" suffix') +elif nm_minor_version %2 == 0 and version_suffix != '' + error('Unexpected "' + version_suffix + '" suffix') +endif nm_id_prefix = 'NM' diff --git a/src/tests/client/test-client.py b/src/tests/client/test-client.py index 6220587a9b..6cf1924c2c 100755 --- a/src/tests/client/test-client.py +++ b/src/tests/client/test-client.py @@ -688,7 +688,17 @@ class Util: micro = ver & 0xFF minor = (ver >> 8) & 0xFF major = ver >> 16 - return "%s.%s.%s" % (major, minor, micro) + + # Convert 1.57.1 -> 1.57.1-dev and 1.55.90 -> 1.56-rc1 + if micro >= 90: + minor += 1 + micro = "-rc" + str(micro - 89) + elif minor % 2 == 1: + micro = f".{micro}-dev" + else: + micro = f".{micro}" + + return "%s.%s%s" % (major, minor, micro) ############################################################################### @@ -3264,7 +3274,7 @@ def main(): sys.executable, __file__, "--started-with-dbus-session", - *sys.argv[1:] + *sys.argv[1:], ) except OSError as e: if e.errno != errno.ENOENT: From 92a6af3be38b8a79b02693e3f90bf168537de5f3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=8D=C3=B1igo=20Huguet?= Date: Thu, 18 Dec 2025 11:44:33 +0100 Subject: [PATCH 26/56] spec: use versioning scheme with ~dev and ~rc suffixes In the previous commit meson.build was adapted to use versions with -dev and -rc suffixes, as we create them in the Git tags, instead of versions with micro>90 for RCs as we used to do. The tarball name will contain the version with the new scheme, so adapt the spec file for it. This will enable us to use Packit to do automatic updates. (cherry picked from commit d975389bcda0711401fd1f1433b2665e6166ee7c) --- contrib/fedora/rpm/NetworkManager.spec | 18 +++++++----------- contrib/fedora/rpm/build.sh | 6 +----- 2 files changed, 8 insertions(+), 16 deletions(-) diff --git a/contrib/fedora/rpm/NetworkManager.spec b/contrib/fedora/rpm/NetworkManager.spec index ca22591a64..9feb8a007c 100644 --- a/contrib/fedora/rpm/NetworkManager.spec +++ b/contrib/fedora/rpm/NetworkManager.spec @@ -13,9 +13,7 @@ %global glib2_version %(pkg-config --modversion glib-2.0 2>/dev/null || echo bad) %global epoch_version 1 -%global real_version __VERSION__ -%global git_tag_version __GIT_TAG_VERSION__ -%global rpm_version %{real_version} +%global base_version __VERSION__ %global release_version __RELEASE_VERSION__ %global snapshot __SNAPSHOT__ %global git_sha __COMMIT__ @@ -29,7 +27,7 @@ %global obsoletes_ifcfg_rh 1:1.36.2 %global nmlibdir %{_prefix}/lib/%{name} -%global nmplugindir %{_libdir}/%{name}/%{version}-%{release} +%global nmplugindir %{_libdir}/%{name}/%{version_no_tilde}-%{release} %global _hardened_build 1 @@ -42,8 +40,6 @@ %global snap %{?snapshot_dot}%{?git_sha_dot} -%global real_version_major %(printf '%s' '%{real_version}' | sed -n 's/^\\([1-9][0-9]*\\.[0-9][0-9]*\\)\\.[0-9][0-9]*$/\\1/p') - %global systemd_units NetworkManager.service NetworkManager-wait-online.service NetworkManager-dispatcher.service nm-priv-helper.service %global systemd_units_cloud_setup nm-cloud-setup.service nm-cloud-setup.timer @@ -164,13 +160,13 @@ Name: NetworkManager Summary: Network connection manager and user applications Epoch: %{epoch_version} -Version: %{rpm_version} +Version: %{base_version} Release: %{release_version}%{?snap}%{?dist} Group: System Environment/Base License: GPL-2.0-or-later AND LGPL-2.1-or-later URL: https://networkmanager.dev/ -#Source: https://gitlab.freedesktop.org/NetworkManager/NetworkManager/-/releases/%{git_tag_version}/downloads/%{name}-%{real_version}.tar.xz +#Source: https://gitlab.freedesktop.org/NetworkManager/NetworkManager/-/releases/%{version_no_tilde}/downloads/%{name}-%{version_no_tilde}.tar.xz Source: __SOURCE1__ Source1: NetworkManager.conf Source2: 00-server.conf @@ -575,7 +571,7 @@ Preferably use nmcli instead. %prep -%autosetup -p1 -n NetworkManager-%{real_version} +%autosetup -p1 -n NetworkManager-%{version_no_tilde} %build @@ -755,8 +751,8 @@ rm -f %{buildroot}%{_unitdir}/NetworkManager-wait-online-initrd.service find %{buildroot}%{_datadir}/gtk-doc -exec touch --reference meson.build '{}' \+ %if 0%{?__debug_package} && ! 0%{?flatpak} -mkdir -p %{buildroot}%{_prefix}/src/debug/NetworkManager-%{real_version} -cp valgrind.suppressions %{buildroot}%{_prefix}/src/debug/NetworkManager-%{real_version} +mkdir -p %{buildroot}%{_prefix}/src/debug/NetworkManager-%{version_no_tilde} +cp valgrind.suppressions %{buildroot}%{_prefix}/src/debug/NetworkManager-%{version_no_tilde} %endif %if %{with ifcfg_rh} diff --git a/contrib/fedora/rpm/build.sh b/contrib/fedora/rpm/build.sh index 1f8188a2f1..ee4be6a079 100755 --- a/contrib/fedora/rpm/build.sh +++ b/contrib/fedora/rpm/build.sh @@ -12,7 +12,6 @@ set -o pipefail # RELEASE_VERSION= # SNAPSHOT= # VERSION= -# GIT_TAG_VERSION= # COMMIT_FULL= # COMMIT= # USERNAME= @@ -113,7 +112,6 @@ UUID=`uuidgen` RELEASE_VERSION="${RELEASE_VERSION:-$(git rev-list HEAD | wc -l)}" SNAPSHOT="${SNAPSHOT:-%{nil\}}" VERSION="${VERSION:-$(get_version || die "Could not read $VERSION")}" -GIT_TAG_VERSION="${GIT_TAG_VERSION:-$VERSION}" COMMIT_FULL="${COMMIT_FULL:-$(git rev-parse --verify HEAD || die "Error reading HEAD revision")}" COMMIT="${COMMIT:-$(printf '%s' "$COMMIT_FULL" | sed 's/^\(.\{10\}\).*/\1/' || die "Error reading HEAD revision")}" BCOND_DEFAULT_DEBUG="${BCOND_DEFAULT_DEBUG:-0}" @@ -157,7 +155,6 @@ if [[ "$SOURCE_FROM_GIT" == "1" ]]; then fi LOG "VERSION=$VERSION" -LOG "GIT_TAG_VERSION=$GIT_TAG_VERSION" LOG "RELEASE_VERSION=$RELEASE_VERSION" LOG "SNAPSHOT=$SNAPSHOT" LOG "COMMIT_FULL=$COMMIT_FULL" @@ -209,8 +206,7 @@ cp "$SOURCE_README_IFCFG_MIGRATED" "$TEMP/SOURCES/readme-ifcfg-rh-migrated.txt" write_changelog -sed -e "s/__VERSION__/$VERSION/g" \ - -e "s/__GIT_TAG_VERSION__/$GIT_TAG_VERSION/g" \ +sed -e "s/__VERSION__/${VERSION/-/\~}/g" \ -e "s/__RELEASE_VERSION__/$RELEASE_VERSION/g" \ -e "s/__SNAPSHOT__/$SNAPSHOT/g" \ -e "s/__COMMIT__/$COMMIT/g" \ From f3ec3957ff59f5c5c12cb55cd87a9dd6bff9cbd7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=8D=C3=B1igo=20Huguet?= Date: Thu, 18 Dec 2025 11:58:07 +0100 Subject: [PATCH 27/56] release.sh: use versioning scheme with -dev and -rc suffixes The previous commits has unified the versioning scheme to only use the version names like 1.56-rc2, 1.56.2 and 1.57.1-dev, like the version names that we use in the Git tags. The scheme with micro>=90 for RCs will be used only internally, in the C headers. The tarballs will be named with the new scheme. Adapt the release.sh script to correctly understand this versioning scheme and to create the tarballs with the right new name. This will enable us to use Packit to automate rpm updates. (cherry picked from commit 9f4261168d7e9fc616cc14d3af056ce8adcf0daf) --- contrib/fedora/rpm/release.sh | 212 +++++++++++++--------------------- 1 file changed, 80 insertions(+), 132 deletions(-) diff --git a/contrib/fedora/rpm/release.sh b/contrib/fedora/rpm/release.sh index ff18cc5b00..ce98e22e35 100755 --- a/contrib/fedora/rpm/release.sh +++ b/contrib/fedora/rpm/release.sh @@ -102,14 +102,8 @@ do_command() { SCRIPTDIR="$(dirname "$(readlink -f "$0")")" GITDIR="$(cd "$SCRIPTDIR" && git rev-parse --show-toplevel || die "Could not get GITDIR")" -parse_version() { - local VERSION=$(grep -E -m1 '^\s+version:' "$GITDIR/meson.build" \ - | cut -d"'" -f2 \ - | sed 's/\./ /g') - - re='^(0|[1-9][0-9]*) (0|[1-9][0-9]*) (0|[1-9][0-9]*)$' - [[ "$VERSION" =~ $re ]] || return 1 - echo "$VERSION" +get_version() { + grep -E -m1 '^\s+version:' "$GITDIR/meson.build" | cut -d"'" -f2 } number_is_even() { @@ -155,7 +149,7 @@ check_gitlab_pipeline() { set_version_number() { sed -i \ - -e '1,20 s/^\( *version: *'\''\)[0-9]\+\.[0-9]\+\.[0-9]\+\('\'',\)$/\1'"$1.$2.$3"'\2/' \ + -E "1,20 s/^( *version: *')[^']+(',) *\$/\1$1\2/" \ meson.build } @@ -259,12 +253,18 @@ done [ -n "$RELEASE_MODE" ] || die_usage "specify the desired release mode" -VERSION_ARR=( $(parse_version) ) || die "cannot detect NetworkManager version" -VERSION_STR="$(IFS=.; echo "${VERSION_ARR[*]}")" +VERSION_STR="$(get_version)" +VERSION_ARR=( $(echo "$VERSION_STR" | sed 's/[\.\-]/ /g') ) +if [[ ${VERSION_ARR[2]} =~ ^rc ]]; then + RC_VERSION=${VERSION_ARR[2]#rc} + VERSION_ARR[2]=0 +else + RC_VERSION= +fi echo "Current version before release: $VERSION_STR (do \"$RELEASE_MODE\" release)" -grep -q "version: '${VERSION_ARR[0]}.${VERSION_ARR[1]}.${VERSION_ARR[2]}'," ./meson.build || die "meson.build does not have expected version" +grep -q "version: '$VERSION_STR'," ./meson.build || die "meson.build does not have expected version" TMP="$(git status --porcelain)" || die "git status failed" test -z "$TMP" || die "git working directory is not clean (git status --porcelain)" @@ -280,50 +280,41 @@ if [ "$CUR_BRANCH" = main ]; then number_is_odd "${VERSION_ARR[1]}" || die "Unexpected version number on main. Should be an odd development version" [ "$RELEASE_MODE" = devel -o "$RELEASE_MODE" = rc1 -o "$RELEASE_MODE" = major-post ] || die "Unexpected branch name \"$CUR_BRANCH\" for \"$RELEASE_MODE\"" else - re='^nm-[0-9]+-[0-9]+$' - [[ "$CUR_BRANCH" =~ $re ]] || die "Unexpected current branch $CUR_BRANCH. Should be main or nm-?-??" - if number_is_odd "${VERSION_ARR[1]}"; then - # we are on a release candiate branch. - [ "$RELEASE_MODE" = rc -o "$RELEASE_MODE" = major ] || die "Unexpected branch name \"$CUR_BRANCH\" for \"$RELEASE_MODE\"" - [ "$CUR_BRANCH" == "nm-${VERSION_ARR[0]}-$((${VERSION_ARR[1]} + 1))" ] || die "Unexpected current branch $CUR_BRANCH. Should be nm-${VERSION_ARR[0]}-$((${VERSION_ARR[1]} + 1))" - else - [ "$RELEASE_MODE" = minor ] || die "Unexpected branch name \"$CUR_BRANCH\" for \"$RELEASE_MODE\"" - [ "$CUR_BRANCH" == "nm-${VERSION_ARR[0]}-${VERSION_ARR[1]}" ] || die "Unexpected current branch $CUR_BRANCH. Should be nm-${VERSION_ARR[0]}-${VERSION_ARR[1]}" - fi + [ "$CUR_BRANCH" == "nm-${VERSION_ARR[0]}-${VERSION_ARR[1]}" ] || die "Unexpected current branch $CUR_BRANCH. Should be nm-${VERSION_ARR[0]}-${VERSION_ARR[1]}" + [ "$RELEASE_MODE" = rc -o "$RELEASE_MODE" = major -o "$RELEASE_MODE" = minor ] || die "Unexpected branch name \"$CUR_BRANCH\" for \"$RELEASE_MODE\"" fi -RC_VERSION= RELEASE_BRANCH= case "$RELEASE_MODE" in minor) number_is_even "${VERSION_ARR[1]}" || die "cannot do minor release on top of version $VERSION_STR" - [ "$CUR_BRANCH" != main ] || die "cannot do a minor release on main" + [ "$RC_VERSION" = "" ] || die "cannot do a minor release on top of an RC version" + [ "$CUR_BRANCH" == "nm-${VERSION_ARR[0]}-${VERSION_ARR[1]}" ] || die "minor release can only be on \"nm-${VERSION_ARR[0]}-${VERSION_ARR[1]}\" branch" ;; devel) number_is_odd "${VERSION_ARR[1]}" || die "cannot do devel release on top of version $VERSION_STR" - [ "$((${VERSION_ARR[2]} + 1))" -lt 90 ] || die "devel release must have a micro version smaller than 90 but current version is $VERSION_STR" + [ "$RC_VERSION" = "" ] || die "cannot do a devel release on top of an RC version" [ "$CUR_BRANCH" == main ] || die "devel release can only be on main" ;; - rc) - number_is_odd "${VERSION_ARR[1]}" || die "cannot do rc release on top of version $VERSION_STR" - [ "${VERSION_ARR[2]}" -ge 90 ] || die "rc release must have a micro version larger than ${VERSION_ARR[0]}.90 but current version is $VERSION_STR" - RC_VERSION="$((${VERSION_ARR[2]} - 88))" - [ "$CUR_BRANCH" == "nm-${VERSION_ARR[0]}-$((${VERSION_ARR[1]} + 1))" ] || die "devel release can only be on \"nm-${VERSION_ARR[0]}-$((${VERSION_ARR[1]} + 1))\" branch" - ;; rc1) number_is_odd "${VERSION_ARR[1]}" || die "cannot do rc release on top of version $VERSION_STR" - [ "${VERSION_ARR[2]}" -lt 90 ] || die "rc release must have a micro version smaller than ${VERSION_ARR[0]}.${VERSION_ARR[1]}.90 but current version is $VERSION_STR" + [ "$RC_VERSION" = "" ] || die "rc1 release cannot be done on top of an RC version" [ "$CUR_BRANCH" == main ] || die "rc1 release can only be on main" RELEASE_BRANCH="nm-${VERSION_ARR[0]}-$((${VERSION_ARR[1]} + 1))" ;; + rc) + number_is_even "${VERSION_ARR[1]}" || die "cannot do rc release on top of version $VERSION_STR" + [ "$RC_VERSION" != "" ] || die "rc release must be done on top of an RC version" + [ "$CUR_BRANCH" == "nm-${VERSION_ARR[0]}-${VERSION_ARR[1]}" ] || die "rc release can only be on \"nm-${VERSION_ARR[0]}-${VERSION_ARR[1]}\" branch" + ;; major) - number_is_odd "${VERSION_ARR[1]}" || die "cannot do major release on top of version $VERSION_STR" - [ "${VERSION_ARR[2]}" -ge 90 ] || die "parent version for major release must have a micro version larger than ${VERSION_ARR[0]}.90 but current version is $VERSION_STR" - [ "$CUR_BRANCH" == "nm-${VERSION_ARR[0]}-$((${VERSION_ARR[1]} + 1))" ] || die "major release can only be on \"nm-${VERSION_ARR[0]}-$((${VERSION_ARR[1]} + 1))\" branch" + number_is_even "${VERSION_ARR[1]}" || die "cannot do major release on top of version $VERSION_STR" + [ "$RC_VERSION" != "" ] || die "major release must be done on top of an RC version" + [ "$CUR_BRANCH" == "nm-${VERSION_ARR[0]}-${VERSION_ARR[1]}" ] || die "major release can only be on \"nm-${VERSION_ARR[0]}-${VERSION_ARR[1]}\" branch" ;; major-post) number_is_odd "${VERSION_ARR[1]}" || die "cannot do major-post release on top of version $VERSION_STR" - [ "$((${VERSION_ARR[2]} + 1))" -lt 90 ] || die "major-post release must have a micro version smaller than 90 but current version is $VERSION_STR" + [ "$RC_VERSION" = "" ] || die "major-post release cannot be done on top of an RC version" [ "$CUR_BRANCH" == main ] || die "major-post release can only be on main" ;; *) @@ -389,7 +380,7 @@ if [ "$RELEASE_MODE" = major -o "$RELEASE_MODE" = minor ]; then fi echo "$(echo_color 36 -n "https://gitlab.freedesktop.org/NetworkManager/networkmanager.pages.freedesktop.org.git") by running" if [ "$RELEASE_MODE" = major ]; then - v="${VERSION_ARR[0]}.$((${VERSION_ARR[1]} + 1)).0" + v="${VERSION_ARR[0]}.${VERSION_ARR[1]}.0" else v="${VERSION_ARR[0]}.${VERSION_ARR[1]}.$((${VERSION_ARR[2]} + 1))" fi @@ -418,8 +409,8 @@ if [ $CHECK_GITLAB = 1 ]; then fi fi -BRANCHES=() -BUILD_TAG= +PUSH_REFS=() +BUILD_VERSION= CLEANUP_CHECKOUT_BRANCH="$CUR_BRANCH" @@ -428,61 +419,21 @@ CLEANUP_REFS+=("refs/heads/$TMP_BRANCH") case "$RELEASE_MODE" in minor) - set_version_number "${VERSION_ARR[0]}" "${VERSION_ARR[1]}" $(("${VERSION_ARR[2]}" + 1)) - git commit -m "release: bump version to ${VERSION_ARR[0]}.${VERSION_ARR[1]}.$(("${VERSION_ARR[2]}" + 1))" -a || die "failed to commit release" - - b="${VERSION_ARR[0]}.${VERSION_ARR[1]}.$(("${VERSION_ARR[2]}" + 1))" - git tag -s -a -m "Tag $b" "$b" HEAD || die "failed to tag release" - BRANCHES+=("$b") - CLEANUP_REFS+=("refs/tags/$b") - BUILD_TAG="$b" - TAR_VERSION="$b" + BUILD_VERSION="${VERSION_ARR[0]}.${VERSION_ARR[1]}.$(("${VERSION_ARR[2]}" + 1))" ;; devel) - set_version_number "${VERSION_ARR[0]}" "${VERSION_ARR[1]}" $(("${VERSION_ARR[2]}" + 1)) - git commit -m "release: bump version to ${VERSION_ARR[0]}.${VERSION_ARR[1]}.$(("${VERSION_ARR[2]}" + 1)) (development)" -a || die "failed to commit devel version bump" - - b="${VERSION_ARR[0]}.${VERSION_ARR[1]}.$(("${VERSION_ARR[2]}" + 1))" - git tag -s -a -m "Tag $b (development)" "$b-dev" HEAD || die "failed to tag release" - BRANCHES+=("$b-dev") - CLEANUP_REFS+=("refs/tags/$b-dev") - BUILD_TAG="$b-dev" - TAR_VERSION="$b" - ;; - rc) - b="${VERSION_ARR[0]}.${VERSION_ARR[1]}.$(("${VERSION_ARR[2]}" + 1))" - t="${VERSION_ARR[0]}.$(("${VERSION_ARR[1]}" + 1))-rc$RC_VERSION" - set_version_number "${VERSION_ARR[0]}" "${VERSION_ARR[1]}" $(("${VERSION_ARR[2]}" + 1)) - git commit -m "release: bump version to $b ($t) (development)" -a || die "failed to commit rc version bump" - - git tag -s -a -m "Tag $b ($t) (development)" "$t" HEAD || die "failed to tag release" - BRANCHES+=("$t") - CLEANUP_REFS+=("refs/tags/$t") - BUILD_TAG="$t" - TAR_VERSION="$b" + BUILD_VERSION="${VERSION_ARR[0]}.${VERSION_ARR[1]}.$(("${VERSION_ARR[2]}" + 1))" + BUILD_VERSION_DESCR="$BUILD_VERSION (development)" + BUILD_VERSION="${BUILD_VERSION}-dev" ;; rc1) - set_version_number "${VERSION_ARR[0]}" "${VERSION_ARR[1]}" 90 - b="${VERSION_ARR[0]}.${VERSION_ARR[1]}.90" - t="${VERSION_ARR[0]}.$(("${VERSION_ARR[1]}" + 1))-rc1" - git commit -m "release: bump version to $b ($t)" -a || die "failed to commit rc1 version bump" - - git tag -s -a -m "Tag $b ($t) (development)" "$t" HEAD || die "failed to tag release $t" - BRANCHES+=("$t") - CLEANUP_REFS+=("refs/tags/$t") - BUILD_TAG="$t" - TAR_VERSION="$b" + BUILD_VERSION="${VERSION_ARR[0]}.$(("${VERSION_ARR[1]}" + 1))-rc1" + ;; + rc) + BUILD_VERSION="${VERSION_ARR[0]}.${VERSION_ARR[1]}-rc$(( $RC_VERSION + 1 ))" ;; major) - b="${VERSION_ARR[0]}.$((${VERSION_ARR[1]} + 1)).0" - set_version_number "${VERSION_ARR[0]}" "$((${VERSION_ARR[1]} + 1))" 0 - git commit -m "release: bump version to $b" -a || die "failed to commit major version bump" - - git tag -s -a -m "Tag $b" "$b" HEAD || die "failed to tag release" - BRANCHES+=("$b") - CLEANUP_REFS+=("refs/tags/$b") - BUILD_TAG="$b" - TAR_VERSION="$b" + BUILD_VERSION="${VERSION_ARR[0]}.${VERSION_ARR[1]}.0" ;; major-post) # We create a merge commit with the content of current "main", with two @@ -494,62 +445,60 @@ case "$RELEASE_MODE" in git merge -Xours --commit -m tmp main || die "merge1" git rm --cached -r . || die "merge2" git checkout main -- . || die "merge3" - b="${VERSION_ARR[0]}.${VERSION_ARR[1]}.$((${VERSION_ARR[2]} + 1))" git commit --amend -m tmp -a || die "failed to commit major version bump" test x = "x$(git diff main HEAD)" || die "there is a diff after merge!" - set_version_number "${VERSION_ARR[0]}" "${VERSION_ARR[1]}" "$((${VERSION_ARR[2]} + 1))" - git commit --amend -m "release: bump version to $b (development)" -a || die "failed to commit major version bump" - git tag -s -a -m "Tag $b (development)" "$b-dev" HEAD || die "failed to tag release" - BRANCHES+=("$b-dev") - CLEANUP_REFS+=("refs/tags/$b-dev") - BUILD_TAG="$b-dev" - TAR_VERSION="$b" + BUILD_VERSION="${VERSION_ARR[0]}.${VERSION_ARR[1]}.$(("${VERSION_ARR[2]}" + 1))" + BUILD_VERSION_DESCR="$BUILD_VERSION (development)" + BUILD_VERSION="${BUILD_VERSION}-dev" ;; *) die "Release mode $RELEASE_MODE not yet implemented" ;; esac -build_tag() { - local BUILD_TAG="$1" - local TAR_FILE="NetworkManager-$2.tar.xz" +build_version() { + local BUILD_VERSION="$1" + local BUILD_VERSION_DESCR="$2" + local TAR_FILE="NetworkManager-$BUILD_VERSION.tar.xz" local SUM_FILE="$TAR_FILE.sha256sum" - git checkout "$BUILD_TAG" || die "failed to checkout $BUILD_TAG" + set_version_number "$BUILD_VERSION" + git commit -m "release: bump version to $BUILD_VERSION_DESCR" -a || die "failed to commit release" + git tag -s -a -m "Release $BUILD_VERSION_DESCR" "$BUILD_VERSION" HEAD || die "failed to tag release" + + PUSH_REFS+=("$BUILD_VERSION") + CLEANUP_REFS+=("refs/tags/$BUILD_VERSION") + + git checkout "$BUILD_VERSION" || die "failed to checkout $BUILD_VERSION" ./contrib/fedora/rpm/build_clean.sh -r || die "build release failed" cp "./build/meson-dist/$TAR_FILE" /tmp/ || die "failed to copy $TAR_FILE to /tmp" cp "./build/meson-dist/$SUM_FILE" /tmp/ || die "failed to copy $SUM_FILE to /tmp" git clean -fdx + + RELEASE_VERSIONS+=("$BUILD_VERSION") } -RELEASE_TAR_VERSIONS=() -RELEASE_TAGS=() -if [ -n "$BUILD_TAG" ]; then - build_tag "$BUILD_TAG" "$TAR_VERSION" - RELEASE_TAR_VERSIONS+=("$TAR_VERSION") - RELEASE_TAGS+=("$BUILD_TAG") +RELEASE_VERSIONS=() +if [ -n "$BUILD_VERSION" ]; then + build_version "$BUILD_VERSION" "${BUILD_VERSION_DESCR:-$BUILD_VERSION}" fi git checkout -B "$CUR_BRANCH" "$TMP_BRANCH" || die "cannot checkout $CUR_BRANCH" -BRANCHES+=( "$CUR_BRANCH" ) +PUSH_REFS+=( "$CUR_BRANCH" ) if [ "$RELEASE_MODE" = rc1 ]; then git branch "$RELEASE_BRANCH" "$TMP_BRANCH" || die "cannot checkout $CUR_BRANCH" - BRANCHES+=( "$RELEASE_BRANCH" ) + PUSH_REFS+=( "$RELEASE_BRANCH" ) CLEANUP_REFS+=( "refs/heads/$RELEASE_BRANCH" ) + git checkout "$TMP_BRANCH" - b="${VERSION_ARR[0]}.$((${VERSION_ARR[1]} + 2)).0" - set_version_number "${VERSION_ARR[0]}" "$((${VERSION_ARR[1]} + 2))" 0 - git commit -m "release: bump version to $b (development)" -a || die "failed to commit devel version bump" - git tag -s -a -m "Tag $b (development)" "$b-dev" HEAD || die "failed to tag release" - BRANCHES+=("$b-dev") - CLEANUP_REFS+=("refs/tags/$b-dev") - BUILD_TAG="$b-dev" - TAR_VERSION="$b" - build_tag "$BUILD_TAG" "$TAR_VERSION" - RELEASE_TAR_VERSIONS+=("$TAR_VERSION") - RELEASE_TAGS+=("$BUILD_TAG") + + BUILD_VERSION="${VERSION_ARR[0]}.$((${VERSION_ARR[1]} + 2)).0" + BUILD_VERSION_DESCR="$BUILD_VERSION (development)" + BUILD_VERSION="${BUILD_VERSION}-dev" + build_version "$BUILD_VERSION" "$BUILD_VERSION_DESCR" + git checkout -B "$CUR_BRANCH" "$TMP_BRANCH" || die "cannot checkout $CUR_BRANCH" fi @@ -565,20 +514,19 @@ if [ -z "$GITLAB_USER_ID" ] || [ "$GITLAB_USER_ID" = "null" ]; then die "failed to authenticate to gitlab.freedesktop.org with the private token" fi -do_command git push "$ORIGIN" "${BRANCHES[@]}" || die "failed to to push branches ${BRANCHES[@]} to $ORIGIN" +do_command git push "$ORIGIN" "${PUSH_REFS[@]}" || die "failed to to push branches ${PUSH_REFS[@]} to $ORIGIN" CREATE_RELEASE_FAIL=0 -for I in "${!RELEASE_TAR_VERSIONS[@]}"; do - TAR_FILE="NetworkManager-${RELEASE_TAR_VERSIONS[$I]}.tar.xz" +for BUILD_VERSION in "${RELEASE_VERSIONS[@]}"; do + TAR_FILE="NetworkManager-$BUILD_VERSION.tar.xz" SUM_FILE="$TAR_FILE.sha256sum" - BUILD_TAG="${RELEASE_TAGS["$I"]}" FAIL=0 # upload tarball and checksum file as generic packages for F in "$TAR_FILE" "$SUM_FILE"; do do_command curl --location --fail-with-body --header "PRIVATE-TOKEN: $GITLAB_TOKEN" \ --upload-file "/tmp/$F" \ - "https://gitlab.freedesktop.org/api/v4/projects/411/packages/generic/NetworkManager/$BUILD_TAG/$F" \ + "https://gitlab.freedesktop.org/api/v4/projects/411/packages/generic/NetworkManager/$BUILD_VERSION/$F" \ || FAIL=1 if [[ $FAIL = 1 ]]; then @@ -595,25 +543,25 @@ for I in "${!RELEASE_TAR_VERSIONS[@]}"; do --request POST "https://gitlab.freedesktop.org/api/v4/projects/411/releases" \ --data "$(cat < Date: Wed, 7 Jan 2026 15:18:31 +0100 Subject: [PATCH 28/56] NEWS: update And fix previous changes that incorrectly assigned the latest change to 1.56.1, when actually not even .0 has been released yet (we're still in RC). --- NEWS | 25 ++++++++++++++----------- 1 file changed, 14 insertions(+), 11 deletions(-) diff --git a/NEWS b/NEWS index 7d8d9abddc..c5eda5ed10 100644 --- a/NEWS +++ b/NEWS @@ -1,19 +1,17 @@ -=============================================== -NetworkManager-1.56.1 -Overview of changes since NetworkManager-1.56.0 -=============================================== - -* For private connections (the ones that specify a user in the - "connection.permissions" property), verify that the user can access - the 802.1X certificates and keys set in the connection. -* Introduce a libnm function that can be used by VPN plugins to check - user permissions on certificate and keys. - ============================================= NetworkManager-1.56 Overview of changes since NetworkManager-1.54 ============================================= +This is a snapshot of NetworkManager development. The API is +subject to change and not guaranteed to be compatible with +the later release. +USE AT YOUR OWN RISK. NOT RECOMMENDED FOR PRODUCTION USE! + +* Unify the versioning to use everywhere the scheme with the -rcX or -dev + suffixes when appropriate. This affects, for example, the URL and filename + of the release tarball and the version reported by nmcli and the daemon. + As an exception, the C API will continue to use the 90+ scheme for RC versions. * nmcli now supports viewing and managing WireGuard peers. * Support reapplying the "sriov.vfs" property as long as "sriov.total-vfs" is not changed. @@ -42,6 +40,11 @@ Overview of changes since NetworkManager-1.54 for eBPF is now detected at run time. * Add new MPTCP 'laminar' endpoint type, and set it by default alongside the 'subflow' one. +* For private connections (the ones that specify a user in the + "connection.permissions" property), verify that the user can access + the 802.1X certificates and keys set in the connection. +* Introduce a libnm function that can be used by VPN plugins to check + user permissions on certificate and keys. ============================================= NetworkManager-1.54 From 258686968fa2749c766806500d716a3fe758e5f2 Mon Sep 17 00:00:00 2001 From: Beniamino Galvani Date: Fri, 19 Dec 2025 17:24:20 +0100 Subject: [PATCH 29/56] core: limit the result from the helper to 32MiB (cherry picked from commit c4b39914c4ea7b17e1cbdfd7efd487b4d35abbb1) --- src/core/nm-core-utils.c | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/core/nm-core-utils.c b/src/core/nm-core-utils.c index 5404ecb9ce..deac04e73b 100644 --- a/src/core/nm-core-utils.c +++ b/src/core/nm-core-utils.c @@ -5163,6 +5163,14 @@ helper_have_data(int fd, GIOCondition condition, gpointer user_data) n_read = nm_utils_fd_read(fd, &info->in_buffer); _LOG2T(info, "read returns %ld", (long) n_read); + if (info->in_buffer.len > 32 * 1024 * 1024) { + helper_complete(info, + g_error_new_literal(NM_UTILS_ERROR, + NM_UTILS_ERROR_UNKNOWN, + "the output is larger than 32MiB")); + return G_SOURCE_CONTINUE; + } + if (n_read > 0) return G_SOURCE_CONTINUE; From 7575117ab5c7dadf9178ab3121933d1a5d1abf2d Mon Sep 17 00:00:00 2001 From: Beniamino Galvani Date: Fri, 19 Dec 2025 17:50:15 +0100 Subject: [PATCH 30/56] supplicant: properly validate blobs The purpose of the validation is to check that we pass to the supplicant a configuration that it can understand. For certificates and keys we enforce a maximum length of 64KiB; that means that the value of the property we send (i.e. the file path or the blob id) can be at most 64KiB. Instead we wrongly checked the size of the blob data. Fix the validation. Also, enforce a maximum blob size of 32MiB. Fixes: e85cc46d0b36 ('core: pass certificates as blobs to supplicant for private connections') (cherry picked from commit eb784c3f2768b726e9ef191e9bd73253ebb921ab) --- src/core/supplicant/nm-supplicant-config.c | 20 +++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/src/core/supplicant/nm-supplicant-config.c b/src/core/supplicant/nm-supplicant-config.c index fd360e7238..d026d9ccad 100644 --- a/src/core/supplicant/nm-supplicant-config.c +++ b/src/core/supplicant/nm-supplicant-config.c @@ -206,20 +206,30 @@ nm_supplicant_config_add_blob(NMSupplicantConfig *self, ConfigOption *old_opt; ConfigOption *opt; NMSupplOptType type; - const guint8 *data; 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); - data = g_bytes_get_data(value, &data_len); + g_bytes_get_data(value, &data_len); g_return_val_if_fail(data_len > 0, FALSE); - priv = NM_SUPPLICANT_CONFIG_GET_PRIVATE(self); + 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; + } - type = nm_supplicant_settings_verify_setting(key, (const char *) data, data_len); + 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, @@ -240,7 +250,7 @@ nm_supplicant_config_add_blob(NMSupplicantConfig *self, } opt = g_slice_new0(ConfigOption); - opt->value = g_strdup_printf("blob://%s", blobid); + opt->value = g_steal_pointer(&full_value); opt->len = strlen(opt->value); opt->type = type; From d41cc08e78d121f8437419ac694fbed9e887ea4d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=8D=C3=B1igo=20Huguet?= Date: Fri, 9 Jan 2026 08:32:58 +0100 Subject: [PATCH 31/56] spec: fix nmplugindir When dist_version is defined in meson, NM installs plugins to a directory called `NetworkManager-${dist_version}`. If the dist version contains a `~`, like `1.56~rc1`, defining nmplugindir with `%{version_no_tilde}` makes it `NetworkManager-1.56-rc1`, causing rpmbuild errors due to the mismatch. Fix it by defining nmplugindir with `%{version}` instead. Fixes: d975389bcda0 ('spec: use versioning scheme with ~dev and ~rc suffixes') (cherry picked from commit 9ebc8aa4807700e5a4da66c3aa111d69a514e374) --- contrib/fedora/rpm/NetworkManager.spec | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contrib/fedora/rpm/NetworkManager.spec b/contrib/fedora/rpm/NetworkManager.spec index 9feb8a007c..25e21c2099 100644 --- a/contrib/fedora/rpm/NetworkManager.spec +++ b/contrib/fedora/rpm/NetworkManager.spec @@ -27,7 +27,7 @@ %global obsoletes_ifcfg_rh 1:1.36.2 %global nmlibdir %{_prefix}/lib/%{name} -%global nmplugindir %{_libdir}/%{name}/%{version_no_tilde}-%{release} +%global nmplugindir %{_libdir}/%{name}/%{version}-%{release} %global _hardened_build 1 From 7372e93044d63527200a6d6d2d2fc5b3bb1fdd3a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=8D=C3=B1igo=20Huguet?= Date: Thu, 8 Jan 2026 16:27:08 +0100 Subject: [PATCH 32/56] spec: remove snapshot and git_sha macros Snapshot is only used from nm-copr-build.sh script, so not very useful. Git_sha is used from build.sh. Other than that, downstream is always nil. Remove them and modify build.sh to use --define "dist xxx" instead of them. This change is motivated by Packit not being able to modify the release number if it has the %{snap} suffix. (cherry picked from commit 5445ad2287bbbfca713f4558769463184f499106) --- contrib/fedora/rpm/NetworkManager.spec | 13 +------------ contrib/fedora/rpm/build.sh | 12 ++++++------ 2 files changed, 7 insertions(+), 18 deletions(-) diff --git a/contrib/fedora/rpm/NetworkManager.spec b/contrib/fedora/rpm/NetworkManager.spec index 25e21c2099..e6d8a62e02 100644 --- a/contrib/fedora/rpm/NetworkManager.spec +++ b/contrib/fedora/rpm/NetworkManager.spec @@ -15,8 +15,6 @@ %global epoch_version 1 %global base_version __VERSION__ %global release_version __RELEASE_VERSION__ -%global snapshot __SNAPSHOT__ -%global git_sha __COMMIT__ %global bcond_default_debug __BCOND_DEFAULT_DEBUG__ %global bcond_default_lto __BCOND_DEFAULT_LTO__ %global bcond_default_test __BCOND_DEFAULT_TEST__ @@ -31,15 +29,6 @@ %global _hardened_build 1 -%if "x%{?snapshot}" != "x" -%global snapshot_dot .%{snapshot} -%endif -%if "x%{?git_sha}" != "x" -%global git_sha_dot .%{git_sha} -%endif - -%global snap %{?snapshot_dot}%{?git_sha_dot} - %global systemd_units NetworkManager.service NetworkManager-wait-online.service NetworkManager-dispatcher.service nm-priv-helper.service %global systemd_units_cloud_setup nm-cloud-setup.service nm-cloud-setup.timer @@ -161,7 +150,7 @@ Name: NetworkManager Summary: Network connection manager and user applications Epoch: %{epoch_version} Version: %{base_version} -Release: %{release_version}%{?snap}%{?dist} +Release: %{release_version}%{?dist} Group: System Environment/Base License: GPL-2.0-or-later AND LGPL-2.1-or-later URL: https://networkmanager.dev/ diff --git a/contrib/fedora/rpm/build.sh b/contrib/fedora/rpm/build.sh index ee4be6a079..101805876c 100755 --- a/contrib/fedora/rpm/build.sh +++ b/contrib/fedora/rpm/build.sh @@ -110,7 +110,6 @@ exec 2>&1 UUID=`uuidgen` RELEASE_VERSION="${RELEASE_VERSION:-$(git rev-list HEAD | wc -l)}" -SNAPSHOT="${SNAPSHOT:-%{nil\}}" VERSION="${VERSION:-$(get_version || die "Could not read $VERSION")}" COMMIT_FULL="${COMMIT_FULL:-$(git rev-parse --verify HEAD || die "Error reading HEAD revision")}" COMMIT="${COMMIT:-$(printf '%s' "$COMMIT_FULL" | sed 's/^\(.\{10\}\).*/\1/' || die "Error reading HEAD revision")}" @@ -208,10 +207,6 @@ write_changelog sed -e "s/__VERSION__/${VERSION/-/\~}/g" \ -e "s/__RELEASE_VERSION__/$RELEASE_VERSION/g" \ - -e "s/__SNAPSHOT__/$SNAPSHOT/g" \ - -e "s/__COMMIT__/$COMMIT/g" \ - -e "s/__COMMIT_FULL__/$COMMIT_FULL/g" \ - -e "s/__SNAPSHOT__/$SNAPSHOT/g" \ -e "s/__SOURCE1__/$(basename "$SOURCE")/g" \ -e "s/__BCOND_DEFAULT_DEBUG__/$BCOND_DEFAULT_DEBUG/g" \ -e "s/__BCOND_DEFAULT_LTO__/${BCOND_DEFAULT_LTO:-"%{nil}"}/g" \ @@ -232,7 +227,12 @@ case "$BUILDTYPE" in ;; esac -rpmbuild --define "_topdir $TEMP" $RPM_BUILD_OPTION "$TEMPSPEC" $NM_RPMBUILD_ARGS || die "ERROR: rpmbuild FAILED" +DIST= +[[ "$COMMIT" != "" ]] && DIST=".${COMMIT}${DIST}" +[[ "$SNAPSHOT" != "" ]] && DIST=".${SNAPSHOT}${DIST}" +[[ "$DIST" != "" ]] && DIST=("--define" "dist ${DIST}$(rpmbuild --eval '%{dist}')") + +rpmbuild --define "_topdir $TEMP" "${DIST[@]}" $RPM_BUILD_OPTION "$TEMPSPEC" $NM_RPMBUILD_ARGS || die "ERROR: rpmbuild FAILED" LS_EXTRA=() From 9c5b56d42a7756b516108dd798d396397e71cc2a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=8D=C3=B1igo=20Huguet?= Date: Thu, 8 Jan 2026 16:31:34 +0100 Subject: [PATCH 33/56] spec: move the main info to the top It's clearer this way, and it will allow to modify directly the "Version:" and "Release:" fields to bump the version. It is more aligned with the layout of other projects' spec files too. (cherry picked from commit 6d952902b9077b53c882016060f71c7be1f9eb94) --- contrib/fedora/rpm/NetworkManager.spec | 23 +++++++++++------------ 1 file changed, 11 insertions(+), 12 deletions(-) diff --git a/contrib/fedora/rpm/NetworkManager.spec b/contrib/fedora/rpm/NetworkManager.spec index e6d8a62e02..73ab88f318 100644 --- a/contrib/fedora/rpm/NetworkManager.spec +++ b/contrib/fedora/rpm/NetworkManager.spec @@ -6,15 +6,23 @@ # # Note that it contains __PLACEHOLDERS__ that will be replaced by the accompanying 'build.sh' script. +Name: NetworkManager +Summary: Network connection manager and user applications +License: GPL-2.0-or-later AND LGPL-2.1-or-later +URL: https://networkmanager.dev/ +Group: System Environment/Base + +Epoch: 1 +Version: __VERSION__ +Release: __RELEASE_VERSION__%{?dist} + +############################################################################### %global wpa_supplicant_version 1:1.1 %global ppp_version %(pkg-config --modversion pppd 2>/dev/null || sed -n 's/^#define\\s*VERSION\\s*"\\([^\\s]*\\)"$/\\1/p' %{_includedir}/pppd/patchlevel.h 2>/dev/null | grep . || echo bad) %global glib2_version %(pkg-config --modversion glib-2.0 2>/dev/null || echo bad) -%global epoch_version 1 -%global base_version __VERSION__ -%global release_version __RELEASE_VERSION__ %global bcond_default_debug __BCOND_DEFAULT_DEBUG__ %global bcond_default_lto __BCOND_DEFAULT_LTO__ %global bcond_default_test __BCOND_DEFAULT_TEST__ @@ -146,15 +154,6 @@ ############################################################################### -Name: NetworkManager -Summary: Network connection manager and user applications -Epoch: %{epoch_version} -Version: %{base_version} -Release: %{release_version}%{?dist} -Group: System Environment/Base -License: GPL-2.0-or-later AND LGPL-2.1-or-later -URL: https://networkmanager.dev/ - #Source: https://gitlab.freedesktop.org/NetworkManager/NetworkManager/-/releases/%{version_no_tilde}/downloads/%{name}-%{version_no_tilde}.tar.xz Source: __SOURCE1__ Source1: NetworkManager.conf From 1a7f424ac81e7da1cda6d60393656b8aae94412f Mon Sep 17 00:00:00 2001 From: Beniamino Galvani Date: Tue, 20 Jan 2026 11:43:05 +0100 Subject: [PATCH 34/56] libnm: add safe file access backported symbols from 1.52.2 Add to branch nm-1-56 symbols for safe file access that were backported to 1.52.2 to allow seamless upgrading from 1.52 to 1.56. --- src/libnm-client-impl/libnm.ver | 5 +++++ src/libnm-client-impl/nm-client.c | 12 ++++++++++++ 2 files changed, 17 insertions(+) diff --git a/src/libnm-client-impl/libnm.ver b/src/libnm-client-impl/libnm.ver index ed5901d79f..a82f6a1747 100644 --- a/src/libnm-client-impl/libnm.ver +++ b/src/libnm-client-impl/libnm.ver @@ -2064,6 +2064,11 @@ global: nm_ethtool_optname_is_fec; } libnm_1_50_0; +libnm_1_52_2 { + #nm_utils_copy_cert_as_user@libnm_1_52_2; + #nm_vpn_plugin_info_supports_safe_private_file_access@libnm_1_52_2; +} libnm_1_52_0; + libnm_1_54_0 { global: nm_setting_ip_config_forwarding_get_type; diff --git a/src/libnm-client-impl/nm-client.c b/src/libnm-client-impl/nm-client.c index 13343cbf52..c036016752 100644 --- a/src/libnm-client-impl/nm-client.c +++ b/src/libnm-client-impl/nm-client.c @@ -9339,3 +9339,15 @@ NM_BACKPORT_SYMBOL(libnm_1_50_4, (optname)); NM_BACKPORT_SYMBOL(libnm_1_50_4, GType, nm_setting_ethtool_fec_mode_get_type, (void), ()); + +NM_BACKPORT_SYMBOL(libnm_1_52_2, + char *, + nm_utils_copy_cert_as_user, + (const char *filename, const char *user, GError **error), + (filename, user, error)); + +NM_BACKPORT_SYMBOL(libnm_1_52_2, + gboolean, + nm_vpn_plugin_info_supports_safe_private_file_access, + (NMVpnPluginInfo * self), + (self)); From 6dc64dfa0c7229fd7f8f86093f664710459263c2 Mon Sep 17 00:00:00 2001 From: Beniamino Galvani Date: Tue, 20 Jan 2026 11:57:30 +0100 Subject: [PATCH 35/56] libnm: add safe file access backported symbols from 1.54.3 Add to branch nm-1-56 symbols for safe file access that were backported to 1.54.3 to allow seamless upgrading from 1.54 to 1.56. --- src/libnm-client-impl/libnm.ver | 5 +++++ src/libnm-client-impl/nm-client.c | 12 ++++++++++++ 2 files changed, 17 insertions(+) diff --git a/src/libnm-client-impl/libnm.ver b/src/libnm-client-impl/libnm.ver index a82f6a1747..6c48978a21 100644 --- a/src/libnm-client-impl/libnm.ver +++ b/src/libnm-client-impl/libnm.ver @@ -2089,6 +2089,11 @@ global: nm_setting_hsr_protocol_version_get_type; } libnm_1_54_0; +libnm_1_54_3 { + #nm_utils_copy_cert_as_user@libnm_1_54_3; + #nm_vpn_plugin_info_supports_safe_private_file_access@libnm_1_54_3; +} libnm_1_54_2; + libnm_1_56_0 { global: nm_dns_server_validate; diff --git a/src/libnm-client-impl/nm-client.c b/src/libnm-client-impl/nm-client.c index c036016752..b81ac6e506 100644 --- a/src/libnm-client-impl/nm-client.c +++ b/src/libnm-client-impl/nm-client.c @@ -9351,3 +9351,15 @@ NM_BACKPORT_SYMBOL(libnm_1_52_2, nm_vpn_plugin_info_supports_safe_private_file_access, (NMVpnPluginInfo * self), (self)); + +NM_BACKPORT_SYMBOL(libnm_1_54_3, + char *, + nm_utils_copy_cert_as_user, + (const char *filename, const char *user, GError **error), + (filename, user, error)); + +NM_BACKPORT_SYMBOL(libnm_1_54_3, + gboolean, + nm_vpn_plugin_info_supports_safe_private_file_access, + (NMVpnPluginInfo * self), + (self)); From 4c5478744c0fc5dc9f41ba162cc417f268adb8f9 Mon Sep 17 00:00:00 2001 From: Jan Vaclav Date: Thu, 15 Jan 2026 11:59:49 +0100 Subject: [PATCH 36/56] vpn: wait for device to become available before creating l3cd In some situations, we will have a defined interface index, but no device, because the idle source was not processed yet. Reschedule _check_complete() in an idle source, so that it runs after the device is processed. Fixes: 306f9c490b2a ('vpn: Use nm_device_create_l3_config_data_from_connection if possible') Resolves: https://issues.redhat.com/browse/RHEL-125796 https://gitlab.freedesktop.org/NetworkManager/NetworkManager/-/merge_requests/2347 (cherry picked from commit 574411b8a56ebea82f5dcc62c0b31bc95747fb10) --- src/core/vpn/nm-vpn-connection.c | 30 +++++++++++++++++++++++++++++- 1 file changed, 29 insertions(+), 1 deletion(-) diff --git a/src/core/vpn/nm-vpn-connection.c b/src/core/vpn/nm-vpn-connection.c index 0b36459cf7..54478c5362 100644 --- a/src/core/vpn/nm-vpn-connection.c +++ b/src/core/vpn/nm-vpn-connection.c @@ -175,6 +175,7 @@ typedef struct { }; GSource *init_fail_on_idle_source; + GSource *check_device_added_idle_source; GSource *connect_timeout_source; GCancellable *main_cancellable; GVariant *connect_hash; @@ -229,6 +230,8 @@ static void _set_vpn_state(NMVpnConnection *self, static void _l3cfg_notify_cb(NML3Cfg *l3cfg, const NML3ConfigNotifyData *notify_data, NMVpnConnection *self); +static void _check_complete(NMVpnConnection *self, gboolean success); + /*****************************************************************************/ #define _NMLOG_DOMAIN LOGD_VPN @@ -1405,6 +1408,18 @@ fw_change_zone_cb(NMFirewalldManager *firewalld_manager, _apply_config(self); } +static gboolean +_check_device_added_idle_cb(gpointer user_data) +{ + NMVpnConnection *self = user_data; + NMVpnConnectionPrivate *priv = NM_VPN_CONNECTION_GET_PRIVATE(self); + + _check_complete(self, TRUE); + nm_clear_g_source_inst(&priv->check_device_added_idle_source); + + return G_SOURCE_CONTINUE; +} + static void _check_complete(NMVpnConnection *self, gboolean success) { @@ -1442,13 +1457,24 @@ _check_complete(NMVpnConnection *self, gboolean success) connection = _get_applied_connection(self); ifindex = nm_vpn_connection_get_ip_ifindex(self, FALSE); + device = nm_manager_get_device_by_ifindex(NM_MANAGER_GET, ifindex); + + /* We have a defined interface index, but the device is not processed yet. + * The processing of the new kernel link could be queued in an idle handler, + * so schedule an idle handler once to check if the device has been processed. + */ + if (ifindex > 0 && !device && !priv->check_device_added_idle_source) { + priv->check_device_added_idle_source = + nm_g_idle_add_source(_check_device_added_idle_cb, self); + return; + } + /* Use nm_device_create_l3_config_data_from_connection here if possible. This ensures that * connection properties like mdns, llmnr, dns-over-tls or dnssec are applied to vpn connections * If this vpn connection does not have its own device resort to nm_l3_config_data_new_from_connection * since we can't properly apply these properties anyway */ if (ifindex > 0) { - device = nm_manager_get_device_by_ifindex(NM_MANAGER_GET, ifindex); nm_assert(device); l3cd = nm_device_create_l3_config_data_from_connection(device, connection); } else { @@ -3042,6 +3068,8 @@ dispose(GObject *object) nm_clear_g_source_inst(&priv->init_fail_on_idle_source); + nm_clear_g_source_inst(&priv->check_device_added_idle_source); + nm_clear_g_cancellable(&priv->main_cancellable); nm_clear_g_source_inst(&priv->start_timeout_source); From b3d10555202d8551e2dc359bf2e4aaff4e9a1d07 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=8D=C3=B1igo=20Huguet?= Date: Tue, 23 Dec 2025 11:57:14 +0100 Subject: [PATCH 37/56] release.sh: add comments (cherry picked from commit d56cd26aeabe452e5b72f68a2b3a4fa876c3c7e3) --- contrib/fedora/rpm/release.sh | 22 +++++++++++++++------- 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/contrib/fedora/rpm/release.sh b/contrib/fedora/rpm/release.sh index ce98e22e35..74ab4347cc 100755 --- a/contrib/fedora/rpm/release.sh +++ b/contrib/fedora/rpm/release.sh @@ -409,11 +409,8 @@ if [ $CHECK_GITLAB = 1 ]; then fi fi -PUSH_REFS=() -BUILD_VERSION= - +# Work on a temporary branch CLEANUP_CHECKOUT_BRANCH="$CUR_BRANCH" - git checkout -B "$TMP_BRANCH" CLEANUP_REFS+=("refs/heads/$TMP_BRANCH") @@ -463,6 +460,7 @@ build_version() { local TAR_FILE="NetworkManager-$BUILD_VERSION.tar.xz" local SUM_FILE="$TAR_FILE.sha256sum" + # Bump version and tag the release set_version_number "$BUILD_VERSION" git commit -m "release: bump version to $BUILD_VERSION_DESCR" -a || die "failed to commit release" git tag -s -a -m "Release $BUILD_VERSION_DESCR" "$BUILD_VERSION" HEAD || die "failed to tag release" @@ -470,35 +468,43 @@ build_version() { PUSH_REFS+=("$BUILD_VERSION") CLEANUP_REFS+=("refs/tags/$BUILD_VERSION") - git checkout "$BUILD_VERSION" || die "failed to checkout $BUILD_VERSION" + # Build to get the tarball for the release ./contrib/fedora/rpm/build_clean.sh -r || die "build release failed" cp "./build/meson-dist/$TAR_FILE" /tmp/ || die "failed to copy $TAR_FILE to /tmp" cp "./build/meson-dist/$SUM_FILE" /tmp/ || die "failed to copy $SUM_FILE to /tmp" git clean -fdx + # Store the release version for later use RELEASE_VERSIONS+=("$BUILD_VERSION") } +# Build and create tarball. Bump version as needed. +PUSH_REFS=() RELEASE_VERSIONS=() if [ -n "$BUILD_VERSION" ]; then build_version "$BUILD_VERSION" "${BUILD_VERSION_DESCR:-$BUILD_VERSION}" fi -git checkout -B "$CUR_BRANCH" "$TMP_BRANCH" || die "cannot checkout $CUR_BRANCH" +# Work was done on the temporary branch, advance the real branch +git checkout -B "$CUR_BRANCH" "$TMP_BRANCH" || die "cannot checkout $CUR_BRANCH" PUSH_REFS+=( "$CUR_BRANCH" ) if [ "$RELEASE_MODE" = rc1 ]; then - git branch "$RELEASE_BRANCH" "$TMP_BRANCH" || die "cannot checkout $CUR_BRANCH" + # Create the release branch (nm-1-xx) + git branch "$RELEASE_BRANCH" "$TMP_BRANCH" || die "cannot checkout $RELEASE_BRANCH" PUSH_REFS+=( "$RELEASE_BRANCH" ) CLEANUP_REFS+=( "refs/heads/$RELEASE_BRANCH" ) + # Work on the temporary branch again git checkout "$TMP_BRANCH" + # Second release for rc1: create new dev version on main BUILD_VERSION="${VERSION_ARR[0]}.$((${VERSION_ARR[1]} + 2)).0" BUILD_VERSION_DESCR="$BUILD_VERSION (development)" BUILD_VERSION="${BUILD_VERSION}-dev" build_version "$BUILD_VERSION" "$BUILD_VERSION_DESCR" + # Work was done on the temporary branch, advance the real branch git checkout -B "$CUR_BRANCH" "$TMP_BRANCH" || die "cannot checkout $CUR_BRANCH" fi @@ -514,8 +520,10 @@ if [ -z "$GITLAB_USER_ID" ] || [ "$GITLAB_USER_ID" = "null" ]; then die "failed to authenticate to gitlab.freedesktop.org with the private token" fi +# Push the modified branches and tags to the origin repository do_command git push "$ORIGIN" "${PUSH_REFS[@]}" || die "failed to to push branches ${PUSH_REFS[@]} to $ORIGIN" +# Create the releases CREATE_RELEASE_FAIL=0 for BUILD_VERSION in "${RELEASE_VERSIONS[@]}"; do TAR_FILE="NetworkManager-$BUILD_VERSION.tar.xz" From d72562e365aed2abaefe88c9daa825347f069e47 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=8D=C3=B1igo=20Huguet?= Date: Tue, 23 Dec 2025 12:13:19 +0100 Subject: [PATCH 38/56] release.sh: assume that the version is already the right one Don't bump the version before tagging the release. Instead, assume that it's already correctly set. This is in preparation for the next commit where we will bump the version after the release, not before. But don't assume that in the case of rc1 and major releases. For rc1 we switch from devel releases to RC releases, and in major we switch from RC releases to stable releases. For example, when we are going to release 1.58-rc1, the current version will be 1.57.X-dev, so we need to bump to 1.58-rc1. When we're going to release 1.58.0, the current version will be 1.58-rcX, so we need to bump to 1.58.0. (cherry picked from commit 3a3a8ea59d6cf5e3df0976c1d6591bf77e7230fb) --- contrib/fedora/rpm/release.sh | 44 +++++++++++++++-------------------- 1 file changed, 19 insertions(+), 25 deletions(-) diff --git a/contrib/fedora/rpm/release.sh b/contrib/fedora/rpm/release.sh index 74ab4347cc..ad673dd918 100755 --- a/contrib/fedora/rpm/release.sh +++ b/contrib/fedora/rpm/release.sh @@ -415,21 +415,16 @@ git checkout -B "$TMP_BRANCH" CLEANUP_REFS+=("refs/heads/$TMP_BRANCH") case "$RELEASE_MODE" in - minor) - BUILD_VERSION="${VERSION_ARR[0]}.${VERSION_ARR[1]}.$(("${VERSION_ARR[2]}" + 1))" - ;; - devel) - BUILD_VERSION="${VERSION_ARR[0]}.${VERSION_ARR[1]}.$(("${VERSION_ARR[2]}" + 1))" - BUILD_VERSION_DESCR="$BUILD_VERSION (development)" - BUILD_VERSION="${BUILD_VERSION}-dev" + minor|devel|rc) + # Version is already correct in meson.build + BUILD_VERSION="$VERSION_STR" ;; rc1) + # Current version is wrong (dev version), need to set rc1 version BUILD_VERSION="${VERSION_ARR[0]}.$(("${VERSION_ARR[1]}" + 1))-rc1" ;; - rc) - BUILD_VERSION="${VERSION_ARR[0]}.${VERSION_ARR[1]}-rc$(( $RC_VERSION + 1 ))" - ;; major) + # Current version is wrong (rc version), need to set major version BUILD_VERSION="${VERSION_ARR[0]}.${VERSION_ARR[1]}.0" ;; major-post) @@ -445,9 +440,8 @@ case "$RELEASE_MODE" in git commit --amend -m tmp -a || die "failed to commit major version bump" test x = "x$(git diff main HEAD)" || die "there is a diff after merge!" - BUILD_VERSION="${VERSION_ARR[0]}.${VERSION_ARR[1]}.$(("${VERSION_ARR[2]}" + 1))" - BUILD_VERSION_DESCR="$BUILD_VERSION (development)" - BUILD_VERSION="${BUILD_VERSION}-dev" + # Version is already correct in meson.build + BUILD_VERSION="$VERSION_STR" ;; *) die "Release mode $RELEASE_MODE not yet implemented" @@ -455,16 +449,20 @@ case "$RELEASE_MODE" in esac build_version() { + local CURR_VERSION="$(get_version)" local BUILD_VERSION="$1" - local BUILD_VERSION_DESCR="$2" + local BUILD_VERSION_DESCR="${BUILD_VERSION/-dev/ (development)}" local TAR_FILE="NetworkManager-$BUILD_VERSION.tar.xz" local SUM_FILE="$TAR_FILE.sha256sum" - # Bump version and tag the release - set_version_number "$BUILD_VERSION" - git commit -m "release: bump version to $BUILD_VERSION_DESCR" -a || die "failed to commit release" - git tag -s -a -m "Release $BUILD_VERSION_DESCR" "$BUILD_VERSION" HEAD || die "failed to tag release" + # The current version is usually already correct, except for rc1 and major. Bump version in those cases. + if [[ "$BUILD_VERSION" != "$CURR_VERSION" ]]; then + set_version_number "$BUILD_VERSION" + git commit -m "release: bump version to $BUILD_VERSION_DESCR" -a || die "failed to commit release" + fi + # Tag the release + git tag -s -a -m "Release $BUILD_VERSION_DESCR" "$BUILD_VERSION" HEAD || die "failed to tag release" PUSH_REFS+=("$BUILD_VERSION") CLEANUP_REFS+=("refs/tags/$BUILD_VERSION") @@ -481,9 +479,7 @@ build_version() { # Build and create tarball. Bump version as needed. PUSH_REFS=() RELEASE_VERSIONS=() -if [ -n "$BUILD_VERSION" ]; then - build_version "$BUILD_VERSION" "${BUILD_VERSION_DESCR:-$BUILD_VERSION}" -fi +build_version "$BUILD_VERSION" # Work was done on the temporary branch, advance the real branch git checkout -B "$CUR_BRANCH" "$TMP_BRANCH" || die "cannot checkout $CUR_BRANCH" @@ -499,10 +495,8 @@ if [ "$RELEASE_MODE" = rc1 ]; then git checkout "$TMP_BRANCH" # Second release for rc1: create new dev version on main - BUILD_VERSION="${VERSION_ARR[0]}.$((${VERSION_ARR[1]} + 2)).0" - BUILD_VERSION_DESCR="$BUILD_VERSION (development)" - BUILD_VERSION="${BUILD_VERSION}-dev" - build_version "$BUILD_VERSION" "$BUILD_VERSION_DESCR" + BUILD_VERSION="${VERSION_ARR[0]}.$((${VERSION_ARR[1]} + 2)).0-dev" + build_version "$BUILD_VERSION" # Work was done on the temporary branch, advance the real branch git checkout -B "$CUR_BRANCH" "$TMP_BRANCH" || die "cannot checkout $CUR_BRANCH" From 0740459a5a5bc6876e51fbf5d42420783331527d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=8D=C3=B1igo=20Huguet?= Date: Tue, 23 Dec 2025 12:49:11 +0100 Subject: [PATCH 39/56] release.sh: bump version after release After tagging a release, create a commit bumping to the next version. This effectively ends the change in the logic initiated in the previous commit, from "bump version, then release" to "release, then bump version". The purpose of this is to have the right version set in nm_version.h and nm_version_macros.h between two releases. Without this change, when we introduced a new symbol, thus using the NM_AVAILABLE_IN_1_XX annotations, we got compilation warnings until we did the next release (making the CI to be red when configured the compilation to fail on warnings). (cherry picked from commit 5666407f156fcd7b328dff6838038ee6993e9f71) --- contrib/fedora/rpm/release.sh | 45 +++++++++++++++++++++++++---------- 1 file changed, 32 insertions(+), 13 deletions(-) diff --git a/contrib/fedora/rpm/release.sh b/contrib/fedora/rpm/release.sh index ad673dd918..2c21b4d98d 100755 --- a/contrib/fedora/rpm/release.sh +++ b/contrib/fedora/rpm/release.sh @@ -415,17 +415,30 @@ git checkout -B "$TMP_BRANCH" CLEANUP_REFS+=("refs/heads/$TMP_BRANCH") case "$RELEASE_MODE" in - minor|devel|rc) + minor) # Version is already correct in meson.build BUILD_VERSION="$VERSION_STR" + NEXT_VERSION="${VERSION_ARR[0]}.${VERSION_ARR[1]}.$((${VERSION_ARR[2]} + 1))" + ;; + devel) + # Version is already correct in meson.build + BUILD_VERSION="$VERSION_STR" + NEXT_VERSION="${VERSION_ARR[0]}.${VERSION_ARR[1]}.$((${VERSION_ARR[2]} + 1))-dev" + ;; + rc) + # Version is already correct in meson.build + BUILD_VERSION="$VERSION_STR" + NEXT_VERSION="${VERSION_ARR[0]}.${VERSION_ARR[1]}-rc$((RC_VERSION + 1))" ;; rc1) # Current version is wrong (dev version), need to set rc1 version - BUILD_VERSION="${VERSION_ARR[0]}.$(("${VERSION_ARR[1]}" + 1))-rc1" + BUILD_VERSION="${VERSION_ARR[0]}.$((${VERSION_ARR[1]} + 1))-rc1" + NEXT_VERSION="${VERSION_ARR[0]}.$((${VERSION_ARR[1]} + 1))-rc2" ;; major) # Current version is wrong (rc version), need to set major version BUILD_VERSION="${VERSION_ARR[0]}.${VERSION_ARR[1]}.0" + NEXT_VERSION="${VERSION_ARR[0]}.${VERSION_ARR[1]}.1" ;; major-post) # We create a merge commit with the content of current "main", with two @@ -442,6 +455,7 @@ case "$RELEASE_MODE" in # Version is already correct in meson.build BUILD_VERSION="$VERSION_STR" + NEXT_VERSION="${VERSION_ARR[0]}.${VERSION_ARR[1]}.$((${VERSION_ARR[2]} + 1))-dev" ;; *) die "Release mode $RELEASE_MODE not yet implemented" @@ -451,7 +465,9 @@ esac build_version() { local CURR_VERSION="$(get_version)" local BUILD_VERSION="$1" + local NEXT_VERSION="$2" local BUILD_VERSION_DESCR="${BUILD_VERSION/-dev/ (development)}" + local NEXT_VERSION_DESCR="${NEXT_VERSION/-dev/ (development)}" local TAR_FILE="NetworkManager-$BUILD_VERSION.tar.xz" local SUM_FILE="$TAR_FILE.sha256sum" @@ -474,16 +490,17 @@ build_version() { # Store the release version for later use RELEASE_VERSIONS+=("$BUILD_VERSION") + + # Bump to next version, so that build between now and the next release has the next version already. + # Otherwise the macros in nm_version.h don't work correctly. + set_version_number "$NEXT_VERSION" + git commit -m "release: bump version to $NEXT_VERSION_DESCR" -a || die "failed to commit version bump" } # Build and create tarball. Bump version as needed. PUSH_REFS=() RELEASE_VERSIONS=() -build_version "$BUILD_VERSION" - -# Work was done on the temporary branch, advance the real branch -git checkout -B "$CUR_BRANCH" "$TMP_BRANCH" || die "cannot checkout $CUR_BRANCH" -PUSH_REFS+=( "$CUR_BRANCH" ) +build_version "$BUILD_VERSION" "$NEXT_VERSION" if [ "$RELEASE_MODE" = rc1 ]; then # Create the release branch (nm-1-xx) @@ -491,17 +508,19 @@ if [ "$RELEASE_MODE" = rc1 ]; then PUSH_REFS+=( "$RELEASE_BRANCH" ) CLEANUP_REFS+=( "refs/heads/$RELEASE_BRANCH" ) - # Work on the temporary branch again - git checkout "$TMP_BRANCH" + # Go back to the commit of the rc1 release, nm-1-xx is one commit further now. + git checkout -B "$TMP_BRANCH" "$BUILD_VERSION" || die "cannot checkout $TMP_BRANCH" # Second release for rc1: create new dev version on main BUILD_VERSION="${VERSION_ARR[0]}.$((${VERSION_ARR[1]} + 2)).0-dev" - build_version "$BUILD_VERSION" - - # Work was done on the temporary branch, advance the real branch - git checkout -B "$CUR_BRANCH" "$TMP_BRANCH" || die "cannot checkout $CUR_BRANCH" + NEXT_VERSION="${VERSION_ARR[0]}.$((${VERSION_ARR[1]} + 2)).1-dev" + build_version "$BUILD_VERSION" "$NEXT_VERSION" fi +# Work was done on the temporary branch, advance the real branch +git checkout -B "$CUR_BRANCH" "$TMP_BRANCH" || die "cannot checkout $CUR_BRANCH" +PUSH_REFS+=( "$CUR_BRANCH" ) + if [[ $GITLAB_TOKEN == "" ]]; then [[ -r ~/.config/nm-release-token ]] || die "cannot read ~/.config/nm-release-token" GITLAB_TOKEN=$(< ~/.config/nm-release-token) From a39acb38e89e7098166cc86976f5834d2283e7e3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=8D=C3=B1igo=20Huguet?= Date: Wed, 24 Dec 2025 10:32:16 +0100 Subject: [PATCH 40/56] release.sh: fix a few small bugs and typos Fix typo freedestkop -> freedesktop. Removed unused argument of check_news (additionally, it was incorrectly using @ instead of $). Fixed incorrect use of `$? = 0` that was always successful. (cherry picked from commit 9a3462af99dfaefdfcee63426ed142fda290c462) --- contrib/fedora/rpm/release.sh | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/contrib/fedora/rpm/release.sh b/contrib/fedora/rpm/release.sh index 2c21b4d98d..6caf1100cb 100755 --- a/contrib/fedora/rpm/release.sh +++ b/contrib/fedora/rpm/release.sh @@ -27,7 +27,7 @@ # * Run in a "clean" environment, i.e. no unusual environment variables set, on a recent # Fedora, with suitable dependencies installed. # -# * First, ensure that you have a valid Gitlab's private token for gitlab.freedestkop.org +# * First, ensure that you have a valid Gitlab's private token for gitlab.freedesktop.org # stored in ~/.config/nm-release-token, or pass one with --gitlab-token argument. # Also, ensure you have a GPG key that you want to use for signing. Also, have gpg-agent running # and possibly configure `git config --get user.signingkey` for the proper key. @@ -155,8 +155,6 @@ set_version_number() { check_news() { local mode="$1" - shift - local ver_arr=("$@") case "$mode" in major|minor) @@ -361,7 +359,7 @@ if [ "$ALLOW_LOCAL_BRANCHES" != 1 ]; then cmp <(git show "$ORIGIN/main:contrib/fedora/rpm/release.sh") "$BASH_SOURCE_ABSOLUTE" || die "$BASH_SOURCE is not identical to \`git show \"$ORIGIN/main:contrib/fedora/rpm/release.sh\"\`" fi -if ! check_news "$RELEASE_MODE" "@{VERSION_ARR[@]}" ; then +if ! check_news "$RELEASE_MODE"; then if [ "$CHECK_NEWS" == 1 ]; then die "NEWS file needs update to mention stable release (skip check with --no-check-news)" fi @@ -592,7 +590,7 @@ for BUILD_VERSION in "${RELEASE_VERSIONS[@]}"; do END )" || FAIL=1 - if [[ $? != 0 ]]; then + if [[ $FAIL = 1 ]]; then fail_msg "failed to create NetworkManager $BUILD_VERSION release" CREATE_RELEASE_FAIL=1 continue From 045f328512cf4a160e48cb1f617baa8ea14c07e5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=8D=C3=B1igo=20Huguet?= Date: Tue, 23 Dec 2025 15:55:16 +0100 Subject: [PATCH 41/56] release: (manually) bump version to 1.56-rc3 After the previous commits, release.sh bumps the version after tagging the release, and not before. Therefore, it expects that the version is already the next one when doing the release. Manually bump the version this time so release.sh sees the right value the next time it's executed after these changes. (cherry picked from commit c0fe80ff87373bfb76de26613a0f69a15c43214d) --- meson.build | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/meson.build b/meson.build index 58f030dd7d..cea821bbb0 100644 --- a/meson.build +++ b/meson.build @@ -5,7 +5,7 @@ project( # NOTE: When incrementing version also add corresponding # NM_VERSION_x_y_z macros in # "src/libnm-core-public/nm-version-macros.h.in" - version: '1.56-rc2', + version: '1.56-rc3', license: 'GPL2+', default_options: [ 'buildtype=debugoptimized', From 8f3b8e0200ff34b9d64714d7d8c214f75a41f3be Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=8D=C3=B1igo=20Huguet?= Date: Tue, 23 Dec 2025 16:05:03 +0100 Subject: [PATCH 42/56] nm-version.h: use the right value of NM_API_VERSION After the changes in release.sh in previous commits, during development the value of NM_VERSION will always be the next version, not the latest released one. As a consequence, we don't need to set MICRO+1 in NM_API_VERSION, which was a temporary workaround. (cherry picked from commit 36275bc51caae466af1952666df2b5d37a2db718) --- src/libnm-core-public/nm-version-macros.h.in | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/libnm-core-public/nm-version-macros.h.in b/src/libnm-core-public/nm-version-macros.h.in index 9384917c3b..b8b89a4473 100644 --- a/src/libnm-core-public/nm-version-macros.h.in +++ b/src/libnm-core-public/nm-version-macros.h.in @@ -82,15 +82,15 @@ /* For releases, NM_API_VERSION is equal to NM_VERSION. * - * For development builds, NM_API_VERSION is the next - * stable API after NM_VERSION. When you run a development + * For development and RC builds, NM_API_VERSION is the next + * stable API after NM_VERSION. When you run a devel or RC * version, you are already using the future API, even if * it is not yet released. Hence, the currently used API * version is the future one. */ #define NM_API_VERSION \ (((NM_MINOR_VERSION % 2) == 1) \ ? NM_ENCODE_VERSION(NM_MAJOR_VERSION, NM_MINOR_VERSION + 1, 0) \ - : NM_ENCODE_VERSION(NM_MAJOR_VERSION, NM_MINOR_VERSION, NM_MICRO_VERSION + 1)) + : NM_VERSION) /* deprecated. */ #define NM_VERSION_CUR_STABLE NM_API_VERSION From 1bdcbdfd4f0d8b999c4153ed9a5f23fbe31eefae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=8D=C3=B1igo=20Huguet?= Date: Tue, 23 Dec 2025 16:09:39 +0100 Subject: [PATCH 43/56] nm-version: allow to define NM_VERSION_MAX_ALLOWED alone Previously, if NM_VERSION_MIN_REQUIRED was not defined, it defaulted to NM_VERSION. As a consequence, if NM_VERSION_MAX_ALLOWED was defined we got a compilation error because MAX_ALLOWED < MIN_REQUIRED. MAX_ALLOWED is used to get compilation warnings if you unintentionally use a libnm's symbol introduced in a newer version. MIN_REQUIRED is used to get rid of warnings about symbol deprecations. Libnm users may want to use MAX_ALLOWED alone, because using a too new symbol would fail to compile with older libnm. But they might want to get deprecation warnings as soon as possible, so they want to leave MIN_REQUIRED empty. (cherry picked from commit f849163e827290329564377e009e763cb134beaa) --- src/libnm-core-public/nm-version.h | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/libnm-core-public/nm-version.h b/src/libnm-core-public/nm-version.h index 775ed62747..af45b6792d 100644 --- a/src/libnm-core-public/nm-version.h +++ b/src/libnm-core-public/nm-version.h @@ -12,16 +12,16 @@ /* Deprecation / Availability macros */ -#if !defined(NM_VERSION_MIN_REQUIRED) || (NM_VERSION_MIN_REQUIRED == 0) -#undef NM_VERSION_MIN_REQUIRED -#define NM_VERSION_MIN_REQUIRED (NM_API_VERSION) -#endif - #if !defined(NM_VERSION_MAX_ALLOWED) || (NM_VERSION_MAX_ALLOWED == 0) #undef NM_VERSION_MAX_ALLOWED #define NM_VERSION_MAX_ALLOWED (NM_API_VERSION) #endif +#if !defined(NM_VERSION_MIN_REQUIRED) || (NM_VERSION_MIN_REQUIRED == 0) +#undef NM_VERSION_MIN_REQUIRED +#define NM_VERSION_MIN_REQUIRED (NM_VERSION_MAX_ALLOWED) +#endif + /* sanity checks */ #if NM_VERSION_MIN_REQUIRED > NM_API_VERSION #error "NM_VERSION_MIN_REQUIRED must be <= NM_API_VERSION" From 3981d392aa247762321e73dd27c4be993073d214 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vladim=C3=ADr=20Bene=C5=A1?= Date: Thu, 12 Feb 2026 11:37:43 +0100 Subject: [PATCH 44/56] NEWS: remove pre-release bits as we do 1.56.0 now --- NEWS | 5 ----- 1 file changed, 5 deletions(-) diff --git a/NEWS b/NEWS index c5eda5ed10..3ee8e88aca 100644 --- a/NEWS +++ b/NEWS @@ -3,11 +3,6 @@ NetworkManager-1.56 Overview of changes since NetworkManager-1.54 ============================================= -This is a snapshot of NetworkManager development. The API is -subject to change and not guaranteed to be compatible with -the later release. -USE AT YOUR OWN RISK. NOT RECOMMENDED FOR PRODUCTION USE! - * Unify the versioning to use everywhere the scheme with the -rcX or -dev suffixes when appropriate. This affects, for example, the URL and filename of the release tarball and the version reported by nmcli and the daemon. From 9188c9fa9b09c177cbd314a5d3fef901d7adf875 Mon Sep 17 00:00:00 2001 From: Beniamino Galvani Date: Fri, 9 Jan 2026 16:03:03 +0100 Subject: [PATCH 45/56] cloud-setup: fix format string MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit On a i686 machine the build fails with: ../src/nm-cloud-setup/main.c: In function ‘_oci_new_vlan_dev’: ../src/nm-cloud-setup/main.c:800:47: error: format ‘%ld’ expects argument of type ‘long int’, but argument 2 has type ‘gssize’ {aka ‘int’} [-Werror=format=] 800 | macvlan_name = g_strdup_printf("macvlan%ld", config_data->iface_idx); | ~~^ ~~~~~~~~~~~~~~~~~~~~~~ | | | | long int gssize {aka int} | %d ../src/nm-cloud-setup/main.c:801:42: error: format ‘%ld’ expects argument of type ‘long int’, but argument 3 has type ‘gssize’ {aka ‘int’} [-Werror=format=] 801 | connection_id = g_strdup_printf("%s%ld", connection_type, config_data->iface_idx); | ~~^ ~~~~~~~~~~~~~~~~~~~~~~ | | | | long int gssize {aka int} | %d Fixes: 68d7e177378b ('Reapply "cloud-setup: create VLANs for multiple VNICs on OCI"') (cherry picked from commit 748be9a3e7ac8706fbed21dd921d7e8058672194) --- src/nm-cloud-setup/main.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/nm-cloud-setup/main.c b/src/nm-cloud-setup/main.c index d2a5a1cf5e..51c6510715 100644 --- a/src/nm-cloud-setup/main.c +++ b/src/nm-cloud-setup/main.c @@ -797,8 +797,8 @@ _oci_new_vlan_dev(SigTermData *sigterm_data, connection = _new_connection(); - macvlan_name = g_strdup_printf("macvlan%ld", config_data->iface_idx); - connection_id = g_strdup_printf("%s%ld", connection_type, config_data->iface_idx); + macvlan_name = g_strdup_printf("macvlan%" G_GSSIZE_FORMAT, config_data->iface_idx); + connection_id = g_strdup_printf("%s%" G_GSSIZE_FORMAT, connection_type, config_data->iface_idx); wired_mac_addr = parent_hwaddr; if (nm_streq(connection_type, NM_SETTING_MACVLAN_SETTING_NAME)) { From a0e03b122837e137422b25a287d62ce47b8af5c4 Mon Sep 17 00:00:00 2001 From: Beniamino Galvani Date: Fri, 28 Nov 2025 17:46:49 +0100 Subject: [PATCH 46/56] supplicant: fix center channel calculation The formula is wrong for channels above 144 because the layout of the 80MHz channels is not regular. Use a lookup table. Fixes: 7bb596177966 ('supplicant: honor the 'wifi.channel-width' property in AP mode') (cherry picked from commit 5763b9b4de64ac3ce1b9c1e6f00d0736de763f94) --- src/core/supplicant/nm-supplicant-config.c | 28 ++++++++++++++++++---- 1 file changed, 23 insertions(+), 5 deletions(-) diff --git a/src/core/supplicant/nm-supplicant-config.c b/src/core/supplicant/nm-supplicant-config.c index d026d9ccad..233afe4894 100644 --- a/src/core/supplicant/nm-supplicant-config.c +++ b/src/core/supplicant/nm-supplicant-config.c @@ -531,6 +531,7 @@ get_ap_params(guint freq, case NM_SETTING_WIRELESS_CHANNEL_WIDTH_80MHZ: { guint channel; + guint center_channel = 0; if (freq < 5000) { /* the setting is not valid */ @@ -540,12 +541,29 @@ get_ap_params(guint freq, /* Determine the center channel according to the table at * https://en.wikipedia.org/wiki/List_of_WLAN_channels */ - channel = (freq - 5000) / 5; - channel = ((channel / 4 - 1) / 4) * 16 + 10; - *out_ht40 = 1; - *out_max_oper_chwidth = 1; - *out_center_freq = 5000 + 5 * channel; + 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; } From cf52d3f52b71806f9068b7afa8517f3c8ffc4778 Mon Sep 17 00:00:00 2001 From: Jan Vaclav Date: Wed, 3 Dec 2025 11:19:47 +0100 Subject: [PATCH 47/56] test-link: test bond with use_carrier=1 `use_carrier` is removed from kernel since 6.18 [1], and returns the following error if set to 0: > option obsolete, use_carrier cannot be disabled This causes a failure of test-link-linux, so let's set it to 1. [1] https://lore.kernel.org/all/2029487.1756512517@famine/ (cherry picked from commit d40e88fd02ccbc07a5e0c7e3cb8a75dee830d1b8) --- src/core/platform/tests/test-link.c | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/core/platform/tests/test-link.c b/src/core/platform/tests/test-link.c index fab6bd2efe..77e7e64121 100644 --- a/src/core/platform/tests/test-link.c +++ b/src/core/platform/tests/test-link.c @@ -123,7 +123,8 @@ software_add(NMLinkType link_type, const char *name) gboolean bond0_exists = !!nm_platform_link_get_by_ifname(NM_PLATFORM_GET, "bond0"); int r; const NMPlatformLnkBond nm_platform_lnk_bond_default = { - .mode = nmtst_rand_select(3, 1), + .mode = nmtst_rand_select(3, 1), + .use_carrier = 1, }; r = nm_platform_link_bond_add(NM_PLATFORM_GET, name, &nm_platform_lnk_bond_default, NULL); From 56b51b98fbb8627c4c09a483702e18fd8aee7ce1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vladim=C3=ADr=20Bene=C5=A1?= Date: Thu, 12 Feb 2026 23:14:41 +0100 Subject: [PATCH 48/56] release: bump version to 1.56.0 --- meson.build | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/meson.build b/meson.build index cea821bbb0..0657ce5af9 100644 --- a/meson.build +++ b/meson.build @@ -5,7 +5,7 @@ project( # NOTE: When incrementing version also add corresponding # NM_VERSION_x_y_z macros in # "src/libnm-core-public/nm-version-macros.h.in" - version: '1.56-rc3', + version: '1.56.0', license: 'GPL2+', default_options: [ 'buildtype=debugoptimized', From f70b37357ae61c2b1756df8ab80e44dd00bd643e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vladim=C3=ADr=20Bene=C5=A1?= Date: Fri, 13 Feb 2026 13:30:02 +0100 Subject: [PATCH 49/56] release: bump version to 1.57.3 (development) --- meson.build | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/meson.build b/meson.build index 733f545885..8b0334240d 100644 --- a/meson.build +++ b/meson.build @@ -5,7 +5,7 @@ project( # NOTE: When incrementing version also add corresponding # NM_VERSION_x_y_z macros in # "src/libnm-core-public/nm-version-macros.h.in" - version: '1.57.2-dev', + version: '1.57.3-dev', license: 'GPL2+', default_options: [ 'buildtype=debugoptimized', From 40f19ad6741d4df3cfae7ab4fa1b95d974cdc472 Mon Sep 17 00:00:00 2001 From: Federico Ton Date: Wed, 28 Jan 2026 19:07:09 +0100 Subject: [PATCH 50/56] man: fix sentence in nmcli manual page A not very clear sentence in the description of the `nmcli device checkpoint` command has been changed. --- man/nmcli.xml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/man/nmcli.xml b/man/nmcli.xml index 5c6d6d3e78..764e5faca9 100644 --- a/man/nmcli.xml +++ b/man/nmcli.xml @@ -1852,9 +1852,9 @@ connections with an option of restoring the network configuration to a known good state in case of an error. - If the a list of interface names is specified, the checkpoint is - taken, the checkpoint is takes only on the specified devices. Otherwise - a checkpoint is taken for all devices. + If a list of interface names is specified, the checkpoint is + taken only on the specified devices. Otherwise a checkpoint is taken for + all devices. Currently the timeout defaults to 15 seconds. This may change in a future version. From 8c93d0bdffd2774b995ef962c6eb6e1f1ab3367c Mon Sep 17 00:00:00 2001 From: Mattia Dal Ben Date: Fri, 6 Feb 2026 19:24:45 +0100 Subject: [PATCH 51/56] introspection: fix documentation for GetSecrets --- .../org.freedesktop.NetworkManager.Settings.Connection.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/introspection/org.freedesktop.NetworkManager.Settings.Connection.xml b/introspection/org.freedesktop.NetworkManager.Settings.Connection.xml index 9b876e75d8..d5717d5d22 100644 --- a/introspection/org.freedesktop.NetworkManager.Settings.Connection.xml +++ b/introspection/org.freedesktop.NetworkManager.Settings.Connection.xml @@ -62,7 +62,7 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/po/POTFILES.in b/po/POTFILES.in index 0bf3ca1c25..b66ca45263 100644 --- a/po/POTFILES.in +++ b/po/POTFILES.in @@ -11,6 +11,7 @@ src/core/devices/nm-device-bridge.c src/core/devices/nm-device-dummy.c src/core/devices/nm-device-ethernet-utils.c src/core/devices/nm-device-ethernet.c +src/core/devices/nm-device-geneve.c src/core/devices/nm-device-infiniband.c src/core/devices/nm-device-ip-tunnel.c src/core/devices/nm-device-loopback.c @@ -46,6 +47,7 @@ src/libnm-client-impl/nm-device-bt.c src/libnm-client-impl/nm-device-dummy.c src/libnm-client-impl/nm-device-ethernet.c src/libnm-client-impl/nm-device-generic.c +src/libnm-client-impl/nm-device-geneve.c src/libnm-client-impl/nm-device-hsr.c src/libnm-client-impl/nm-device-infiniband.c src/libnm-client-impl/nm-device-ip-tunnel.c diff --git a/src/core/devices/nm-device-factory.c b/src/core/devices/nm-device-factory.c index 1585836281..24755a5f00 100644 --- a/src/core/devices/nm-device-factory.c +++ b/src/core/devices/nm-device-factory.c @@ -412,6 +412,7 @@ nm_device_factory_manager_load_factories(NMDeviceFactoryManagerFactoryFunc callb _ADD_INTERNAL(nm_dummy_device_factory_get_type); _ADD_INTERNAL(nm_ethernet_device_factory_get_type); _ADD_INTERNAL(nm_generic_device_factory_get_type); + _ADD_INTERNAL(nm_geneve_device_factory_get_type); _ADD_INTERNAL(nm_hsr_device_factory_get_type); _ADD_INTERNAL(nm_infiniband_device_factory_get_type); _ADD_INTERNAL(nm_ip_tunnel_device_factory_get_type); diff --git a/src/core/devices/nm-device-geneve.c b/src/core/devices/nm-device-geneve.c new file mode 100644 index 0000000000..0968a2fb9b --- /dev/null +++ b/src/core/devices/nm-device-geneve.c @@ -0,0 +1,487 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +/* + * Copyright (C) 2026 Red Hat, Inc. + */ + +#include "src/core/nm-default-daemon.h" + +#include "nm-manager.h" +#include "nm-device-geneve.h" + +#include "libnm-core-intern/nm-core-internal.h" +#include "nm-act-request.h" +#include "nm-device-private.h" +#include "nm-setting-geneve.h" +#include "libnm-platform/nm-platform.h" +#include "nm-device-factory.h" + +#define _NMLOG_DEVICE_TYPE NMDeviceGeneve +#include "nm-device-logging.h" + +NM_GOBJECT_PROPERTIES_DEFINE(NMDeviceGeneve, + PROP_ID, + PROP_REMOTE, + PROP_TOS, + PROP_TTL, + PROP_DF, + PROP_DST_PORT, ); + +typedef struct { + NMPlatformLnkGeneve props; +} NMDeviceGenevePrivate; + +struct _NMDeviceGeneve { + NMDevice parent; + NMDeviceGenevePrivate _priv; +}; + +struct _NMDeviceGeneveClass { + NMDeviceClass parent; +}; + +G_DEFINE_TYPE(NMDeviceGeneve, nm_device_geneve, NM_TYPE_DEVICE) + +#define NM_DEVICE_GENEVE_GET_PRIVATE(self) \ + _NM_GET_PRIVATE(self, NMDeviceGeneve, NM_IS_DEVICE_GENEVE, NMDevice) + +/*****************************************************************************/ + +static NMDeviceCapabilities +get_generic_capabilities(NMDevice *dev) +{ + return NM_DEVICE_CAP_IS_SOFTWARE; +} + +static void +update_properties(NMDevice *device) +{ + NMDeviceGeneve *self; + NMDeviceGenevePrivate *priv; + const NMPlatformLink *plink; + const NMPlatformLnkGeneve *props; + int ifindex; + + g_return_if_fail(NM_IS_DEVICE_GENEVE(device)); + self = NM_DEVICE_GENEVE(device); + priv = NM_DEVICE_GENEVE_GET_PRIVATE(self); + + ifindex = nm_device_get_ifindex(device); + g_return_if_fail(ifindex > 0); + props = nm_platform_link_get_lnk_geneve(nm_device_get_platform(device), ifindex, &plink); + + if (!props) { + _LOGW(LOGD_PLATFORM, "could not get GENEVE properties"); + return; + } + + g_object_freeze_notify((GObject *) device); + +#define CHECK_PROPERTY_CHANGED(field, prop) \ + G_STMT_START \ + { \ + if (priv->props.field != props->field) { \ + priv->props.field = props->field; \ + _notify(self, prop); \ + } \ + } \ + G_STMT_END + +#define CHECK_PROPERTY_CHANGED_IN6ADDR(field, prop) \ + G_STMT_START \ + { \ + if (memcmp(&priv->props.field, &props->field, sizeof(props->field)) != 0) { \ + priv->props.field = props->field; \ + _notify(self, prop); \ + } \ + } \ + G_STMT_END + + CHECK_PROPERTY_CHANGED(id, PROP_ID); + CHECK_PROPERTY_CHANGED(remote, PROP_REMOTE); + CHECK_PROPERTY_CHANGED_IN6ADDR(remote6, PROP_REMOTE); + CHECK_PROPERTY_CHANGED(tos, PROP_TOS); + CHECK_PROPERTY_CHANGED(ttl, PROP_TTL); + CHECK_PROPERTY_CHANGED(df, PROP_DF); + CHECK_PROPERTY_CHANGED(dst_port, PROP_DST_PORT); + + g_object_thaw_notify((GObject *) device); +} + +static void +link_changed(NMDevice *device, const NMPlatformLink *pllink) +{ + NM_DEVICE_CLASS(nm_device_geneve_parent_class)->link_changed(device, pllink); + update_properties(device); +} + +static void +unrealize_notify(NMDevice *device) +{ + NMDeviceGeneve *self = NM_DEVICE_GENEVE(device); + NMDeviceGenevePrivate *priv = NM_DEVICE_GENEVE_GET_PRIVATE(self); + guint i; + + NM_DEVICE_CLASS(nm_device_geneve_parent_class)->unrealize_notify(device); + + memset(&priv->props, 0, sizeof(NMPlatformLnkGeneve)); + + for (i = 1; i < _PROPERTY_ENUMS_LAST; i++) + g_object_notify_by_pspec(G_OBJECT(self), obj_properties[i]); +} + +static gboolean +create_and_realize(NMDevice *device, + NMConnection *connection, + NMDevice *parent, + const NMPlatformLink **out_plink, + GError **error) +{ + const char *iface = nm_device_get_iface(device); + NMPlatformLnkGeneve props = {}; + NMSettingGeneve *s_geneve; + const char *str; + int r; + + s_geneve = nm_connection_get_setting_geneve(connection); + g_return_val_if_fail(s_geneve, FALSE); + + props.id = nm_setting_geneve_get_id(s_geneve); + + str = nm_setting_geneve_get_remote(s_geneve); + if (!nm_inet_parse_bin(AF_INET, str, NULL, &props.remote) + && !nm_inet_parse_bin(AF_INET6, str, NULL, &props.remote6)) { + return nm_assert_unreachable_val(FALSE); + } + props.tos = nm_setting_geneve_get_tos(s_geneve); + props.ttl = nm_setting_geneve_get_ttl(s_geneve); + props.df = nm_setting_geneve_get_df(s_geneve); + props.dst_port = nm_setting_geneve_get_destination_port(s_geneve); + + r = nm_platform_link_geneve_add(nm_device_get_platform(device), iface, &props, out_plink); + if (r < 0) { + g_set_error(error, + NM_DEVICE_ERROR, + NM_DEVICE_ERROR_CREATION_FAILED, + "Failed to create geneve interface '%s' for '%s': %s", + iface, + nm_connection_get_id(connection), + nm_strerror(r)); + return FALSE; + } + + return TRUE; +} + +static gboolean +address_matches(const char *candidate, in_addr_t addr4, struct in6_addr *addr6) +{ + NMIPAddr candidate_addr; + int addr_family; + + if (!candidate) + return addr4 == 0u && IN6_IS_ADDR_UNSPECIFIED(addr6); + + if (!nm_inet_parse_bin(AF_UNSPEC, candidate, &addr_family, &candidate_addr)) + return FALSE; + + if (!nm_ip_addr_equal(addr_family, + &candidate_addr, + NM_IS_IPv4(addr_family) ? (gpointer) &addr4 : addr6)) + return FALSE; + + if (NM_IS_IPv4(addr_family)) + return IN6_IS_ADDR_UNSPECIFIED(addr6); + else + return addr4 == 0u; +} + +static gboolean +check_connection_compatible(NMDevice *device, + NMConnection *connection, + gboolean check_properties, + GError **error) +{ + NMDeviceGenevePrivate *priv = NM_DEVICE_GENEVE_GET_PRIVATE(device); + NMSettingGeneve *s_geneve; + + if (!NM_DEVICE_CLASS(nm_device_geneve_parent_class) + ->check_connection_compatible(device, connection, check_properties, error)) + return FALSE; + + if (check_properties && nm_device_is_real(device)) { + s_geneve = nm_connection_get_setting_geneve(connection); + + if (priv->props.id != nm_setting_geneve_get_id(s_geneve)) { + nm_utils_error_set_literal(error, + NM_UTILS_ERROR_CONNECTION_AVAILABLE_TEMPORARY, + "geneve id mismatches"); + return FALSE; + } + + if (!address_matches(nm_setting_geneve_get_remote(s_geneve), + priv->props.remote, + &priv->props.remote6)) { + nm_utils_error_set_literal(error, + NM_UTILS_ERROR_CONNECTION_AVAILABLE_TEMPORARY, + "geneve remote address mismatches"); + return FALSE; + } + + if (priv->props.dst_port != nm_setting_geneve_get_destination_port(s_geneve)) { + nm_utils_error_set_literal(error, + NM_UTILS_ERROR_CONNECTION_AVAILABLE_TEMPORARY, + "geneve destination port mismatches"); + return FALSE; + } + + if (priv->props.tos != nm_setting_geneve_get_tos(s_geneve)) { + nm_utils_error_set_literal(error, + NM_UTILS_ERROR_CONNECTION_AVAILABLE_TEMPORARY, + "geneve TOS mismatches"); + return FALSE; + } + + if (priv->props.ttl != nm_setting_geneve_get_ttl(s_geneve)) { + nm_utils_error_set_literal(error, + NM_UTILS_ERROR_CONNECTION_AVAILABLE_TEMPORARY, + "geneve TTL mismatches"); + return FALSE; + } + + if (priv->props.df != nm_setting_geneve_get_df(s_geneve)) { + nm_utils_error_set_literal(error, + NM_UTILS_ERROR_CONNECTION_AVAILABLE_TEMPORARY, + "geneve DF mismatches"); + return FALSE; + } + } + + return TRUE; +} + +static gboolean +complete_connection(NMDevice *device, + NMConnection *connection, + const char *specific_object, + NMConnection *const *existing_connections, + GError **error) +{ + NMSettingGeneve *s_geneve; + + nm_utils_complete_generic(nm_device_get_platform(device), + connection, + NM_SETTING_GENEVE_SETTING_NAME, + existing_connections, + NULL, + _("Geneve connection"), + NULL, + NULL); + + s_geneve = nm_connection_get_setting_geneve(connection); + if (!s_geneve) { + g_set_error_literal(error, + NM_DEVICE_ERROR, + NM_DEVICE_ERROR_INVALID_CONNECTION, + "A 'geneve' setting is required."); + return FALSE; + } + + return TRUE; +} + +static void +update_connection(NMDevice *device, NMConnection *connection) +{ + NMDeviceGenevePrivate *priv = NM_DEVICE_GENEVE_GET_PRIVATE(device); + NMSettingGeneve *s_geneve = _nm_connection_ensure_setting(connection, NM_TYPE_SETTING_GENEVE); + char sbuf[NM_INET_ADDRSTRLEN]; + + if (priv->props.id != nm_setting_geneve_get_id(s_geneve)) + g_object_set(G_OBJECT(s_geneve), NM_SETTING_GENEVE_ID, priv->props.id, NULL); + + /* Handle remote (IPv4 or IPv6) */ + if (priv->props.remote) { + g_object_set(s_geneve, + NM_SETTING_GENEVE_REMOTE, + nm_inet4_ntop(priv->props.remote, sbuf), + NULL); + } else if (memcmp(&priv->props.remote6, &in6addr_any, sizeof(in6addr_any))) { + g_object_set(s_geneve, + NM_SETTING_GENEVE_REMOTE, + nm_inet6_ntop(&priv->props.remote6, sbuf), + NULL); + } + + if (priv->props.dst_port != nm_setting_geneve_get_destination_port(s_geneve)) + g_object_set(G_OBJECT(s_geneve), + NM_SETTING_GENEVE_DESTINATION_PORT, + priv->props.dst_port, + NULL); + + if (priv->props.tos != nm_setting_geneve_get_tos(s_geneve)) + g_object_set(G_OBJECT(s_geneve), NM_SETTING_GENEVE_TOS, priv->props.tos, NULL); + + if (priv->props.ttl != nm_setting_geneve_get_ttl(s_geneve)) + g_object_set(G_OBJECT(s_geneve), NM_SETTING_GENEVE_TTL, priv->props.ttl, NULL); + + if (priv->props.df != nm_setting_geneve_get_df(s_geneve)) + g_object_set(G_OBJECT(s_geneve), NM_SETTING_GENEVE_DF, priv->props.df, NULL); +} + +static void +get_property(GObject *object, guint prop_id, GValue *value, GParamSpec *pspec) +{ + NMDeviceGenevePrivate *priv = NM_DEVICE_GENEVE_GET_PRIVATE(object); + + switch (prop_id) { + case PROP_ID: + g_value_set_uint(value, priv->props.id); + break; + case PROP_REMOTE: + if (priv->props.remote) + g_value_take_string(value, nm_inet4_ntop_dup(priv->props.remote)); + else if (!IN6_IS_ADDR_UNSPECIFIED(&priv->props.remote6)) + g_value_take_string(value, nm_inet6_ntop_dup(&priv->props.remote6)); + break; + case PROP_TOS: + g_value_set_uchar(value, priv->props.tos); + break; + case PROP_TTL: + g_value_set_uchar(value, priv->props.ttl); + break; + case PROP_DF: + g_value_set_uint(value, priv->props.df); + break; + case PROP_DST_PORT: + g_value_set_uint(value, priv->props.dst_port); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec); + break; + } +} + +/*****************************************************************************/ + +static void +nm_device_geneve_init(NMDeviceGeneve *self) +{} + +static const NMDBusInterfaceInfoExtended interface_info_device_geneve = { + .parent = NM_DEFINE_GDBUS_INTERFACE_INFO_INIT( + NM_DBUS_INTERFACE_DEVICE_GENEVE, + .properties = NM_DEFINE_GDBUS_PROPERTY_INFOS( + NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE("Id", "u", NM_DEVICE_GENEVE_ID), + NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE("Remote", "s", NM_DEVICE_GENEVE_REMOTE), + NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE("Tos", "y", NM_DEVICE_GENEVE_TOS), + NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE("Ttl", "y", NM_DEVICE_GENEVE_TTL), + NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE("Df", "u", NM_DEVICE_GENEVE_DF), + NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE("DstPort", + "q", + NM_DEVICE_GENEVE_DST_PORT), ), ), +}; + +static void +nm_device_geneve_class_init(NMDeviceGeneveClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS(klass); + NMDBusObjectClass *dbus_object_class = NM_DBUS_OBJECT_CLASS(klass); + NMDeviceClass *device_class = NM_DEVICE_CLASS(klass); + + object_class->get_property = get_property; + + dbus_object_class->interface_infos = NM_DBUS_INTERFACE_INFOS(&interface_info_device_geneve); + + device_class->connection_type_supported = NM_SETTING_GENEVE_SETTING_NAME; + device_class->connection_type_check_compatible = NM_SETTING_GENEVE_SETTING_NAME; + device_class->link_types = NM_DEVICE_DEFINE_LINK_TYPES(NM_LINK_TYPE_GENEVE); + + device_class->link_changed = link_changed; + device_class->unrealize_notify = unrealize_notify; + device_class->create_and_realize = create_and_realize; + device_class->check_connection_compatible = check_connection_compatible; + device_class->complete_connection = complete_connection; + device_class->get_generic_capabilities = get_generic_capabilities; + device_class->update_connection = update_connection; + + obj_properties[PROP_ID] = g_param_spec_uint(NM_DEVICE_GENEVE_ID, + "", + "", + 0, + G_MAXUINT32, + 0, + G_PARAM_READABLE | G_PARAM_STATIC_STRINGS); + + obj_properties[PROP_REMOTE] = g_param_spec_string(NM_DEVICE_GENEVE_REMOTE, + "", + "", + NULL, + G_PARAM_READABLE | G_PARAM_STATIC_STRINGS); + + obj_properties[PROP_TOS] = g_param_spec_uchar(NM_DEVICE_GENEVE_TOS, + "", + "", + 0, + 255, + 0, + G_PARAM_READABLE | G_PARAM_STATIC_STRINGS); + + obj_properties[PROP_TTL] = g_param_spec_uchar(NM_DEVICE_GENEVE_TTL, + "", + "", + 0, + 255, + 0, + G_PARAM_READABLE | G_PARAM_STATIC_STRINGS); + + obj_properties[PROP_DF] = g_param_spec_uint(NM_DEVICE_GENEVE_DF, + "", + "", + 0, + 2, + 0, + G_PARAM_READABLE | G_PARAM_STATIC_STRINGS); + + obj_properties[PROP_DST_PORT] = g_param_spec_uint(NM_DEVICE_GENEVE_DST_PORT, + "", + "", + 0, + 65535, + 0, + G_PARAM_READABLE | G_PARAM_STATIC_STRINGS); + + g_object_class_install_properties(object_class, _PROPERTY_ENUMS_LAST, obj_properties); +} + +/*****************************************************************************/ + +#define NM_TYPE_GENEVE_DEVICE_FACTORY (nm_geneve_device_factory_get_type()) +#define NM_GENEVE_DEVICE_FACTORY(obj) \ + (_NM_G_TYPE_CHECK_INSTANCE_CAST((obj), NM_TYPE_GENEVE_DEVICE_FACTORY, NMGeneveDeviceFactory)) + +static NMDevice * +create_device(NMDeviceFactory *factory, + const char *iface, + const NMPlatformLink *plink, + NMConnection *connection, + gboolean *out_ignore) +{ + return g_object_new(NM_TYPE_DEVICE_GENEVE, + NM_DEVICE_IFACE, + iface, + NM_DEVICE_TYPE_DESC, + "Geneve", + NM_DEVICE_DEVICE_TYPE, + NM_DEVICE_TYPE_GENEVE, + NM_DEVICE_LINK_TYPE, + NM_LINK_TYPE_GENEVE, + NULL); +} + +NM_DEVICE_FACTORY_DEFINE_INTERNAL( + GENEVE, + Geneve, + geneve, + NM_DEVICE_FACTORY_DECLARE_LINK_TYPES(NM_LINK_TYPE_GENEVE) + NM_DEVICE_FACTORY_DECLARE_SETTING_TYPES(NM_SETTING_GENEVE_SETTING_NAME), + factory_class->create_device = create_device;); diff --git a/src/core/devices/nm-device-geneve.h b/src/core/devices/nm-device-geneve.h new file mode 100644 index 0000000000..d0a44e9dbb --- /dev/null +++ b/src/core/devices/nm-device-geneve.h @@ -0,0 +1,33 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +/* + * Copyright (C) 2026 Red Hat, Inc. + */ + +#ifndef __NETWORKMANAGER_DEVICE_GENEVE_H__ +#define __NETWORKMANAGER_DEVICE_GENEVE_H__ + +#include "nm-device.h" + +#define NM_TYPE_DEVICE_GENEVE (nm_device_geneve_get_type()) +#define NM_DEVICE_GENEVE(obj) \ + (_NM_G_TYPE_CHECK_INSTANCE_CAST((obj), NM_TYPE_DEVICE_GENEVE, NMDeviceGeneve)) +#define NM_DEVICE_GENEVE_CLASS(klass) \ + (G_TYPE_CHECK_CLASS_CAST((klass), NM_TYPE_DEVICE_GENEVE, NMDeviceGeneveClass)) +#define NM_IS_DEVICE_GENEVE(obj) (G_TYPE_CHECK_INSTANCE_TYPE((obj), NM_TYPE_DEVICE_GENEVE)) +#define NM_IS_DEVICE_GENEVE_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE((klass), NM_TYPE_DEVICE_GENEVE)) +#define NM_DEVICE_GENEVE_GET_CLASS(obj) \ + (G_TYPE_INSTANCE_GET_CLASS((obj), NM_TYPE_DEVICE_GENEVE, NMDeviceGeneveClass)) + +#define NM_DEVICE_GENEVE_ID "id" +#define NM_DEVICE_GENEVE_REMOTE "remote" +#define NM_DEVICE_GENEVE_TOS "tos" +#define NM_DEVICE_GENEVE_TTL "ttl" +#define NM_DEVICE_GENEVE_DF "df" +#define NM_DEVICE_GENEVE_DST_PORT "dst-port" + +typedef struct _NMDeviceGeneve NMDeviceGeneve; +typedef struct _NMDeviceGeneveClass NMDeviceGeneveClass; + +GType nm_device_geneve_get_type(void); + +#endif /* __NETWORKMANAGER_DEVICE_GENEVE_H__ */ diff --git a/src/core/devices/nm-device.c b/src/core/devices/nm-device.c index 434683d6b4..f3f09db18c 100644 --- a/src/core/devices/nm-device.c +++ b/src/core/devices/nm-device.c @@ -5962,7 +5962,6 @@ nm_device_get_route_metric_default(NMDeviceType device_type) * in some aspects a VPN. */ case NM_DEVICE_TYPE_WIREGUARD: return NM_VPN_ROUTE_METRIC_DEFAULT; - case NM_DEVICE_TYPE_ETHERNET: case NM_DEVICE_TYPE_VETH: return 100; @@ -5996,6 +5995,8 @@ nm_device_get_route_metric_default(NMDeviceType device_type) return 470; case NM_DEVICE_TYPE_VXLAN: return 500; + case NM_DEVICE_TYPE_GENEVE: + return 525; case NM_DEVICE_TYPE_DUMMY: return 550; case NM_DEVICE_TYPE_WIFI: @@ -19567,7 +19568,7 @@ set_property(GObject *object, guint prop_id, const GValue *value, GParamSpec *ps nm_assert(priv->type == NM_DEVICE_TYPE_UNKNOWN); priv->type = g_value_get_uint(value); nm_assert(priv->type > NM_DEVICE_TYPE_UNKNOWN); - nm_assert(priv->type <= NM_DEVICE_TYPE_IPVLAN); + nm_assert(priv->type <= NM_DEVICE_TYPE_GENEVE); break; case PROP_LINK_TYPE: /* construct-only */ diff --git a/src/core/meson.build b/src/core/meson.build index 26d27f96e2..4a3dfd8e11 100644 --- a/src/core/meson.build +++ b/src/core/meson.build @@ -111,6 +111,7 @@ libNetworkManager = static_library( 'devices/nm-device-ethernet-utils.c', 'devices/nm-device-factory.c', 'devices/nm-device-generic.c', + 'devices/nm-device-geneve.c', 'devices/nm-device-hsr.c', 'devices/nm-device-infiniband.c', 'devices/nm-device-ip-tunnel.c', diff --git a/src/libnm-client-impl/libnm.ver b/src/libnm-client-impl/libnm.ver index 3d2bc79304..e9850ea6e5 100644 --- a/src/libnm-client-impl/libnm.ver +++ b/src/libnm-client-impl/libnm.ver @@ -2107,6 +2107,13 @@ global: libnm_1_58_0 { global: nm_connection_get_setting_geneve; + nm_device_geneve_get_df; + nm_device_geneve_get_dst_port; + nm_device_geneve_get_id; + nm_device_geneve_get_remote; + nm_device_geneve_get_tos; + nm_device_geneve_get_ttl; + nm_device_geneve_get_type; nm_ip_config_get_clat_address; nm_ip_config_get_clat_pref64; nm_setting_geneve_df_get_type; diff --git a/src/libnm-client-impl/meson.build b/src/libnm-client-impl/meson.build index b49366292f..3352ebfee0 100644 --- a/src/libnm-client-impl/meson.build +++ b/src/libnm-client-impl/meson.build @@ -16,6 +16,7 @@ libnm_client_impl_sources = files( 'nm-device-bt.c', 'nm-device-dummy.c', 'nm-device-ethernet.c', + 'nm-device-geneve.c', 'nm-device-generic.c', 'nm-device-hsr.c', 'nm-device-infiniband.c', diff --git a/src/libnm-client-impl/nm-client.c b/src/libnm-client-impl/nm-client.c index b81ac6e506..f13835269e 100644 --- a/src/libnm-client-impl/nm-client.c +++ b/src/libnm-client-impl/nm-client.c @@ -29,6 +29,7 @@ #include "nm-device-dummy.h" #include "nm-device-ethernet.h" #include "nm-device-generic.h" +#include "nm-device-geneve.h" #include "nm-device-hsr.h" #include "nm-device-infiniband.h" #include "nm-device-ip-tunnel.h" diff --git a/src/libnm-client-impl/nm-device-geneve.c b/src/libnm-client-impl/nm-device-geneve.c new file mode 100644 index 0000000000..415e978d16 --- /dev/null +++ b/src/libnm-client-impl/nm-device-geneve.c @@ -0,0 +1,344 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +/* + * Copyright (C) 2026 Red Hat, Inc. + */ + +#include "libnm-client-impl/nm-default-libnm.h" + +#include "nm-device-geneve.h" + +#include "nm-setting-connection.h" +#include "nm-setting-geneve.h" +#include "nm-utils.h" +#include "nm-object-private.h" + +/*****************************************************************************/ + +NM_GOBJECT_PROPERTIES_DEFINE_BASE(PROP_ID, + PROP_REMOTE, + PROP_TOS, + PROP_TTL, + PROP_DST_PORT, + PROP_DF, ); + +typedef struct { + char *remote; + guint32 id; + gint32 ttl; + guint16 dst_port; + guint8 df; + guint8 tos; +} NMDeviceGenevePrivate; + +struct _NMDeviceGeneve { + NMDevice parent; + NMDeviceGenevePrivate _priv; +}; + +struct _NMDeviceGeneveClass { + NMDeviceClass parent; +}; + +G_DEFINE_TYPE(NMDeviceGeneve, nm_device_geneve, NM_TYPE_DEVICE) + +#define NM_DEVICE_GENEVE_GET_PRIVATE(self) \ + _NM_GET_PRIVATE(self, NMDeviceGeneve, NM_IS_DEVICE_GENEVE, NMObject, NMDevice) + +/*****************************************************************************/ + +/** + * nm_device_geneve_get_id: + * @device: a #NMDeviceGeneve + * + * Returns: the device's GENEVE ID. + * + * Since: 1.58 + **/ +guint +nm_device_geneve_get_id(NMDeviceGeneve *device) +{ + g_return_val_if_fail(NM_IS_DEVICE_GENEVE(device), 0); + + return NM_DEVICE_GENEVE_GET_PRIVATE(device)->id; +} + +/** + * nm_device_geneve_get_remote: + * @device: a #NMDeviceGeneve + * + * Returns: the IP address of the remote tunnel endpoint + * + * Since: 1.58 + **/ +const char * +nm_device_geneve_get_remote(NMDeviceGeneve *device) +{ + g_return_val_if_fail(NM_IS_DEVICE_GENEVE(device), NULL); + + return _nml_coerce_property_str_not_empty(NM_DEVICE_GENEVE_GET_PRIVATE(device)->remote); +} + +/** + * nm_device_geneve_get_dst_port: + * @device: a #NMDeviceGeneve + * + * Returns: the UDP destination port + * + * Since: 1.58 + **/ +guint +nm_device_geneve_get_dst_port(NMDeviceGeneve *device) +{ + g_return_val_if_fail(NM_IS_DEVICE_GENEVE(device), 0); + + return NM_DEVICE_GENEVE_GET_PRIVATE(device)->dst_port; +} + +/** + * nm_device_geneve_get_tos: + * @device: a #NMDeviceGeneve + * + * Returns: the TOS value to use in outgoing packets + * + * Since: 1.58 + **/ +guint +nm_device_geneve_get_tos(NMDeviceGeneve *device) +{ + g_return_val_if_fail(NM_IS_DEVICE_GENEVE(device), 0); + + return NM_DEVICE_GENEVE_GET_PRIVATE(device)->tos; +} + +/** + * nm_device_geneve_get_ttl: + * @device: a #NMDeviceGeneve + * + * Returns: the time-to-live value to use in outgoing packets + * + * Since: 1.58 + **/ +guint +nm_device_geneve_get_ttl(NMDeviceGeneve *device) +{ + g_return_val_if_fail(NM_IS_DEVICE_GENEVE(device), 0); + + return NM_DEVICE_GENEVE_GET_PRIVATE(device)->ttl; +} + +/** + * nm_device_geneve_get_df: + * @device: a #NMDeviceGeneve + * + * Returns: the Don't Fragment (DF) bit to set in outgoing packets + * + * Since: 1.58 + **/ +guint +nm_device_geneve_get_df(NMDeviceGeneve *device) +{ + g_return_val_if_fail(NM_IS_DEVICE_GENEVE(device), 0); + + return NM_DEVICE_GENEVE_GET_PRIVATE(device)->df; +} + +static gboolean +connection_compatible(NMDevice *device, NMConnection *connection, GError **error) +{ + NMSettingGeneve *s_geneve; + + if (!NM_DEVICE_CLASS(nm_device_geneve_parent_class) + ->connection_compatible(device, connection, error)) + return FALSE; + + if (!nm_connection_is_type(connection, NM_SETTING_GENEVE_SETTING_NAME)) { + g_set_error_literal(error, + NM_DEVICE_ERROR, + NM_DEVICE_ERROR_INCOMPATIBLE_CONNECTION, + _("The connection was not a GENEVE connection.")); + return FALSE; + } + + s_geneve = nm_connection_get_setting_geneve(connection); + if (nm_setting_geneve_get_id(s_geneve) != nm_device_geneve_get_id(NM_DEVICE_GENEVE(device))) { + g_set_error_literal( + error, + NM_DEVICE_ERROR, + NM_DEVICE_ERROR_INCOMPATIBLE_CONNECTION, + _("The GENEVE identifiers of the device and the connection didn't match.")); + return FALSE; + } + + return TRUE; +} + +static GType +get_setting_type(NMDevice *device) +{ + return NM_TYPE_SETTING_GENEVE; +} + +/*****************************************************************************/ + +static void +get_property(GObject *object, guint prop_id, GValue *value, GParamSpec *pspec) +{ + NMDeviceGeneve *device = NM_DEVICE_GENEVE(object); + + switch (prop_id) { + case PROP_ID: + g_value_set_uint(value, nm_device_geneve_get_id(device)); + break; + case PROP_REMOTE: + g_value_set_string(value, nm_device_geneve_get_remote(device)); + break; + case PROP_TOS: + g_value_set_uint(value, nm_device_geneve_get_tos(device)); + break; + case PROP_TTL: + g_value_set_int(value, nm_device_geneve_get_ttl(device)); + break; + case PROP_DST_PORT: + g_value_set_uint(value, nm_device_geneve_get_dst_port(device)); + break; + case PROP_DF: + g_value_set_uint(value, nm_device_geneve_get_df(device)); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec); + break; + } +} + +static void +nm_device_geneve_init(NMDeviceGeneve *device) +{} + +static void +finalize(GObject *object) +{ + NMDeviceGenevePrivate *priv = NM_DEVICE_GENEVE_GET_PRIVATE(object); + + g_free(priv->remote); + + G_OBJECT_CLASS(nm_device_geneve_parent_class)->finalize(object); +} + +const NMLDBusMetaIface _nml_dbus_meta_iface_nm_device_geneve = NML_DBUS_META_IFACE_INIT_PROP( + NM_DBUS_INTERFACE_DEVICE_GENEVE, + nm_device_geneve_get_type, + NML_DBUS_META_INTERFACE_PRIO_INSTANTIATE_30, + NML_DBUS_META_IFACE_DBUS_PROPERTIES( + NML_DBUS_META_PROPERTY_INIT_Y("Df", PROP_DF, NMDeviceGeneve, _priv.df), + NML_DBUS_META_PROPERTY_INIT_Q("DstPort", PROP_DST_PORT, NMDeviceGeneve, _priv.dst_port), + NML_DBUS_META_PROPERTY_INIT_U("Id", PROP_ID, NMDeviceGeneve, _priv.id), + NML_DBUS_META_PROPERTY_INIT_S("Remote", PROP_REMOTE, NMDeviceGeneve, _priv.remote), + NML_DBUS_META_PROPERTY_INIT_Y("Tos", PROP_TOS, NMDeviceGeneve, _priv.tos), + NML_DBUS_META_PROPERTY_INIT_I("Ttl", PROP_TTL, NMDeviceGeneve, _priv.ttl), ), ); + +static void +nm_device_geneve_class_init(NMDeviceGeneveClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS(klass); + NMObjectClass *nm_object_class = NM_OBJECT_CLASS(klass); + NMDeviceClass *device_class = NM_DEVICE_CLASS(klass); + + object_class->get_property = get_property; + object_class->finalize = finalize; + + _NM_OBJECT_CLASS_INIT_PRIV_PTR_DIRECT(nm_object_class, NMDeviceGeneve); + + device_class->connection_compatible = connection_compatible; + device_class->get_setting_type = get_setting_type; + + /** + * NMDeviceGeneve:id: + * + * The device's GENEVE ID. + * + * Since: 1.58 + **/ + obj_properties[PROP_ID] = g_param_spec_uint(NM_DEVICE_GENEVE_ID, + "", + "", + 0, + (1 << 24) - 1, + 0, + G_PARAM_READABLE | G_PARAM_STATIC_STRINGS); + + /** + * NMDeviceGeneve:remote: + * + * The IP address of the remote tunnel endpoint. + * + * Since: 1.58 + */ + obj_properties[PROP_REMOTE] = g_param_spec_string(NM_DEVICE_GENEVE_REMOTE, + "", + "", + NULL, + G_PARAM_READABLE | G_PARAM_STATIC_STRINGS); + + /** + * NMDeviceGeneve:tos: + * + * The TOS value to use in outgoing packets. + * + * Since: 1.58 + */ + obj_properties[PROP_TOS] = g_param_spec_uchar(NM_DEVICE_GENEVE_TOS, + "", + "", + 0, + 255, + 0, + G_PARAM_READABLE | G_PARAM_STATIC_STRINGS); + + /** + * NMDeviceGeneve:ttl: + * + * The time-to-live value to use in outgoing packets. + * + * Since: 1.58 + */ + obj_properties[PROP_TTL] = g_param_spec_int(NM_DEVICE_GENEVE_TTL, + "", + "", + -1, + 255, + 0, + G_PARAM_READABLE | G_PARAM_STATIC_STRINGS); + + /** + * NMDeviceGeneve:dst-port: + * + * The UDP destination port used to communicate with the remote GENEVE tunnel + * endpoint. + * + * Since: 1.58 + */ + obj_properties[PROP_DST_PORT] = g_param_spec_uint(NM_DEVICE_GENEVE_DST_PORT, + "", + "", + 0, + 65535, + 0, + G_PARAM_READABLE | G_PARAM_STATIC_STRINGS); + + /** + * NMDeviceGeneve:df: + * + * The Don't Fragment (DF) bit to set in outgoing packets. + * + * Since: 1.58 + */ + obj_properties[PROP_DF] = g_param_spec_uchar(NM_DEVICE_GENEVE_DF, + "", + "", + 0, + 2, + 0, + G_PARAM_READABLE | G_PARAM_STATIC_STRINGS); + + _nml_dbus_meta_class_init_with_properties(object_class, &_nml_dbus_meta_iface_nm_device_geneve); +} diff --git a/src/libnm-client-impl/nm-device.c b/src/libnm-client-impl/nm-device.c index 1712efa5bb..9203fd6f13 100644 --- a/src/libnm-client-impl/nm-device.c +++ b/src/libnm-client-impl/nm-device.c @@ -302,6 +302,7 @@ coerce_type(NMDeviceType type) case NM_DEVICE_TYPE_TUN: case NM_DEVICE_TYPE_VETH: case NM_DEVICE_TYPE_GENERIC: + case NM_DEVICE_TYPE_GENEVE: case NM_DEVICE_TYPE_UNUSED1: case NM_DEVICE_TYPE_UNUSED2: case NM_DEVICE_TYPE_UNKNOWN: @@ -1792,6 +1793,8 @@ get_type_name(NMDevice *device) return _("MACVLAN"); case NM_DEVICE_TYPE_VXLAN: return _("VXLAN"); + case NM_DEVICE_TYPE_GENEVE: + return _("GENEVE"); case NM_DEVICE_TYPE_IP_TUNNEL: return _("IPTunnel"); case NM_DEVICE_TYPE_TUN: diff --git a/src/libnm-client-impl/nm-libnm-utils.c b/src/libnm-client-impl/nm-libnm-utils.c index c2fa2addef..9f1b515c2e 100644 --- a/src/libnm-client-impl/nm-libnm-utils.c +++ b/src/libnm-client-impl/nm-libnm-utils.c @@ -789,6 +789,7 @@ const NMLDBusMetaIface *const _nml_dbus_meta_ifaces[] = { &_nml_dbus_meta_iface_nm_device_bridge, &_nml_dbus_meta_iface_nm_device_dummy, &_nml_dbus_meta_iface_nm_device_generic, + &_nml_dbus_meta_iface_nm_device_geneve, &_nml_dbus_meta_iface_nm_device_hsr, &_nml_dbus_meta_iface_nm_device_iptunnel, &_nml_dbus_meta_iface_nm_device_infiniband, diff --git a/src/libnm-client-impl/nm-libnm-utils.h b/src/libnm-client-impl/nm-libnm-utils.h index 7dcf8c18dc..27b77b6009 100644 --- a/src/libnm-client-impl/nm-libnm-utils.h +++ b/src/libnm-client-impl/nm-libnm-utils.h @@ -579,7 +579,7 @@ struct _NMLDBusMetaIface { NML_DBUS_META_IFACE_OBJ_PROPERTIES(), \ ##__VA_ARGS__) -extern const NMLDBusMetaIface *const _nml_dbus_meta_ifaces[47]; +extern const NMLDBusMetaIface *const _nml_dbus_meta_ifaces[48]; extern const NMLDBusMetaIface _nml_dbus_meta_iface_nm; extern const NMLDBusMetaIface _nml_dbus_meta_iface_nm_accesspoint; @@ -593,6 +593,7 @@ extern const NMLDBusMetaIface _nml_dbus_meta_iface_nm_device_bond; extern const NMLDBusMetaIface _nml_dbus_meta_iface_nm_device_bridge; extern const NMLDBusMetaIface _nml_dbus_meta_iface_nm_device_dummy; extern const NMLDBusMetaIface _nml_dbus_meta_iface_nm_device_generic; +extern const NMLDBusMetaIface _nml_dbus_meta_iface_nm_device_geneve; extern const NMLDBusMetaIface _nml_dbus_meta_iface_nm_device_hsr; extern const NMLDBusMetaIface _nml_dbus_meta_iface_nm_device_infiniband; extern const NMLDBusMetaIface _nml_dbus_meta_iface_nm_device_iptunnel; diff --git a/src/libnm-client-public/NetworkManager.h b/src/libnm-client-public/NetworkManager.h index 2d1c56521e..880bac6ed7 100644 --- a/src/libnm-client-public/NetworkManager.h +++ b/src/libnm-client-public/NetworkManager.h @@ -115,6 +115,7 @@ #include "nm-device-dummy.h" #include "nm-device-ethernet.h" #include "nm-device-generic.h" +#include "nm-device-geneve.h" #include "nm-device-hsr.h" #include "nm-device-infiniband.h" #include "nm-device-ip-tunnel.h" diff --git a/src/libnm-client-public/meson.build b/src/libnm-client-public/meson.build index b8ae9cce07..5aa6de2518 100644 --- a/src/libnm-client-public/meson.build +++ b/src/libnm-client-public/meson.build @@ -18,6 +18,7 @@ libnm_client_headers = files( 'nm-device-dummy.h', 'nm-device-ethernet.h', 'nm-device-generic.h', + 'nm-device-geneve.h', 'nm-device-hsr.h', 'nm-device-infiniband.h', 'nm-device-ip-tunnel.h', diff --git a/src/libnm-client-public/nm-autoptr.h b/src/libnm-client-public/nm-autoptr.h index f21f2970c8..12379fe1aa 100644 --- a/src/libnm-client-public/nm-autoptr.h +++ b/src/libnm-client-public/nm-autoptr.h @@ -41,6 +41,7 @@ G_DEFINE_AUTOPTR_CLEANUP_FUNC(NMDeviceBt, g_object_unref) G_DEFINE_AUTOPTR_CLEANUP_FUNC(NMDeviceDummy, g_object_unref) G_DEFINE_AUTOPTR_CLEANUP_FUNC(NMDeviceEthernet, g_object_unref) G_DEFINE_AUTOPTR_CLEANUP_FUNC(NMDeviceGeneric, g_object_unref) +G_DEFINE_AUTOPTR_CLEANUP_FUNC(NMDeviceGeneve, g_object_unref) G_DEFINE_AUTOPTR_CLEANUP_FUNC(NMDeviceHsr, g_object_unref) G_DEFINE_AUTOPTR_CLEANUP_FUNC(NMDeviceIPTunnel, g_object_unref) G_DEFINE_AUTOPTR_CLEANUP_FUNC(NMDeviceInfiniband, g_object_unref) diff --git a/src/libnm-client-public/nm-device-geneve.h b/src/libnm-client-public/nm-device-geneve.h new file mode 100644 index 0000000000..5f7c92b30b --- /dev/null +++ b/src/libnm-client-public/nm-device-geneve.h @@ -0,0 +1,60 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +/* + * Copyright (C) 2015 Red Hat, Inc. + */ + +#ifndef __NM_DEVICE_GENEVE_H__ +#define __NM_DEVICE_GENEVE_H__ + +#if !defined(__NETWORKMANAGER_H_INSIDE__) && !defined(NETWORKMANAGER_COMPILATION) +#error "Only can be included directly." +#endif + +#include "nm-device.h" + +G_BEGIN_DECLS + +#define NM_TYPE_DEVICE_GENEVE (nm_device_geneve_get_type()) +#define NM_DEVICE_GENEVE(obj) \ + (G_TYPE_CHECK_INSTANCE_CAST((obj), NM_TYPE_DEVICE_GENEVE, NMDeviceGeneve)) +#define NM_DEVICE_GENEVE_CLASS(klass) \ + (G_TYPE_CHECK_CLASS_CAST((klass), NM_TYPE_DEVICE_GENEVE, NMDeviceGeneveClass)) +#define NM_IS_DEVICE_GENEVE(obj) (G_TYPE_CHECK_INSTANCE_TYPE((obj), NM_TYPE_DEVICE_GENEVE)) +#define NM_IS_DEVICE_GENEVE_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE((klass), NM_TYPE_DEVICE_GENEVE)) +#define NM_DEVICE_GENEVE_GET_CLASS(obj) \ + (G_TYPE_INSTANCE_GET_CLASS((obj), NM_TYPE_DEVICE_GENEVE, NMDeviceGeneveClass)) + +#define NM_DEVICE_GENEVE_ID "id" +#define NM_DEVICE_GENEVE_REMOTE "remote" +#define NM_DEVICE_GENEVE_TOS "tos" +#define NM_DEVICE_GENEVE_TTL "ttl" +#define NM_DEVICE_GENEVE_DST_PORT "dst-port" +#define NM_DEVICE_GENEVE_DF "df" + +/** + * NMDeviceGeneve: + * + * Since: 1.58 + */ +typedef struct _NMDeviceGeneve NMDeviceGeneve; +typedef struct _NMDeviceGeneveClass NMDeviceGeneveClass; + +NM_AVAILABLE_IN_1_58 +GType nm_device_geneve_get_type(void); + +NM_AVAILABLE_IN_1_58 +guint nm_device_geneve_get_id(NMDeviceGeneve *device); +NM_AVAILABLE_IN_1_58 +const char *nm_device_geneve_get_remote(NMDeviceGeneve *device); +NM_AVAILABLE_IN_1_58 +guint nm_device_geneve_get_dst_port(NMDeviceGeneve *device); +NM_AVAILABLE_IN_1_58 +guint nm_device_geneve_get_tos(NMDeviceGeneve *device); +NM_AVAILABLE_IN_1_58 +guint nm_device_geneve_get_ttl(NMDeviceGeneve *device); +NM_AVAILABLE_IN_1_58 +guint nm_device_geneve_get_df(NMDeviceGeneve *device); + +G_END_DECLS + +#endif /* __NM_DEVICE_GENEVE_H__ */ diff --git a/src/libnm-core-impl/nm-connection.c b/src/libnm-core-impl/nm-connection.c index d7e19627df..9ce72b4145 100644 --- a/src/libnm-core-impl/nm-connection.c +++ b/src/libnm-core-impl/nm-connection.c @@ -3272,6 +3272,7 @@ nm_connection_is_virtual(NMConnection *connection) NM_SETTING_BOND_SETTING_NAME, NM_SETTING_BRIDGE_SETTING_NAME, NM_SETTING_DUMMY_SETTING_NAME, + NM_SETTING_GENEVE_SETTING_NAME, NM_SETTING_HSR_SETTING_NAME, NM_SETTING_IP_TUNNEL_SETTING_NAME, NM_SETTING_IPVLAN_SETTING_NAME, diff --git a/src/libnm-core-public/nm-dbus-interface.h b/src/libnm-core-public/nm-dbus-interface.h index 42bff04ae0..8d34066f0e 100644 --- a/src/libnm-core-public/nm-dbus-interface.h +++ b/src/libnm-core-public/nm-dbus-interface.h @@ -36,6 +36,7 @@ #define NM_DBUS_INTERFACE_DEVICE_BRIDGE NM_DBUS_INTERFACE_DEVICE ".Bridge" #define NM_DBUS_INTERFACE_DEVICE_DUMMY NM_DBUS_INTERFACE_DEVICE ".Dummy" #define NM_DBUS_INTERFACE_DEVICE_GENERIC NM_DBUS_INTERFACE_DEVICE ".Generic" +#define NM_DBUS_INTERFACE_DEVICE_GENEVE NM_DBUS_INTERFACE_DEVICE ".Geneve" #define NM_DBUS_INTERFACE_DEVICE_GRE NM_DBUS_INTERFACE_DEVICE ".Gre" #define NM_DBUS_INTERFACE_DEVICE_HSR NM_DBUS_INTERFACE_DEVICE ".Hsr" #define NM_DBUS_INTERFACE_DEVICE_INFINIBAND NM_DBUS_INTERFACE_DEVICE ".Infiniband" @@ -250,6 +251,7 @@ typedef enum { * @NM_DEVICE_TYPE_LOOPBACK: a loopback interface. Since: 1.42. * @NM_DEVICE_TYPE_HSR: A HSR/PRP device. Since: 1.46. * @NM_DEVICE_TYPE_IPVLAN: A IPVLAN device. Since: 1.52. + * @NM_DEVICE_TYPE_GENEVE: A GENEVE device. Since: 1.58. * * #NMDeviceType values indicate the type of hardware represented by a * device object. @@ -290,6 +292,7 @@ typedef enum { NM_DEVICE_TYPE_LOOPBACK = 32, NM_DEVICE_TYPE_HSR = 33, NM_DEVICE_TYPE_IPVLAN = 34, + NM_DEVICE_TYPE_GENEVE = 35, } NMDeviceType; /** diff --git a/vapi/NM-1.0.metadata b/vapi/NM-1.0.metadata index 6f1720ad7a..3084b7c289 100644 --- a/vapi/NM-1.0.metadata +++ b/vapi/NM-1.0.metadata @@ -115,6 +115,7 @@ DEVICE_BT_* parent="NM.DeviceBt" name="DEVICE DEVICE_DUMMY_* parent="NM.DeviceDummy" name="DEVICE_DUMMY_(.+)" DEVICE_ETHERNET_* parent="NM.DeviceEthernet" name="DEVICE_ETHERNET_(.+)" DEVICE_GENERIC_* parent="NM.DeviceGeneric" name="DEVICE_GENERIC_(.+)" +DEVICE_GENEVE_* parent="NM.DeviceGeneve" name="DEVICE_GENEVE_(.+)" DEVICE_HSR_* parent="NM.DeviceHsr" name="DEVICE_HSR_(.+)" DEVICE_INFINIBAND_* parent="NM.DeviceInfiniband" name="DEVICE_INFINIBAND_(.+)" DEVICE_IP_TUNNEL_* parent="NM.DeviceIPTunnel" name="DEVICE_IP_TUNNEL_(.+)" From 2e2b4946ea4827e1a05d715d80a22828ac37f4b0 Mon Sep 17 00:00:00 2001 From: Rahul Rajesh Date: Tue, 17 Feb 2026 15:16:48 -0500 Subject: [PATCH 56/56] NEWS: add support for GENEVE interface https://gitlab.freedesktop.org/NetworkManager/NetworkManager/-/merge_requests/2352 Resolves: https://issues.redhat.com/browse/RHEL-122042 --- NEWS | 1 + 1 file changed, 1 insertion(+) diff --git a/NEWS b/NEWS index 65260a14dc..ffd9dc43d9 100644 --- a/NEWS +++ b/NEWS @@ -47,6 +47,7 @@ USE AT YOUR OWN RISK. NOT RECOMMENDED FOR PRODUCTION USE! * WIFI connections using wpa-psk respect the setting connection.auth-retry and only prompt for new secrets during the last authentication attempt before failing. +* Add support for GENEVE interface. ============================================= NetworkManager-1.56