core: support automatically adding DNS routes

When the "ipvX.routed-dns" property is set to true, add a route for
each DNS server via the current interface. The feature works in the
following way.

A new routing rule is created ("priority $PRIO not fwmark $MARK lookup
$TABLE") where $PRIO, $MARK and $TABLE are fixed values and are the
same for all interfaces. This rule is evaluated before standard rules
and tries to look up routes in table $TABLE, where NM adds the routes
to DNS servers.

To determine the next-hop to the name server, NM issues a RTM_GETROUTE
netlink request to kernel, specifying to return the route via the
current interface. In order to avoid results from $TABLE, NM also sets
the fwmark as $MARK in the request.
This commit is contained in:
Beniamino Galvani
2024-10-03 15:22:51 +02:00
parent 5122ba48ef
commit 5449b18a94
5 changed files with 187 additions and 3 deletions

View File

@@ -937,6 +937,9 @@ ipv6.ip6-privacy=0
<term><varname>ip-tunnel.mtu</varname></term> <term><varname>ip-tunnel.mtu</varname></term>
<listitem><para>If configured explicitly to 0, the MTU is not reconfigured during device activation unless it is required due to IPv6 constraints. If left unspecified, a DHCP/IPv6 SLAAC provided value is used or a default of 1500.</para></listitem> <listitem><para>If configured explicitly to 0, the MTU is not reconfigured during device activation unless it is required due to IPv6 constraints. If left unspecified, a DHCP/IPv6 SLAAC provided value is used or a default of 1500.</para></listitem>
</varlistentry> </varlistentry>
<varlistentry>
<term><varname>ipv4.routed-dns</varname></term>
</varlistentry>
<varlistentry> <varlistentry>
<term><varname>ipv4.dad-timeout</varname></term> <term><varname>ipv4.dad-timeout</varname></term>
</varlistentry> </varlistentry>
@@ -993,6 +996,9 @@ ipv6.ip6-privacy=0
removes extraneous routes from the tables. removes extraneous routes from the tables.
</para></listitem> </para></listitem>
</varlistentry> </varlistentry>
<varlistentry>
<term><varname>ipv6.routed-dns</varname></term>
</varlistentry>
<varlistentry> <varlistentry>
<term><varname>ipv6.addr-gen-mode</varname></term> <term><varname>ipv6.addr-gen-mode</varname></term>
<listitem><para>If the per-profile setting is either "default" or "default-or-eui64", the <listitem><para>If the per-profile setting is either "default" or "default-or-eui64", the

View File

@@ -1361,6 +1361,35 @@ _prop_get_ipv6_ra_timeout(NMDevice *self)
0); 0);
} }
static NMSettingIPConfigRoutedDns
_prop_get_ipvx_routed_dns(NMDevice *self, int addr_family)
{
NMSettingIPConfig *s_ip;
NMSettingIPConfigRoutedDns val;
int IS_IPv4;
g_return_val_if_fail(NM_IS_DEVICE(self), NM_SETTING_IP_CONFIG_ROUTED_DNS_NO);
IS_IPv4 = NM_IS_IPv4(addr_family);
s_ip = nm_device_get_applied_setting(self,
IS_IPv4 ? NM_TYPE_SETTING_IP4_CONFIG
: NM_TYPE_SETTING_IP6_CONFIG);
if (!s_ip)
return NM_SETTING_IP_CONFIG_ROUTED_DNS_NO;
val = nm_setting_ip_config_get_routed_dns(s_ip);
if (val != NM_SETTING_IP_CONFIG_ROUTED_DNS_DEFAULT)
return val;
return nm_config_data_get_connection_default_int64(NM_CONFIG_GET_DATA,
IS_IPv4 ? NM_CON_DEFAULT("ipv4.routed-dns")
: NM_CON_DEFAULT("ipv6.routed-dns"),
self,
NM_SETTING_IP_CONFIG_ROUTED_DNS_NO,
NM_SETTING_IP_CONFIG_ROUTED_DNS_YES,
NM_SETTING_IP_CONFIG_ROUTED_DNS_NO);
}
static NMSettingConnectionMdns static NMSettingConnectionMdns
_prop_get_connection_mdns(NMDevice *self) _prop_get_connection_mdns(NMDevice *self)
{ {
@@ -10974,7 +11003,7 @@ static void
_dev_ipmanual_start(NMDevice *self) _dev_ipmanual_start(NMDevice *self)
{ {
NMDevicePrivate *priv = NM_DEVICE_GET_PRIVATE(self); NMDevicePrivate *priv = NM_DEVICE_GET_PRIVATE(self);
nm_auto_unref_l3cd const NML3ConfigData *l3cd = NULL; nm_auto_unref_l3cd_init NML3ConfigData *l3cd = NULL;
if (priv->ipmanual_data.state_4 != NM_DEVICE_IP_STATE_NONE if (priv->ipmanual_data.state_4 != NM_DEVICE_IP_STATE_NONE
|| priv->ipmanual_data.state_6 != NM_DEVICE_IP_STATE_NONE) || priv->ipmanual_data.state_6 != NM_DEVICE_IP_STATE_NONE)
@@ -10984,6 +11013,13 @@ _dev_ipmanual_start(NMDevice *self)
l3cd = l3cd =
nm_device_create_l3_config_data_from_connection(self, nm_device_create_l3_config_data_from_connection(self,
nm_device_get_applied_connection(self)); nm_device_get_applied_connection(self));
if (_prop_get_ipvx_routed_dns(self, AF_INET) == NM_SETTING_IP_CONFIG_ROUTED_DNS_YES) {
nm_l3_config_data_set_routed_dns(l3cd, AF_INET, TRUE);
}
if (_prop_get_ipvx_routed_dns(self, AF_INET6) == NM_SETTING_IP_CONFIG_ROUTED_DNS_YES) {
nm_l3_config_data_set_routed_dns(l3cd, AF_INET6, TRUE);
}
} }
if (!l3cd) { if (!l3cd) {

View File

@@ -10,7 +10,9 @@
#include "nm-compat-headers/linux/if_addr.h" #include "nm-compat-headers/linux/if_addr.h"
#include <linux/if_ether.h> #include <linux/if_ether.h>
#include <linux/rtnetlink.h> #include <linux/rtnetlink.h>
#include <linux/fib_rules.h>
#include "libnm-core-aux-intern/nm-libnm-core-utils.h"
#include "libnm-glib-aux/nm-prioq.h" #include "libnm-glib-aux/nm-prioq.h"
#include "libnm-glib-aux/nm-time-utils.h" #include "libnm-glib-aux/nm-time-utils.h"
#include "libnm-platform/nm-platform.h" #include "libnm-platform/nm-platform.h"
@@ -671,7 +673,7 @@ _nm_l3cfg_emit_signal_notify_commit(NML3Cfg *self,
NM_IN_SET(type, NM_L3_CONFIG_NOTIFY_TYPE_PRE_COMMIT, NM_L3_CONFIG_NOTIFY_TYPE_POST_COMMIT)); NM_IN_SET(type, NM_L3_CONFIG_NOTIFY_TYPE_PRE_COMMIT, NM_L3_CONFIG_NOTIFY_TYPE_POST_COMMIT));
notify_data.notify_type = type; notify_data.notify_type = type;
notify_data.commit = (typeof(notify_data.commit)){ notify_data.commit = (typeof(notify_data.commit)) {
.l3cd_old = l3cd_old, .l3cd_old = l3cd_old,
.l3cd_new = l3cd_new, .l3cd_new = l3cd_new,
.l3cd_changed = l3cd_changed, .l3cd_changed = l3cd_changed,
@@ -3886,6 +3888,139 @@ out_ip4_address:
} }
} }
/*****************************************************************************/
#define DNS_ROUTES_FWMARK_TABLE_PRIO 20053
static void
_l3cfg_routed_dns(NML3Cfg *self, NML3ConfigData *l3cd)
{
for (int IS_IPv4 = 1; IS_IPv4 >= 0; IS_IPv4--) {
const char *const *nameservers;
guint i;
guint len;
int addr_family;
gboolean route_added = FALSE;
addr_family = IS_IPv4 ? AF_INET : AF_INET6;
if (!nm_l3_config_data_get_routed_dns(l3cd, addr_family)) {
if (self->priv.dns_route_added_x[IS_IPv4]) {
/* Even if the DNS-routes feature is disabled, it was enabled
* before. Therefore, we need to set one last time the routing
* table sync mode to FULL, to clear the DNS routes added
* previously. */
self->priv.dns_route_added_x[IS_IPv4] = FALSE;
nm_l3_config_data_set_route_table_sync(l3cd,
addr_family,
NM_IP_ROUTE_TABLE_SYNC_MODE_FULL);
}
continue;
}
nameservers = nm_l3_config_data_get_nameservers(l3cd, addr_family, &len);
nm_l3_config_data_set_route_table_sync(l3cd, addr_family, NM_IP_ROUTE_TABLE_SYNC_MODE_FULL);
for (i = 0; i < len; i++) {
nm_auto_nmpobj NMPObject *obj = NULL;
const NMPlatformIPXRoute *route;
NMPlatformIPXRoute route_new;
char addr_buf[INET6_ADDRSTRLEN];
char route_buf[128];
NMIPAddr addr;
int r;
if (!nm_utils_dnsname_parse_assert(addr_family, nameservers[i], NULL, &addr, NULL))
continue;
/* Find the gateway to the DNS over the current interface. When
* doing the lookup, we want to ignore existing DNS routes added
* before: use policy routing with a special fwmark that skips
* the table containing DNS routes. */
r = nm_platform_ip_route_get(self->priv.platform,
addr_family,
&addr,
DNS_ROUTES_FWMARK_TABLE_PRIO,
self->priv.ifindex,
&obj);
if (r < 0) {
_LOGT("could not get route to DNS %s",
nm_inet_ntop(addr_family, addr.addr_ptr, addr_buf));
continue;
}
route = NMP_OBJECT_CAST_IPX_ROUTE(obj);
if (IS_IPv4) {
route_new.r4 = (NMPlatformIP4Route) {
.network = addr.addr4,
.plen = 32,
.table_any = FALSE,
.metric_any = TRUE,
.table_coerced = nm_platform_route_table_coerce(DNS_ROUTES_FWMARK_TABLE_PRIO),
.gateway = route->r4.gateway,
.rt_source = NM_IP_CONFIG_SOURCE_USER,
};
nm_platform_ip_route_normalize(addr_family, &route_new.rx);
_LOGT("route to %s: %s",
nm_inet4_ntop(addr.addr4, addr_buf),
nm_platform_ip4_route_to_string(&route_new.r4, route_buf, sizeof(route_buf)));
nm_l3_config_data_add_route_4(l3cd, &route_new.r4);
nm_l3_config_data_set_route_table_sync(l3cd,
AF_INET,
NM_IP_ROUTE_TABLE_SYNC_MODE_FULL);
route_added = TRUE;
} else {
route_new.r6 = (NMPlatformIP6Route) {
.network = addr.addr6,
.plen = 128,
.table_any = FALSE,
.metric_any = TRUE,
.table_coerced = nm_platform_route_table_coerce(DNS_ROUTES_FWMARK_TABLE_PRIO),
.gateway = route->r6.gateway,
.rt_source = NM_IP_CONFIG_SOURCE_USER,
};
nm_platform_ip_route_normalize(addr_family, &route_new.rx);
_LOGT("route to %s: %s",
nm_inet6_ntop(&addr.addr6, addr_buf),
nm_platform_ip6_route_to_string(&route_new.r6, route_buf, sizeof(route_buf)));
nm_l3_config_data_add_route_6(l3cd, &route_new.r6);
route_added = TRUE;
}
}
if (route_added) {
NMPlatformRoutingRule rule;
/* Add a routing rule that selects the table when not using the
* special fwmark. Note that the rule is shared between all
* devices that use DNS routes. At the moment there is no cleanup
* mechanism: once added the rule stays forever. */
rule = ((NMPlatformRoutingRule) {
.addr_family = addr_family,
.flags = FIB_RULE_INVERT,
.priority = DNS_ROUTES_FWMARK_TABLE_PRIO,
.table = DNS_ROUTES_FWMARK_TABLE_PRIO,
.fwmark = DNS_ROUTES_FWMARK_TABLE_PRIO,
.fwmask = 0xffffffff,
.action = FR_ACT_TO_TBL,
.protocol = RTPROT_STATIC,
});
/* FIXME: don't add the rule every time */
nmp_global_tracker_track_rule(self->priv.global_tracker, &rule, 10, self, NULL);
nmp_global_tracker_sync(self->priv.global_tracker, NMP_OBJECT_TYPE_ROUTING_RULE, TRUE);
}
self->priv.dns_route_added_x[IS_IPv4] = route_added;
}
}
static void static void
_l3cfg_update_combined_config(NML3Cfg *self, _l3cfg_update_combined_config(NML3Cfg *self,
gboolean to_commit, gboolean to_commit,
@@ -4053,6 +4188,8 @@ _l3cfg_update_combined_config(NML3Cfg *self,
nm_assert(l3cd); nm_assert(l3cd);
nm_assert(nm_l3_config_data_get_ifindex(l3cd) == self->priv.ifindex); nm_assert(nm_l3_config_data_get_ifindex(l3cd) == self->priv.ifindex);
_l3cfg_routed_dns(self, l3cd);
nm_l3_config_data_seal(l3cd); nm_l3_config_data_seal(l3cd);
} }

View File

@@ -198,6 +198,7 @@ struct _NML3Cfg {
const NMPObject *plobj; const NMPObject *plobj;
const NMPObject *plobj_next; const NMPObject *plobj_next;
int ifindex; int ifindex;
gboolean dns_route_added_x[2]; /* index with IS_IPv4 */
} priv; } priv;
/* NML3Cfg strongly cooperates with NMNetns. The latter is /* NML3Cfg strongly cooperates with NMNetns. The latter is

View File

@@ -2448,6 +2448,10 @@ device_l3cd_changed(NMDevice *device,
*/ */
state = nm_device_get_state(device); state = nm_device_get_state(device);
if (l3cd_new && state >= NM_DEVICE_STATE_IP_CONFIG && state < NM_DEVICE_STATE_DEACTIVATING) { if (l3cd_new && state >= NM_DEVICE_STATE_IP_CONFIG && state < NM_DEVICE_STATE_DEACTIVATING) {
/* Since the device L3CD_CHANGED signal is emitted *after* the commit of
* configuration, addresses and routes are already set in kernel when we
* write the configuration to resolv.conf or send it to the DNS plugin.
* This prevents "leaks" of DNS queries via the wrong routes.*/
nm_dns_manager_set_ip_config(priv->dns_manager, nm_dns_manager_set_ip_config(priv->dns_manager,
AF_UNSPEC, AF_UNSPEC,
device, device,