From f42e422b69c3b2f1dc573028657434c88643dc6e Mon Sep 17 00:00:00 2001 From: Beniamino Galvani Date: Thu, 12 Feb 2026 17:20:35 +0100 Subject: [PATCH 1/6] libnm-core: add function to detect directly-unreachable gateways nm_connection_get_unreachable_gateways() is a non-public function, available in the daemon and clients, which detects gateways in the static configuration that are not directly reachable. Unreachable gateways are often the consequence of user mistakes; we want to catch them early. In the following commits, warnings will be emitted when a connection is created/modified/activated and has unreachable gateways. --- .../nm-libnm-core-utils.c | 157 +++++++++ .../nm-libnm-core-utils.h | 4 + src/libnm-core-impl/tests/test-general.c | 323 ++++++++++++++++++ 3 files changed, 484 insertions(+) diff --git a/src/libnm-core-aux-intern/nm-libnm-core-utils.c b/src/libnm-core-aux-intern/nm-libnm-core-utils.c index a39f645468..b8daa9899b 100644 --- a/src/libnm-core-aux-intern/nm-libnm-core-utils.c +++ b/src/libnm-core-aux-intern/nm-libnm-core-utils.c @@ -1149,3 +1149,160 @@ nm_setting_ovs_other_config_check_val(const char *val, GError **error) return TRUE; } + +/*****************************************************************************/ + +typedef struct { + NMIPAddr dest; + guint prefix; +} DirectRoute; + +static void +_setting_ip_config_collect_unreachable_gateways(NMSettingIPConfig *s_ip, GHashTable **result) +{ + const int addr_family = nm_setting_ip_config_get_addr_family(s_ip); + guint num_routes; + guint num_addrs; + guint n_direct_routes = 0; + NMIPAddr gw_bin = NM_IP_ADDR_INIT; + guint i; + guint j; + const char *gateway; + gs_free DirectRoute *direct_routes = NULL; + + num_routes = nm_setting_ip_config_get_num_routes(s_ip); + num_addrs = nm_setting_ip_config_get_num_addresses(s_ip); + + direct_routes = g_new0(DirectRoute, num_routes + num_addrs); + + /* Collect direct routes (routes without a gateway) from the setting. */ + for (i = 0; i < num_routes; i++) { + NMIPRoute *route = nm_setting_ip_config_get_route(s_ip, i); + + if (nm_ip_route_get_next_hop(route)) + continue; + + nm_ip_route_get_dest_binary(route, &direct_routes[n_direct_routes].dest); + direct_routes[n_direct_routes].prefix = nm_ip_route_get_prefix(route); + n_direct_routes++; + } + + /* Add prefix routes (device routes) for each static address. */ + for (i = 0; i < num_addrs; i++) { + NMIPAddress *addr = nm_setting_ip_config_get_address(s_ip, i); + + nm_ip_address_get_address_binary(addr, &direct_routes[n_direct_routes].dest); + direct_routes[n_direct_routes].prefix = nm_ip_address_get_prefix(addr); + n_direct_routes++; + } + + /* Check the setting's default gateway. */ + gateway = nm_setting_ip_config_get_gateway(s_ip); + if (gateway && nm_inet_parse_bin(addr_family, gateway, NULL, &gw_bin)) { + gboolean reachable = FALSE; + + if (addr_family == AF_INET6 && IN6_IS_ADDR_LINKLOCAL(&gw_bin.addr6)) + reachable = TRUE; + + if (!reachable) { + for (j = 0; j < n_direct_routes; j++) { + if (nm_ip_addr_same_prefix(addr_family, + &gw_bin, + &direct_routes[j].dest, + direct_routes[j].prefix)) { + reachable = TRUE; + break; + } + } + } + + if (!reachable) { + if (!*result) + *result = g_hash_table_new(nm_str_hash, g_str_equal); + g_hash_table_add(*result, (gpointer) gateway); + } + } + + /* Check gateways of each route in the setting. */ + for (i = 0; i < num_routes; i++) { + NMIPRoute *route = nm_setting_ip_config_get_route(s_ip, i); + NMIPAddr next_hop = NM_IP_ADDR_INIT; + gboolean reachable = FALSE; + GVariant *attribute; + + if (!nm_ip_route_get_next_hop_binary(route, &next_hop)) + continue; + + if (addr_family == AF_INET6 && IN6_IS_ADDR_LINKLOCAL(&next_hop.addr6)) + continue; + + attribute = nm_ip_route_get_attribute(route, NM_IP_ROUTE_ATTRIBUTE_ONLINK); + if (attribute && g_variant_is_of_type(attribute, G_VARIANT_TYPE("b")) + && g_variant_get_boolean(attribute)) { + /* the gateway of a onlink route is reachable */ + continue; + } + + for (j = 0; j < n_direct_routes; j++) { + if (nm_ip_addr_same_prefix(addr_family, + &next_hop, + &direct_routes[j].dest, + direct_routes[j].prefix)) { + reachable = TRUE; + break; + } + } + + if (!reachable) { + if (!*result) + *result = g_hash_table_new(nm_str_hash, g_str_equal); + g_hash_table_add(*result, (gpointer) nm_ip_route_get_next_hop(route)); + } + } +} + +/** + * nm_connection_get_unreachable_gateways: + * @connection: the #NMConnection + * + * Checks whether there are gateways (either the default gateway or gateways + * of routes) that are not directly reachable in the IPv4 and IPv6 settings + * of the connection. A gateway is considered directly reachable if it falls + * within the subnet of a direct route (a route without a next hop) or of a + * prefix route from a static address. + * + * Returns: a %NULL-terminated array of gateway strings not directly reachable, + * or %NULL if all gateways are reachable. The individual strings are owned + * by the setting. Free the returned array with g_free(). + */ +const char ** +nm_connection_get_unreachable_gateways(NMConnection *connection) +{ + gs_unref_hashtable GHashTable *result = NULL; + NMSettingIPConfig *s_ip; + guint len; + const char **strv; + + if (!connection) + return NULL; + + s_ip = nm_connection_get_setting_ip4_config(connection); + if (s_ip + && nm_streq0(nm_setting_ip_config_get_method(s_ip), NM_SETTING_IP4_CONFIG_METHOD_MANUAL)) { + _setting_ip_config_collect_unreachable_gateways(s_ip, &result); + } + + s_ip = nm_connection_get_setting_ip6_config(connection); + if (s_ip + && nm_streq0(nm_setting_ip_config_get_method(s_ip), NM_SETTING_IP6_CONFIG_METHOD_MANUAL)) { + _setting_ip_config_collect_unreachable_gateways(s_ip, &result); + } + + if (result) { + strv = (const char **) g_hash_table_get_keys_as_array(result, &len); + nm_strv_sort(strv, len); + return strv; + } + + return NULL; +} 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 e14444bb9a..7931116b57 100644 --- a/src/libnm-core-aux-intern/nm-libnm-core-utils.h +++ b/src/libnm-core-aux-intern/nm-libnm-core-utils.h @@ -344,6 +344,10 @@ gboolean nm_setting_ovs_other_config_check_val(const char *val, GError **error); /*****************************************************************************/ +const char **nm_connection_get_unreachable_gateways(NMConnection *connection); + +/*****************************************************************************/ + /* Wi-Fi frequencies range for each band */ #define _NM_WIFI_FREQ_MIN_2GHZ 2412 #define _NM_WIFI_FREQ_MAX_2GHZ 2484 diff --git a/src/libnm-core-impl/tests/test-general.c b/src/libnm-core-impl/tests/test-general.c index 9143da0435..1e2904a152 100644 --- a/src/libnm-core-impl/tests/test-general.c +++ b/src/libnm-core-impl/tests/test-general.c @@ -11613,6 +11613,328 @@ test_dhcp_iaid_hexstr(void) /*****************************************************************************/ +static void +test_unreachable_gateways(void) +{ + NMConnection *conn; + NMSettingIPConfig *s_ip4; + NMSettingIPConfig *s_ip6; + gs_free const char **result = NULL; + + /* IPv4 gateway reachable via address prefix route */ + { + conn = + nmtst_create_minimal_connection("test-ugw", NULL, NM_SETTING_WIRED_SETTING_NAME, NULL); + s_ip4 = (NMSettingIPConfig *) nm_setting_ip4_config_new(); + g_object_set(s_ip4, + NM_SETTING_IP_CONFIG_METHOD, + NM_SETTING_IP4_CONFIG_METHOD_MANUAL, + NM_SETTING_IP_CONFIG_GATEWAY, + "192.168.1.1", + NULL); + nmtst_setting_ip_config_add_address(s_ip4, "192.168.1.5", 24); + nm_connection_add_setting(conn, NM_SETTING(s_ip4)); + + result = nm_connection_get_unreachable_gateways(conn); + g_assert(!result); + g_object_unref(conn); + } + + /* IPv4 gateway NOT reachable */ + { + conn = + nmtst_create_minimal_connection("test-ugw", NULL, NM_SETTING_WIRED_SETTING_NAME, NULL); + s_ip4 = (NMSettingIPConfig *) nm_setting_ip4_config_new(); + g_object_set(s_ip4, + NM_SETTING_IP_CONFIG_METHOD, + NM_SETTING_IP4_CONFIG_METHOD_MANUAL, + NM_SETTING_IP_CONFIG_GATEWAY, + "10.0.0.1", + NULL); + nmtst_setting_ip_config_add_address(s_ip4, "192.168.1.5", 24); + nmtst_setting_ip_config_add_address(s_ip4, "172.16.1.1", 16); + nm_connection_add_setting(conn, NM_SETTING(s_ip4)); + + result = nm_connection_get_unreachable_gateways(conn); + g_assert(result); + g_assert_cmpint(g_strv_length((char **) result), ==, 1); + g_assert_cmpstr(result[0], ==, "10.0.0.1"); + nm_clear_g_free(&result); + g_object_unref(conn); + } + + /* IPv4 gateway NOT reachable. It's ignored because of method "auto" */ + { + conn = + nmtst_create_minimal_connection("test-ugw", NULL, NM_SETTING_WIRED_SETTING_NAME, NULL); + s_ip4 = (NMSettingIPConfig *) nm_setting_ip4_config_new(); + g_object_set(s_ip4, + NM_SETTING_IP_CONFIG_METHOD, + NM_SETTING_IP4_CONFIG_METHOD_AUTO, + NM_SETTING_IP_CONFIG_GATEWAY, + "10.0.0.1", + NULL); + nmtst_setting_ip_config_add_address(s_ip4, "192.168.1.5", 24); + nmtst_setting_ip_config_add_address(s_ip4, "172.16.1.1", 16); + nm_connection_add_setting(conn, NM_SETTING(s_ip4)); + + result = nm_connection_get_unreachable_gateways(conn); + g_assert(!result); + g_object_unref(conn); + } + + /* Route gateway reachable via address prefix route */ + { + conn = + nmtst_create_minimal_connection("test-ugw", NULL, NM_SETTING_WIRED_SETTING_NAME, NULL); + s_ip4 = (NMSettingIPConfig *) nm_setting_ip4_config_new(); + g_object_set(s_ip4, NM_SETTING_IP_CONFIG_METHOD, NM_SETTING_IP4_CONFIG_METHOD_MANUAL, NULL); + nmtst_setting_ip_config_add_address(s_ip4, "192.168.1.5", 24); + nmtst_setting_ip_config_add_route(s_ip4, "10.0.0.0", 8, "192.168.1.254", 100); + nm_connection_add_setting(conn, NM_SETTING(s_ip4)); + + result = nm_connection_get_unreachable_gateways(conn); + g_assert(!result); + g_object_unref(conn); + } + + /* Route gateway NOT reachable, check sorting */ + { + conn = + nmtst_create_minimal_connection("test-ugw", NULL, NM_SETTING_WIRED_SETTING_NAME, NULL); + s_ip4 = (NMSettingIPConfig *) nm_setting_ip4_config_new(); + g_object_set(s_ip4, NM_SETTING_IP_CONFIG_METHOD, NM_SETTING_IP4_CONFIG_METHOD_MANUAL, NULL); + nmtst_setting_ip_config_add_address(s_ip4, "192.168.1.5", 24); + nmtst_setting_ip_config_add_route(s_ip4, "10.0.0.0", 16, "172.16.0.2", 100); + nmtst_setting_ip_config_add_route(s_ip4, "10.1.0.0", 16, "172.16.0.4", 100); + nmtst_setting_ip_config_add_route(s_ip4, "10.2.0.0", 16, "172.16.0.3", 100); + nmtst_setting_ip_config_add_route(s_ip4, "10.3.0.0", 16, "172.16.0.1", 100); + nm_connection_add_setting(conn, NM_SETTING(s_ip4)); + + result = nm_connection_get_unreachable_gateways(conn); + g_assert(result); + g_assert_cmpint(g_strv_length((char **) result), ==, 4); + g_assert_cmpstr(result[0], ==, "172.16.0.1"); + g_assert_cmpstr(result[1], ==, "172.16.0.2"); + g_assert_cmpstr(result[2], ==, "172.16.0.3"); + g_assert_cmpstr(result[3], ==, "172.16.0.4"); + nm_clear_g_free(&result); + g_object_unref(conn); + } + + /* Route gateway reachable via a direct route in the setting */ + { + conn = + nmtst_create_minimal_connection("test-ugw", NULL, NM_SETTING_WIRED_SETTING_NAME, NULL); + s_ip4 = (NMSettingIPConfig *) nm_setting_ip4_config_new(); + g_object_set(s_ip4, NM_SETTING_IP_CONFIG_METHOD, NM_SETTING_IP4_CONFIG_METHOD_MANUAL, NULL); + nmtst_setting_ip_config_add_address(s_ip4, "192.168.1.5", 24); + nmtst_setting_ip_config_add_route(s_ip4, "172.16.0.0", 16, NULL, 100); + nmtst_setting_ip_config_add_route(s_ip4, "10.0.0.0", 8, "172.16.0.1", 100); + nm_connection_add_setting(conn, NM_SETTING(s_ip4)); + + result = nm_connection_get_unreachable_gateways(conn); + g_assert(!result); + g_object_unref(conn); + } + + /* No gateways */ + { + conn = + nmtst_create_minimal_connection("test-ugw", NULL, NM_SETTING_WIRED_SETTING_NAME, NULL); + s_ip4 = (NMSettingIPConfig *) nm_setting_ip4_config_new(); + g_object_set(s_ip4, NM_SETTING_IP_CONFIG_METHOD, NM_SETTING_IP4_CONFIG_METHOD_MANUAL, NULL); + nmtst_setting_ip_config_add_address(s_ip4, "192.168.1.5", 24); + nm_connection_add_setting(conn, NM_SETTING(s_ip4)); + + result = nm_connection_get_unreachable_gateways(conn); + g_assert(!result); + g_object_unref(conn); + } + + /* Both default gateway and route gateway unreachable */ + { + conn = + nmtst_create_minimal_connection("test-ugw", NULL, NM_SETTING_WIRED_SETTING_NAME, NULL); + s_ip4 = (NMSettingIPConfig *) nm_setting_ip4_config_new(); + g_object_set(s_ip4, + NM_SETTING_IP_CONFIG_METHOD, + NM_SETTING_IP4_CONFIG_METHOD_MANUAL, + NM_SETTING_IP_CONFIG_GATEWAY, + "10.0.0.1", + NULL); + nmtst_setting_ip_config_add_address(s_ip4, "192.168.1.5", 24); + nmtst_setting_ip_config_add_route(s_ip4, "10.0.0.0", 8, "172.16.0.1", 100); + nm_connection_add_setting(conn, NM_SETTING(s_ip4)); + + result = nm_connection_get_unreachable_gateways(conn); + g_assert(result); + g_assert_cmpint(g_strv_length((char **) result), ==, 2); + g_assert_cmpstr(result[0], ==, "10.0.0.1"); + g_assert_cmpstr(result[1], ==, "172.16.0.1"); + nm_clear_g_free(&result); + g_object_unref(conn); + } + + /* Test deduplication */ + { + conn = + nmtst_create_minimal_connection("test-ugw", NULL, NM_SETTING_WIRED_SETTING_NAME, NULL); + s_ip4 = (NMSettingIPConfig *) nm_setting_ip4_config_new(); + g_object_set(s_ip4, + NM_SETTING_IP_CONFIG_METHOD, + NM_SETTING_IP4_CONFIG_METHOD_MANUAL, + NM_SETTING_IP_CONFIG_GATEWAY, + "192.168.1.1", + NULL); + nmtst_setting_ip_config_add_address(s_ip4, "192.168.1.5", 24); + nmtst_setting_ip_config_add_route(s_ip4, "10.0.0.0", 16, "172.16.0.1", 100); + nmtst_setting_ip_config_add_route(s_ip4, "10.1.0.0", 16, "172.16.0.1", 100); + nmtst_setting_ip_config_add_route(s_ip4, "10.2.0.0", 16, "172.16.0.1", 100); + nm_connection_add_setting(conn, NM_SETTING(s_ip4)); + + result = nm_connection_get_unreachable_gateways(conn); + g_assert(result); + g_assert_cmpint(g_strv_length((char **) result), ==, 1); + g_assert_cmpstr(result[0], ==, "172.16.0.1"); + nm_clear_g_free(&result); + g_object_unref(conn); + } + + /* IPv6 gateway reachable via address prefix route */ + { + conn = + nmtst_create_minimal_connection("test-ugw", NULL, NM_SETTING_WIRED_SETTING_NAME, NULL); + s_ip6 = (NMSettingIPConfig *) nm_setting_ip6_config_new(); + g_object_set(s_ip6, + NM_SETTING_IP_CONFIG_METHOD, + NM_SETTING_IP6_CONFIG_METHOD_MANUAL, + NM_SETTING_IP_CONFIG_GATEWAY, + "fd01::1", + NULL); + nmtst_setting_ip_config_add_address(s_ip6, "fd01::10", 64); + nm_connection_add_setting(conn, NM_SETTING(s_ip6)); + + result = nm_connection_get_unreachable_gateways(conn); + g_assert(!result); + g_object_unref(conn); + } + + /* IPv6 gateway NOT reachable */ + { + conn = + nmtst_create_minimal_connection("test-ugw", NULL, NM_SETTING_WIRED_SETTING_NAME, NULL); + s_ip6 = (NMSettingIPConfig *) nm_setting_ip6_config_new(); + g_object_set(s_ip6, + NM_SETTING_IP_CONFIG_METHOD, + NM_SETTING_IP6_CONFIG_METHOD_MANUAL, + NM_SETTING_IP_CONFIG_GATEWAY, + "fd02::1", + NULL); + nmtst_setting_ip_config_add_address(s_ip6, "fd01::10", 64); + nm_connection_add_setting(conn, NM_SETTING(s_ip6)); + + result = nm_connection_get_unreachable_gateways(conn); + g_assert(result); + g_assert_cmpint(g_strv_length((char **) result), ==, 1); + g_assert_cmpstr(result[0], ==, "fd02::1"); + nm_clear_g_free(&result); + g_object_unref(conn); + } + + /* Multiple addresses, gateway reachable via second address */ + { + conn = + nmtst_create_minimal_connection("test-ugw", NULL, NM_SETTING_WIRED_SETTING_NAME, NULL); + s_ip4 = (NMSettingIPConfig *) nm_setting_ip4_config_new(); + g_object_set(s_ip4, + NM_SETTING_IP_CONFIG_METHOD, + NM_SETTING_IP4_CONFIG_METHOD_MANUAL, + NM_SETTING_IP_CONFIG_GATEWAY, + "10.0.0.1", + NULL); + nmtst_setting_ip_config_add_address(s_ip4, "192.168.1.5", 24); + nmtst_setting_ip_config_add_address(s_ip4, "10.0.0.5", 24); + nm_connection_add_setting(conn, NM_SETTING(s_ip4)); + + result = nm_connection_get_unreachable_gateways(conn); + g_assert(!result); + g_object_unref(conn); + } + + /* Unreachable gateways in both IPv4 and IPv6 */ + { + conn = + nmtst_create_minimal_connection("test-ugw", NULL, NM_SETTING_WIRED_SETTING_NAME, NULL); + s_ip4 = (NMSettingIPConfig *) nm_setting_ip4_config_new(); + g_object_set(s_ip4, + NM_SETTING_IP_CONFIG_METHOD, + NM_SETTING_IP4_CONFIG_METHOD_MANUAL, + NM_SETTING_IP_CONFIG_GATEWAY, + "10.0.0.1", + NULL); + nmtst_setting_ip_config_add_address(s_ip4, "192.168.1.5", 24); + nm_connection_add_setting(conn, NM_SETTING(s_ip4)); + + s_ip6 = (NMSettingIPConfig *) nm_setting_ip6_config_new(); + g_object_set(s_ip6, + NM_SETTING_IP_CONFIG_METHOD, + NM_SETTING_IP6_CONFIG_METHOD_MANUAL, + NM_SETTING_IP_CONFIG_GATEWAY, + "fd02::1", + NULL); + nmtst_setting_ip_config_add_address(s_ip6, "fd01::10", 64); + nm_connection_add_setting(conn, NM_SETTING(s_ip6)); + + result = nm_connection_get_unreachable_gateways(conn); + g_assert(result); + g_assert_cmpint(g_strv_length((char **) result), ==, 2); + g_assert_cmpstr(result[0], ==, "10.0.0.1"); + g_assert_cmpstr(result[1], ==, "fd02::1"); + nm_clear_g_free(&result); + g_object_unref(conn); + } + + /* Onlink and IPv6-link-local routes */ + { + NMIPRoute *route; + + conn = + nmtst_create_minimal_connection("test-ugw", NULL, NM_SETTING_WIRED_SETTING_NAME, NULL); + + s_ip4 = (NMSettingIPConfig *) nm_setting_ip4_config_new(); + g_object_set(s_ip4, NM_SETTING_IP_CONFIG_METHOD, NM_SETTING_IP4_CONFIG_METHOD_MANUAL, NULL); + nmtst_setting_ip_config_add_address(s_ip4, "192.168.1.5", 24); + + route = nm_ip_route_new(AF_INET, "10.0.0.1", 8, "192.168.20.1", 100, NULL); + g_assert(route); + nm_ip_route_set_attribute(route, NM_IP_ROUTE_ATTRIBUTE_ONLINK, g_variant_new_boolean(TRUE)); + g_assert(nm_setting_ip_config_add_route(s_ip4, route)); + nm_ip_route_unref(route); + + nm_connection_add_setting(conn, NM_SETTING(s_ip4)); + + s_ip6 = (NMSettingIPConfig *) nm_setting_ip6_config_new(); + g_object_set(s_ip6, NM_SETTING_IP_CONFIG_METHOD, NM_SETTING_IP6_CONFIG_METHOD_MANUAL, NULL); + nmtst_setting_ip_config_add_address(s_ip6, "fd01::10", 64); + nm_connection_add_setting(conn, NM_SETTING(s_ip6)); + + route = nm_ip_route_new(AF_INET6, "fd02::", 64, "fd03::1111", 100, NULL); + g_assert(route); + nm_ip_route_set_attribute(route, NM_IP_ROUTE_ATTRIBUTE_ONLINK, g_variant_new_boolean(TRUE)); + g_assert(nm_setting_ip_config_add_route(s_ip6, route)); + nm_ip_route_unref(route); + + nmtst_setting_ip_config_add_route(s_ip6, "fd04::", 64, "fe80::1111", 100); + + result = nm_connection_get_unreachable_gateways(conn); + g_assert(!result); + g_object_unref(conn); + } +} + +/*****************************************************************************/ + NMTST_DEFINE(); int @@ -11963,6 +12285,7 @@ main(int argc, char **argv) g_test_add_func("/core/general/test_dns_uri_get_legacy", test_dns_uri_parse_plain); g_test_add_func("/core/general/test_dns_uri_normalize", test_dns_uri_normalize); g_test_add_func("/core/general/test_dhcp_iaid_hexstr", test_dhcp_iaid_hexstr); + g_test_add_func("/core/general/test_unreachable_gateways", test_unreachable_gateways); return g_test_run(); } From 2b4b8d7e7eda826230141a60f72a97e7cdb58543 Mon Sep 17 00:00:00 2001 From: Beniamino Galvani Date: Thu, 12 Feb 2026 17:21:55 +0100 Subject: [PATCH 2/6] libnm-core: add function to get a warning message for unreachable gateways We are going to print the same warning message in different places (the daemon, nmcli, nmtui). Add a function to return the message. Note that the message needs to be translated in clients but not in the daemon logs. --- .../nm-libnm-core-utils.c | 36 +++++++++++++++++++ .../nm-libnm-core-utils.h | 2 ++ 2 files changed, 38 insertions(+) diff --git a/src/libnm-core-aux-intern/nm-libnm-core-utils.c b/src/libnm-core-aux-intern/nm-libnm-core-utils.c index b8daa9899b..9a0b5e7c15 100644 --- a/src/libnm-core-aux-intern/nm-libnm-core-utils.c +++ b/src/libnm-core-aux-intern/nm-libnm-core-utils.c @@ -1306,3 +1306,39 @@ nm_connection_get_unreachable_gateways(NMConnection *connection) return NULL; } + +/** + * nm_connection_get_unreachable_gateways_warning: + * @connection: the #NMConnection + * @translate: whether to translate the message (use %TRUE for user-facing + * tools like nmcli, %FALSE for daemon logs) + * + * Checks whether there are unreachable gateways in the connection and returns + * a formatted warning message if so. + * + * Returns: a warning message string, or %NULL if all gateways are reachable. + * Free with g_free(). + */ +char * +nm_connection_get_unreachable_gateways_warning(NMConnection *connection, gboolean translate) +{ + gs_free const char **gateways = NULL; + gs_free char *gw_list = NULL; + const char *msg = + N_("the following gateways are not directly reachable from any configured address or " + "route: %s. NetworkManager currently adds on-link routes for them automatically, " + "but this will change in the future. Consider adding addresses or routes whose " + "subnets cover these gateways"); + + gateways = nm_connection_get_unreachable_gateways(connection); + if (!gateways) + return NULL; + + gw_list = g_strjoinv(", ", (char **) gateways); + + NM_PRAGMA_WARNING_DISABLE("-Wformat-nonliteral") + if (translate) + return g_strdup_printf(_(msg), gw_list); + return g_strdup_printf(msg, gw_list); + NM_PRAGMA_WARNING_REENABLE +} 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 7931116b57..ad0bad442d 100644 --- a/src/libnm-core-aux-intern/nm-libnm-core-utils.h +++ b/src/libnm-core-aux-intern/nm-libnm-core-utils.h @@ -346,6 +346,8 @@ gboolean nm_setting_ovs_other_config_check_val(const char *val, GError **error); const char **nm_connection_get_unreachable_gateways(NMConnection *connection); +char *nm_connection_get_unreachable_gateways_warning(NMConnection *connection, gboolean translate); + /*****************************************************************************/ /* Wi-Fi frequencies range for each band */ From ec5f98e7a84c2e23d158198261082b754bc1d0cd Mon Sep 17 00:00:00 2001 From: Beniamino Galvani Date: Thu, 12 Feb 2026 17:22:42 +0100 Subject: [PATCH 3/6] nmcli: emit warning for unreachable gateways --- src/nmcli/connections.c | 5 +++ .../test_005.expected | 15 ++++++++ src/tests/client/test-client.py | 35 +++++++++++++++++++ 3 files changed, 55 insertions(+) create mode 100644 src/tests/client/test-client.check-on-disk/test_005.expected diff --git a/src/nmcli/connections.c b/src/nmcli/connections.c index 11b7ec3c9b..6f0356c56f 100644 --- a/src/nmcli/connections.c +++ b/src/nmcli/connections.c @@ -5651,6 +5651,7 @@ connection_warnings(NmCli *nmc, NMConnection *connection) guint i, found; const char *id; const char *deprecated; + gs_free char *gw_warning = NULL; deprecated = nmc_connection_check_deprecated(NM_CONNECTION(connection)); if (deprecated) @@ -5679,6 +5680,10 @@ connection_warnings(NmCli *nmc, NMConnection *connection) nm_connection_get_uuid(NM_CONNECTION(connection)), found); } + + gw_warning = nm_connection_get_unreachable_gateways_warning(connection, TRUE); + if (gw_warning) + nmc_printerr("Warning: %s.\n", gw_warning); } static void diff --git a/src/tests/client/test-client.check-on-disk/test_005.expected b/src/tests/client/test-client.check-on-disk/test_005.expected new file mode 100644 index 0000000000..04f2f8adde --- /dev/null +++ b/src/tests/client/test-client.check-on-disk/test_005.expected @@ -0,0 +1,15 @@ +size: 683 +location: src/tests/client/test-client.py:test_005()/1 +cmd: $NMCLI c add type ethernet ifname eth0 con-name con-xx1 ipv4.method manual ipv4.addresses 192.168.1.1/24 ipv4.gateway 192.168.2.1 ipv4.routes '192.168.4.4 192.168.4.1' +lang: C +returncode: 0 +stdout: 80 bytes +>>> +Connection 'con-xx1' (UUID-con-xx1-REPLACED-REPLACED-REPLA) successfully added. + +<<< +stderr: 300 bytes +>>> +Warning: the following gateways are not directly reachable from any configured address or route: 192.168.2.1, 192.168.4.1. NetworkManager currently adds on-link routes for them automatically, but this will change in the future. Consider adding addresses or routes whose subnets cover these gateways. + +<<< diff --git a/src/tests/client/test-client.py b/src/tests/client/test-client.py index 37149d0530..3d618e35d5 100755 --- a/src/tests/client/test-client.py +++ b/src/tests/client/test-client.py @@ -2222,6 +2222,41 @@ class TestNmcli(unittest.TestCase): replace_stdout=replace_uuids, ) + @nm_test + def test_005(self): + self.init_001() + + replace_uuids = [] + + replace_uuids.append( + self.ctx.srv.ReplaceTextConUuid( + "con-xx1", "UUID-con-xx1-REPLACED-REPLACED-REPLA" + ) + ) + + # Check the warning about unreachable gateways + self.call_nmcli( + [ + "c", + "add", + "type", + "ethernet", + "ifname", + "eth0", + "con-name", + "con-xx1", + "ipv4.method", + "manual", + "ipv4.addresses", + "192.168.1.1/24", + "ipv4.gateway", + "192.168.2.1", + "ipv4.routes", + "192.168.4.4 192.168.4.1", + ], + replace_stdout=replace_uuids, + ) + @nm_test_no_dbus def test_offline(self): # Make sure we're not using D-Bus From 907508f4bf0f1606430d13647a6fae99d3336421 Mon Sep 17 00:00:00 2001 From: Beniamino Galvani Date: Thu, 12 Feb 2026 17:23:37 +0100 Subject: [PATCH 4/6] nmtui: emit warning for unreachable gateways --- src/nmtui/nmt-editor.c | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/nmtui/nmt-editor.c b/src/nmtui/nmt-editor.c index f1dd43860d..723d4edce9 100644 --- a/src/nmtui/nmt-editor.c +++ b/src/nmtui/nmt-editor.c @@ -14,6 +14,7 @@ #include "nmt-editor.h" +#include "libnm-core-aux-intern/nm-libnm-core-utils.h" #include "nm-utils.h" #include "nmtui.h" @@ -153,10 +154,16 @@ save_connection_and_exit(NmtNewtButton *button, gpointer user_data) NmtEditor *editor = user_data; NmtEditorPrivate *priv = NMT_EDITOR_GET_PRIVATE(editor); NmtSyncOp op; - GError *error = NULL; + GError *error = NULL; + gs_free char *gw_warning = NULL; nm_connection_replace_settings_from_connection(priv->orig_connection, priv->edit_connection); + gw_warning = nm_connection_get_unreachable_gateways_warning(priv->orig_connection, TRUE); + if (gw_warning) { + nmt_newt_message_dialog(_("Warning: %s"), gw_warning); + } + nmt_sync_op_init(&op); if (NM_IS_REMOTE_CONNECTION(priv->orig_connection)) { nm_remote_connection_commit_changes_async(NM_REMOTE_CONNECTION(priv->orig_connection), From 8b9a702e1d2cae2577ad3bd8aa290a480780d7de Mon Sep 17 00:00:00 2001 From: Beniamino Galvani Date: Thu, 12 Feb 2026 17:23:53 +0100 Subject: [PATCH 5/6] core: emit warning for unreachable gateways --- src/core/devices/nm-device.c | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/core/devices/nm-device.c b/src/core/devices/nm-device.c index 81a403f201..919ec82e12 100644 --- a/src/core/devices/nm-device.c +++ b/src/core/devices/nm-device.c @@ -3684,6 +3684,7 @@ nm_device_create_l3_config_data_from_connection(NMDevice *self, NMConnection *co { NML3ConfigData *l3cd; int ifindex; + gs_free char *gw_warning = NULL; nm_assert(NM_IS_DEVICE(self)); nm_assert(!connection || NM_IS_CONNECTION(connection)); @@ -3704,6 +3705,10 @@ nm_device_create_l3_config_data_from_connection(NMDevice *self, NMConnection *co nm_l3_config_data_set_ip6_privacy(l3cd, _prop_get_ipv6_ip6_privacy(self, connection)); nm_l3_config_data_set_mptcp_flags(l3cd, _prop_get_connection_mptcp_flags(self, connection)); + gw_warning = nm_connection_get_unreachable_gateways_warning(connection, FALSE); + if (gw_warning) + _LOGW(LOGD_IP, "%s", gw_warning); + return l3cd; } From 589286df7865bad48227820e9b3807ea748cdf9e Mon Sep 17 00:00:00 2001 From: Beniamino Galvani Date: Thu, 12 Feb 2026 17:34:38 +0100 Subject: [PATCH 6/6] NEWS: mention the warnings for unreachable gateways --- NEWS | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/NEWS b/NEWS index 3855bda0f2..cba09f8722 100644 --- a/NEWS +++ b/NEWS @@ -12,6 +12,14 @@ USE AT YOUR OWN RISK. NOT RECOMMENDED FOR PRODUCTION USE! 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. +* Connection profiles with manual IP addressing and with gateways that are not + directly reachable will generate a warning on activation and when they are + added/modified via nmcli and nmtui. NetworkManager currently adds on-link + routes for them automatically, but this will change in the future. To fix the + warning, users should add addresses or routes whose subnets cover these + gateways. A gateway (either the default gateway or the next-hop of a route) is + considered directly reachable if it falls within the subnet of a direct route + (a route without a next hop) or of a prefix route from a static address. * Restrict the connectivity check to use the DNS servers defined on the same link. If the link has no DNS servers, the connectivity check will use any servers available in the system.