From 73d0d2cbb5689214a48d11857fa18c8f7f32389e Mon Sep 17 00:00:00 2001 From: Beniamino Galvani Date: Thu, 12 Feb 2026 17:20:35 +0100 Subject: [PATCH] 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. --- .../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(); }