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.
This commit is contained in:
Beniamino Galvani 2026-02-12 17:20:35 +01:00
parent 128b49fe21
commit 73d0d2cbb5
3 changed files with 484 additions and 0 deletions

View file

@ -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;
}

View file

@ -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

View file

@ -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();
}