diff options
Diffstat (limited to 'src/nm-default-route-manager.c')
-rw-r--r-- | src/nm-default-route-manager.c | 1365 |
1 files changed, 1365 insertions, 0 deletions
diff --git a/src/nm-default-route-manager.c b/src/nm-default-route-manager.c new file mode 100644 index 000000000..5cc1bcd2b --- /dev/null +++ b/src/nm-default-route-manager.c @@ -0,0 +1,1365 @@ +/* -*- Mode: C; tab-width: 4; indent-tabs-mode: t; c-basic-offset: 4 -*- */ +/* NetworkManager -- Network link manager + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Copyright (C) 2014 Red Hat, Inc. + */ + + +#include "config.h" + +#include "nm-default-route-manager.h" + +#include "string.h" + +#include "nm-logging.h" +#include "nm-device.h" +#include "nm-vpn-connection.h" +#include "nm-platform.h" +#include "nm-manager.h" +#include "nm-ip4-config.h" +#include "nm-ip6-config.h" +#include "nm-activation-request.h" + +typedef struct { + GPtrArray *entries_ip4; + GPtrArray *entries_ip6; + struct { + guint guard; + guint backoff_wait_time_ms; + guint idle_handle; + gboolean has_v4_changes; + gboolean has_v6_changes; + } resync; +} NMDefaultRouteManagerPrivate; + +#define NM_DEFAULT_ROUTE_MANAGER_GET_PRIVATE(o) (G_TYPE_INSTANCE_GET_PRIVATE ((o), NM_TYPE_DEFAULT_ROUTE_MANAGER, NMDefaultRouteManagerPrivate)) + +G_DEFINE_TYPE (NMDefaultRouteManager, nm_default_route_manager, G_TYPE_OBJECT) + +static NMDefaultRouteManager *_instance; + +#define _LOG(level, addr_family, ...) \ + G_STMT_START { \ + int __addr_family = (addr_family); \ + guint64 __domain = __addr_family == AF_INET ? LOGD_IP4 : LOGD_IP6; \ + \ + if (nm_logging_enabled ((level), (__domain))) { \ + char __ch = __addr_family == AF_INET ? '4' : (__addr_family == AF_INET6 ? '6' : '-'); \ + char __prefix[30] = "default-route"; \ + \ + if ((self) != _instance) \ + g_snprintf (__prefix, sizeof (__prefix), "default-route%c[%p]", __ch, (self)); \ + else \ + __prefix[STRLEN ("default-route")] = __ch; \ + nm_log ((level), (__domain), \ + "%s: " _NM_UTILS_MACRO_FIRST(__VA_ARGS__), \ + __prefix _NM_UTILS_MACRO_REST(__VA_ARGS__)); \ + } \ + } G_STMT_END + +#define _LOGD(addr_family, ...) _LOG (LOGL_DEBUG, addr_family, __VA_ARGS__) +#define _LOGI(addr_family, ...) _LOG (LOGL_INFO , addr_family, __VA_ARGS__) +#define _LOGW(addr_family, ...) _LOG (LOGL_WARN , addr_family, __VA_ARGS__) +#define _LOGE(addr_family, ...) _LOG (LOGL_ERR , addr_family, __VA_ARGS__) + +#define LOG_ENTRY_FMT "entry[%u/%s:%p:%s:%c%c]" +#define LOG_ENTRY_ARGS(entry_idx, entry) \ + (entry_idx), \ + NM_IS_DEVICE ((entry)->source.pointer) ? "dev" : "vpn", \ + (entry)->source.pointer, \ + NM_IS_DEVICE ((entry)->source.pointer) ? nm_device_get_iface ((entry)->source.device) : nm_vpn_connection_get_connection_id ((entry)->source.vpn), \ + ((entry)->never_default ? 'N' : 'n'), \ + ((entry)->synced ? 'S' : 's') + +/***********************************************************************************/ + +static void _resync_idle_cancel (NMDefaultRouteManager *self); + +/***********************************************************************************/ + +typedef struct { + union { + void *pointer; + GObject *object; + NMDevice *device; + NMVpnConnection *vpn; + } source; + NMPlatformIPXRoute route; + + /* Whether the route is synced to platform and has a default route. + * + * ( synced && !never_default): the interface gets a default route that + * is enforced and managed by NMDefaultRouteManager. + * + * (!synced && !never_default): the interface has this route, but it is assumed. + * Assumed interfaces are those that have no tracked entry or that only have + * (!synced && !never_default) entries. NMDefaultRouteManager will not touch + * default routes on these interfaces. + * This combination makes only sense for device sources. + * They are tracked so that assumed devices can also be the best device. + * + * ( synced && never_default): entries of this kind are a placeholder + * to indicate that the ifindex is managed but has no default-route. + * Missing entries also indicate that a certain ifindex has no default-route. + * The difference is that missing entries are considered assumed while on + * (synced && never_default) entires the absence of the default route + * is enforced. NMDefaultRouteManager will actively remove any default + * route on such ifindexes. + * Also, for VPN sources in addition we track them so that a never-default + * VPN connection can be choosen by get_best_config() to receive the DNS configuration. + * + * (!synced && never_default): this combination makes no sense. + */ + gboolean synced; + gboolean never_default; + + guint32 effective_metric; +} Entry; + +typedef struct { + int addr_family; + GPtrArray *(*get_entries) (NMDefaultRouteManagerPrivate *priv); + const char *(*platform_route_to_string) (const NMPlatformIPRoute *route); + GArray *(*platform_route_get_all) (int ifindex, NMPlatformGetRouteMode mode); + gboolean (*platform_route_delete_default) (int ifindex, guint32 metric); + guint32 (*route_metric_normalize) (guint32 metric); +} VTableIP; + +static const VTableIP vtable_ip4, vtable_ip6; + +#define VTABLE_IS_IP4 (vtable->addr_family == AF_INET) + +static NMPlatformIPRoute * +_vt_route_index (const VTableIP *vtable, GArray *routes, guint index) +{ + if (VTABLE_IS_IP4) + return (NMPlatformIPRoute *) &g_array_index (routes, NMPlatformIP4Route, index); + else + return (NMPlatformIPRoute *) &g_array_index (routes, NMPlatformIP6Route, index); +} + +static gboolean +_vt_routes_has_entry (const VTableIP *vtable, GArray *routes, const Entry *entry) +{ + guint i; + NMPlatformIPXRoute route = entry->route; + + route.rx.metric = entry->effective_metric; + + if (VTABLE_IS_IP4) { + for (i = 0; i < routes->len; i++) { + NMPlatformIP4Route *r = &g_array_index (routes, NMPlatformIP4Route, i); + + route.rx.source = r->source; + if (nm_platform_ip4_route_cmp (r, &route.r4) == 0) + return TRUE; + } + } else { + for (i = 0; i < routes->len; i++) { + NMPlatformIP6Route *r = &g_array_index (routes, NMPlatformIP6Route, i); + + route.rx.source = r->source; + if (nm_platform_ip6_route_cmp (r, &route.r6) == 0) + return TRUE; + } + } + return FALSE; +} + +static void +_entry_free (Entry *entry) +{ + if (entry) { + g_object_unref (entry->source.object); + g_slice_free (Entry, entry); + } +} + +static Entry * +_entry_find_by_source (GPtrArray *entries, gpointer source, guint *out_idx) +{ + guint i; + + for (i = 0; i < entries->len; i++) { + Entry *e = g_ptr_array_index (entries, i); + + if (e->source.pointer == source) { + if (out_idx) + *out_idx = i; + return e; + } + } + + if (out_idx) + *out_idx = G_MAXUINT; + return NULL; +} + +static gboolean +_platform_route_sync_add (const VTableIP *vtable, NMDefaultRouteManager *self, guint32 metric) +{ + NMDefaultRouteManagerPrivate *priv = NM_DEFAULT_ROUTE_MANAGER_GET_PRIVATE (self); + GPtrArray *entries = vtable->get_entries (priv); + guint i; + Entry *entry_unsynced = NULL; + Entry *entry = NULL; + gboolean success; + + /* Find the entries for the given metric. + * The effective metric for synced entries is choosen in a way that it + * is unique (except for G_MAXUINT32, where a clash is not solvable). */ + for (i = 0; i < entries->len; i++) { + Entry *e = g_ptr_array_index (entries, i); + + if (e->never_default) + continue; + + if (e->effective_metric != metric) + continue; + + if (e->synced) { + g_assert (!entry || metric == G_MAXUINT32); + if (!entry) + entry = e; + } else + entry_unsynced = e; + } + + /* We don't expect to have an unsynced *and* a synced entry for the same metric. + * Unless, (a) their metric is G_MAXUINT32, in which case we could not find an unused effective metric, + * or (b) if we have an unsynced and a synced entry for the same ifindex. + * The latter case happens for example when activating an openvpn connection (synced) and + * assuming the corresponding tun0 interface (unsynced). */ + g_assert (!entry || !entry_unsynced || (entry->route.rx.ifindex == entry_unsynced->route.rx.ifindex) || metric == G_MAXUINT32); + + /* we only add the route, if we have an (to be synced) entry for it. */ + if (!entry) + return FALSE; + + if (VTABLE_IS_IP4) { + success = nm_platform_ip4_route_add (entry->route.rx.ifindex, + entry->route.rx.source, + 0, + 0, + entry->route.r4.gateway, + 0, + entry->effective_metric, + entry->route.rx.mss); + } else { + success = nm_platform_ip6_route_add (entry->route.rx.ifindex, + entry->route.rx.source, + in6addr_any, + 0, + entry->route.r6.gateway, + entry->effective_metric, + entry->route.rx.mss); + } + if (!success) { + _LOGW (vtable->addr_family, "failed to add default route %s with effective metric %u", + vtable->platform_route_to_string (&entry->route.rx), (guint) entry->effective_metric); + } + return TRUE; +} + +static gboolean +_platform_route_sync_flush (const VTableIP *vtable, NMDefaultRouteManager *self, int ifindex_to_flush) +{ + NMDefaultRouteManagerPrivate *priv = NM_DEFAULT_ROUTE_MANAGER_GET_PRIVATE (self); + GPtrArray *entries = vtable->get_entries (priv); + GArray *routes; + guint i, j; + gboolean changed = FALSE; + + /* prune all other default routes from this device. */ + routes = vtable->platform_route_get_all (0, NM_PLATFORM_GET_ROUTE_MODE_ONLY_DEFAULT); + + for (i = 0; i < routes->len; i++) { + const NMPlatformIPRoute *route; + gboolean has_ifindex_synced = FALSE; + Entry *entry = NULL; + + route = _vt_route_index (vtable, routes, i); + + /* look at all entires and see if the route for this ifindex pair is + * a known entry. */ + for (j = 0; j < entries->len; j++) { + Entry *e = g_ptr_array_index (entries, j); + + if ( e->route.rx.ifindex == route->ifindex + && e->synced) { + has_ifindex_synced = TRUE; + if ( !e->never_default + && e->effective_metric == route->metric) + entry = e; + } + } + + /* we only delete the route if we don't have a matching entry, + * and there is at least one entry that references this ifindex + * (indicating that the ifindex is managed by us -- not assumed). + * + * Otherwise, don't delete the route because it's configured + * externally (and will be assumed -- or already is assumed). + */ + if ( !entry + && (has_ifindex_synced || ifindex_to_flush == route->ifindex)) { + vtable->platform_route_delete_default (route->ifindex, route->metric); + changed = TRUE; + } + } + g_array_free (routes, TRUE); + return changed; +} + +static int +_sort_entries_cmp (gconstpointer a, gconstpointer b, gpointer user_data) +{ + guint32 m_a, m_b; + const Entry *e_a = *((const Entry **) a); + const Entry *e_b = *((const Entry **) b); + + /* when comparing routes, we consider the (original) metric. */ + m_a = e_a->route.rx.metric; + m_b = e_b->route.rx.metric; + + /* we normalize route.metric already in _ipx_update_default_route(). + * so we can just compare the metrics numerically */ + + if (m_a != m_b) + return (m_a < m_b) ? -1 : 1; + + /* If the metrics are equal, we prefer the one that is !never_default */ + if (!!e_a->never_default != !!e_b->never_default) + return e_a->never_default ? 1 : -1; + + /* If the metrics are equal, we prefer the one that is assumed (!synced). + * Entries that we sync, can be modified so that only the best + * entry has a (deterministically) lowest metric. + * With assumed devices we cannot increase/change the metric. + * For example: two devices, both metric 0. One is assumed the other is + * synced. + * If we would choose the synced entry as best, we cannot + * increase the metric of the assumed one and we would have non-determinism. + * If we instead prefer the assumed device, we can increase the metric + * of the synced device and the assumed device is (deterministically) + * prefered. + * If both devices are assumed, we also have non-determinism, but also + * we don't reorder either. + */ + if (!!e_a->synced != !!e_b->synced) + return e_a->synced ? 1 : -1; + + /* otherwise, do not reorder */ + return 0; +} + +static GHashTable * +_get_assumed_interface_metrics (const VTableIP *vtable, NMDefaultRouteManager *self, GArray *routes) +{ + NMDefaultRouteManagerPrivate *priv = NM_DEFAULT_ROUTE_MANAGER_GET_PRIVATE (self); + GPtrArray *entries; + guint i, j; + GHashTable *result; + + /* create a list of all metrics that are currently assigned on an interface + * that is *not* already covered by one of our synced entries. + * IOW, returns the metrics that are in use by assumed interfaces + * that we want to preserve. */ + + entries = vtable->get_entries (priv); + + result = g_hash_table_new (NULL, NULL); + + for (i = 0; i < routes->len; i++) { + gboolean ifindex_has_synced_entry = FALSE; + const NMPlatformIPRoute *route; + + route = _vt_route_index (vtable, routes, i); + + for (j = 0; j < entries->len; j++) { + Entry *e = g_ptr_array_index (entries, j); + + if ( e->synced + && e->route.rx.ifindex == route->ifindex) { + ifindex_has_synced_entry = TRUE; + break; + } + } + + if (!ifindex_has_synced_entry) + g_hash_table_add (result, GUINT_TO_POINTER (vtable->route_metric_normalize (route->metric))); + } + + return result; +} + +static int +_sort_metrics_ascending_fcn (gconstpointer a, gconstpointer b) +{ + guint32 m_a = *((guint32 *) a); + guint32 m_b = *((guint32 *) b); + + if (m_a < m_b) + return -1; + return m_a == m_b ? 0 : 1; +} + +static gboolean +_resync_all (const VTableIP *vtable, NMDefaultRouteManager *self, const Entry *changed_entry, const Entry *old_entry, gboolean external_change) +{ + NMDefaultRouteManagerPrivate *priv = NM_DEFAULT_ROUTE_MANAGER_GET_PRIVATE (self); + Entry *entry; + guint i, j; + gint64 last_metric = -1; + guint32 expected_metric; + GPtrArray *entries; + GArray *changed_metrics = g_array_new (FALSE, FALSE, sizeof (guint32)); + GHashTable *assumed_metrics; + GArray *routes; + gboolean changed = FALSE; + int ifindex_to_flush = 0; + + g_assert (priv->resync.guard == 0); + priv->resync.guard++; + + if (!external_change) { + if (VTABLE_IS_IP4) + priv->resync.has_v4_changes = FALSE; + else + priv->resync.has_v6_changes = FALSE; + if (!priv->resync.has_v4_changes && !priv->resync.has_v6_changes) + _resync_idle_cancel (self); + } + + entries = vtable->get_entries (priv); + + routes = vtable->platform_route_get_all (0, NM_PLATFORM_GET_ROUTE_MODE_ONLY_DEFAULT); + + assumed_metrics = _get_assumed_interface_metrics (vtable, self, routes); + + if (old_entry && old_entry->synced && !old_entry->never_default) { + /* The old version obviously changed. */ + g_array_append_val (changed_metrics, old_entry->effective_metric); + } + + /* first iterate over all entries and adjust the effective metrics. */ + for (i = 0; i < entries->len; i++) { + entry = g_ptr_array_index (entries, i); + + g_assert (entry != old_entry); + + if (entry->never_default) + continue; + + if (!entry->synced) { + gboolean has_synced_entry = FALSE; + + /* A non synced entry is completely ignored, if we have + * a synced entry for the same if index. + * Otherwise the metric of the entry is still remembered as + * last_metric to avoid reusing it. */ + for (j = 0; j < entries->len; j++) { + const Entry *e = g_ptr_array_index (entries, j); + + if ( e->synced + && e->route.rx.ifindex == entry->route.rx.ifindex) { + has_synced_entry = TRUE; + break; + } + } + if (!has_synced_entry) + last_metric = MAX (last_metric, (gint64) entry->effective_metric); + continue; + } + + expected_metric = entry->route.rx.metric; + if ((gint64) expected_metric <= last_metric) + expected_metric = last_metric == G_MAXUINT32 ? G_MAXUINT32 : last_metric + 1; + + while ( expected_metric < G_MAXUINT32 + && g_hash_table_contains (assumed_metrics, GUINT_TO_POINTER (expected_metric))) { + gboolean has_metric_for_ifindex = FALSE; + + /* Check if there are assumed devices that have default routes with this metric. + * If there are any, we have to pick another effective_metric. */ + + /* However, if there is a matching route (ifindex+metric) for our current entry, we are done. */ + for (j = 0; j < routes->len; j++) { + const NMPlatformIPRoute *r = _vt_route_index (vtable, routes, i); + + if ( r->metric == expected_metric + && r->ifindex == entry->route.rx.ifindex) { + has_metric_for_ifindex = TRUE; + break; + } + } + if (has_metric_for_ifindex) + break; + expected_metric++; + } + + if (changed_entry == entry) { + /* for the changed entry, the previous metric was either old_entry->effective_metric, + * or none. Hence, we only have to remember what is going to change. */ + g_array_append_val (changed_metrics, expected_metric); + if (old_entry) { + _LOGD (vtable->addr_family, LOG_ENTRY_FMT": update %s (%u -> %u)", LOG_ENTRY_ARGS (i, entry), + vtable->platform_route_to_string (&entry->route.rx), (guint) old_entry->effective_metric, + (guint) expected_metric); + } else { + _LOGD (vtable->addr_family, LOG_ENTRY_FMT": add %s (%u)", LOG_ENTRY_ARGS (i, entry), + vtable->platform_route_to_string (&entry->route.rx), (guint) expected_metric); + } + } else if (entry->effective_metric != expected_metric) { + g_array_append_val (changed_metrics, entry->effective_metric); + g_array_append_val (changed_metrics, expected_metric); + _LOGD (vtable->addr_family, LOG_ENTRY_FMT": resync metric %s (%u -> %u)", LOG_ENTRY_ARGS (i, entry), + vtable->platform_route_to_string (&entry->route.rx), (guint) entry->effective_metric, + (guint) expected_metric); + } else { + if (!_vt_routes_has_entry (vtable, routes, entry)) { + g_array_append_val (changed_metrics, entry->effective_metric); + _LOGD (vtable->addr_family, LOG_ENTRY_FMT": readd route %s (%u -> %u)", LOG_ENTRY_ARGS (i, entry), + vtable->platform_route_to_string (&entry->route.rx), (guint) entry->effective_metric, + (guint) entry->effective_metric); + } + } + + if (entry->effective_metric != expected_metric) { + entry->effective_metric = expected_metric; + changed = TRUE; + } + last_metric = expected_metric; + } + + g_array_free (routes, TRUE); + + g_array_sort (changed_metrics, _sort_metrics_ascending_fcn); + last_metric = -1; + for (j = 0; j < changed_metrics->len; j++) { + expected_metric = g_array_index (changed_metrics, guint32, j); + + if (last_metric == (gint64) expected_metric) { + /* skip duplicates. */ + continue; + } + changed |= _platform_route_sync_add (vtable, self, expected_metric); + last_metric = expected_metric; + } + + if ( old_entry + && !changed_entry + && old_entry->synced + && !old_entry->never_default) { + /* If we entriely remove an entry that was synced before, we must make + * sure to flush routes for this ifindex too. Otherwise they linger + * around as "assumed" routes */ + ifindex_to_flush = old_entry->route.rx.ifindex; + } + + changed |= _platform_route_sync_flush (vtable, self, ifindex_to_flush); + + g_array_free (changed_metrics, TRUE); + g_hash_table_unref (assumed_metrics); + + priv->resync.guard--; + return changed; +} + +static void +_entry_at_idx_update (const VTableIP *vtable, NMDefaultRouteManager *self, guint entry_idx, const Entry *old_entry) +{ + NMDefaultRouteManagerPrivate *priv = NM_DEFAULT_ROUTE_MANAGER_GET_PRIVATE (self); + Entry *entry; + GPtrArray *entries; + + entries = vtable->get_entries (priv); + g_assert (entry_idx < entries->len); + + entry = g_ptr_array_index (entries, entry_idx); + + g_assert ( !old_entry + || (entry->source.pointer == old_entry->source.pointer && entry->route.rx.ifindex == old_entry->route.rx.ifindex)); + + if (!entry->synced && !entry->never_default) + entry->effective_metric = entry->route.rx.metric; + + _LOGD (vtable->addr_family, LOG_ENTRY_FMT": %s %s", + LOG_ENTRY_ARGS (entry_idx, entry), + old_entry ? "update" : "add", + vtable->platform_route_to_string (&entry->route.rx)); + + g_ptr_array_sort_with_data (entries, _sort_entries_cmp, NULL); + + _resync_all (vtable, self, entry, old_entry, FALSE); +} + +static void +_entry_at_idx_remove (const VTableIP *vtable, NMDefaultRouteManager *self, guint entry_idx) +{ + NMDefaultRouteManagerPrivate *priv = NM_DEFAULT_ROUTE_MANAGER_GET_PRIVATE (self); + Entry *entry; + GPtrArray *entries; + + entries = vtable->get_entries (priv); + + g_assert (entry_idx < entries->len); + + entry = g_ptr_array_index (entries, entry_idx); + + _LOGD (vtable->addr_family, LOG_ENTRY_FMT": remove %s (%u)", LOG_ENTRY_ARGS (entry_idx, entry), + vtable->platform_route_to_string (&entry->route.rx), (guint) entry->effective_metric); + + /* Remove the entry from the list (but don't free it yet) */ + g_ptr_array_index (entries, entry_idx) = NULL; + g_ptr_array_remove_index (entries, entry_idx); + + _resync_all (vtable, self, NULL, entry, FALSE); + + _entry_free (entry); +} + +/***********************************************************************************/ + +static void +_ipx_update_default_route (const VTableIP *vtable, NMDefaultRouteManager *self, gpointer source) +{ + NMDefaultRouteManagerPrivate *priv; + Entry *entry; + guint entry_idx; + const NMPlatformIPRoute *default_route = NULL; + NMPlatformIPXRoute rt; + int ip_ifindex; + GPtrArray *entries; + NMDevice *device = NULL; + NMVpnConnection *vpn = NULL; + gboolean never_default = FALSE; + gboolean synced = FALSE; + + g_return_if_fail (NM_IS_DEFAULT_ROUTE_MANAGER (self)); + if (NM_IS_DEVICE (source)) + device = source; + else if (NM_IS_VPN_CONNECTION (source)) + vpn = source; + else + g_return_if_reached (); + + if (device) + ip_ifindex = nm_device_get_ip_ifindex (device); + else { + ip_ifindex = nm_vpn_connection_get_ip_ifindex (vpn); + + if (ip_ifindex <= 0) { + NMDevice *parent = nm_active_connection_get_device (NM_ACTIVE_CONNECTION (vpn)); + + if (parent) + ip_ifindex = nm_device_get_ip_ifindex (parent); + } + } + + priv = NM_DEFAULT_ROUTE_MANAGER_GET_PRIVATE (self); + + entries = vtable->get_entries (priv); + entry = _entry_find_by_source (entries, source, &entry_idx); + + if ( entry + && entry->route.rx.ifindex != ip_ifindex) { + /* Strange... the ifindex changed... Remove the device and start again. */ + _LOGD (vtable->addr_family, "ifindex of "LOG_ENTRY_FMT" changed: %d -> %d", + LOG_ENTRY_ARGS (entry_idx, entry), + entry->route.rx.ifindex, ip_ifindex); + + g_object_freeze_notify (G_OBJECT (self)); + _entry_at_idx_remove (vtable, self, entry_idx); + g_assert (!_entry_find_by_source (entries, source, NULL)); + _ipx_update_default_route (vtable, self, source); + g_object_thaw_notify (G_OBJECT (self)); + return; + } + + /* get the @default_route from the device. */ + if (ip_ifindex > 0) { + if (device) { + gboolean is_assumed; + + if (VTABLE_IS_IP4) + default_route = (const NMPlatformIPRoute *) nm_device_get_ip4_default_route (device, &is_assumed); + else + default_route = (const NMPlatformIPRoute *) nm_device_get_ip6_default_route (device, &is_assumed); + if (!default_route && !is_assumed) { + /* the device has no default route, but it is not assumed. That means, NMDefaultRouteManager + * enforces that the device has no default route. + * + * Hence we have to keep track of this entry, otherwise a missing entry tells us + * that the interface is assumed and NM would not remove the default routes on + * the device. */ + memset (&rt, 0, sizeof (rt)); + rt.rx.ifindex = ip_ifindex; + rt.rx.source = NM_IP_CONFIG_SOURCE_UNKNOWN; + rt.rx.metric = G_MAXUINT32; + default_route = &rt.rx; + + never_default = TRUE; + synced = TRUE; + } else + synced = default_route && !is_assumed; + } else { + NMConnection *connection = nm_active_connection_get_connection ((NMActiveConnection *) vpn); + + if ( connection + && nm_vpn_connection_get_vpn_state (vpn) == NM_VPN_CONNECTION_STATE_ACTIVATED) { + + memset (&rt, 0, sizeof (rt)); + if (VTABLE_IS_IP4) { + NMIP4Config *vpn_config; + + vpn_config = nm_vpn_connection_get_ip4_config (vpn); + if (vpn_config) { + never_default = nm_ip4_config_get_never_default (vpn_config); + rt.r4.ifindex = ip_ifindex; + rt.r4.source = NM_IP_CONFIG_SOURCE_VPN; + rt.r4.gateway = nm_vpn_connection_get_ip4_internal_gateway (vpn); + rt.r4.metric = nm_vpn_connection_get_ip4_route_metric (vpn); + rt.r4.mss = nm_ip4_config_get_mss (vpn_config); + default_route = &rt.rx; + } + } else { + NMIP6Config *vpn_config; + + vpn_config = nm_vpn_connection_get_ip6_config (vpn); + if (vpn_config) { + const struct in6_addr *int_gw = nm_vpn_connection_get_ip6_internal_gateway (vpn); + + never_default = nm_ip6_config_get_never_default (vpn_config); + rt.r6.ifindex = ip_ifindex; + rt.r6.source = NM_IP_CONFIG_SOURCE_VPN; + rt.r6.gateway = int_gw ? *int_gw : in6addr_any; + rt.r6.metric = nm_vpn_connection_get_ip6_route_metric (vpn); + rt.r6.mss = nm_ip6_config_get_mss (vpn_config); + default_route = &rt.rx; + } + } + } + synced = TRUE; + } + } + g_assert (!default_route || default_route->plen == 0); + + if (!entry && !default_route) + /* nothing to do */; + else if (!entry) { + /* add */ + entry = g_slice_new0 (Entry); + entry->source.object = g_object_ref (source); + + if (VTABLE_IS_IP4) + entry->route.r4 = *((const NMPlatformIP4Route *) default_route); + else + entry->route.r6 = *((const NMPlatformIP6Route *) default_route); + + /* only use normalized metrics */ + entry->route.rx.metric = vtable->route_metric_normalize (entry->route.rx.metric); + entry->route.rx.ifindex = ip_ifindex; + entry->never_default = never_default; + entry->effective_metric = entry->route.rx.metric; + entry->synced = synced; + + g_ptr_array_add (entries, entry); + _entry_at_idx_update (vtable, self, entries->len - 1, NULL); + } else if (default_route) { + /* update */ + Entry old_entry, new_entry; + + new_entry = *entry; + if (VTABLE_IS_IP4) + new_entry.route.r4 = *((const NMPlatformIP4Route *) default_route); + else + new_entry.route.r6 = *((const NMPlatformIP6Route *) default_route); + /* only use normalized metrics */ + new_entry.route.rx.metric = vtable->route_metric_normalize (new_entry.route.rx.metric); + new_entry.route.rx.ifindex = ip_ifindex; + new_entry.never_default = never_default; + new_entry.synced = synced; + + if (memcmp (entry, &new_entry, sizeof (new_entry)) == 0) + return; + + old_entry = *entry; + *entry = new_entry; + _entry_at_idx_update (vtable, self, entry_idx, &old_entry); + } else { + /* delete */ + _entry_at_idx_remove (vtable, self, entry_idx); + } +} + +void +nm_default_route_manager_ip4_update_default_route (NMDefaultRouteManager *self, gpointer source) +{ + _ipx_update_default_route (&vtable_ip4, self, source); +} + +void +nm_default_route_manager_ip6_update_default_route (NMDefaultRouteManager *self, gpointer source) +{ + _ipx_update_default_route (&vtable_ip6, self, source); +} + +/***********************************************************************************/ + +static gboolean +_ipx_connection_has_default_route (const VTableIP *vtable, NMDefaultRouteManager *self, NMConnection *connection) +{ + const char *method; + NMSettingIPConfig *s_ip; + + g_return_val_if_fail (NM_IS_DEFAULT_ROUTE_MANAGER (self), FALSE); + g_return_val_if_fail (NM_IS_CONNECTION (connection), FALSE); + + if (VTABLE_IS_IP4) + s_ip = nm_connection_get_setting_ip4_config (connection); + else + s_ip = nm_connection_get_setting_ip6_config (connection); + if (!s_ip || nm_setting_ip_config_get_never_default (s_ip)) + return FALSE; + + if (VTABLE_IS_IP4) { + method = nm_utils_get_ip_config_method (connection, NM_TYPE_SETTING_IP4_CONFIG); + if ( !method + || !strcmp (method, NM_SETTING_IP4_CONFIG_METHOD_DISABLED) + || !strcmp (method, NM_SETTING_IP4_CONFIG_METHOD_LINK_LOCAL)) + return FALSE; + } else { + method = nm_utils_get_ip_config_method (connection, NM_TYPE_SETTING_IP6_CONFIG); + if ( !method + || !strcmp (method, NM_SETTING_IP6_CONFIG_METHOD_IGNORE) + || !strcmp (method, NM_SETTING_IP6_CONFIG_METHOD_LINK_LOCAL)) + return FALSE; + } + + return TRUE; +} + +gboolean +nm_default_route_manager_ip4_connection_has_default_route (NMDefaultRouteManager *self, NMConnection *connection) +{ + return _ipx_connection_has_default_route (&vtable_ip4, self, connection); +} + +gboolean +nm_default_route_manager_ip6_connection_has_default_route (NMDefaultRouteManager *self, NMConnection *connection) +{ + return _ipx_connection_has_default_route (&vtable_ip6, self, connection); +} + +/***********************************************************************************/ + +static NMDevice * +_ipx_get_best_device (const VTableIP *vtable, NMDefaultRouteManager *self, const GSList *devices) +{ + NMDefaultRouteManagerPrivate *priv; + GPtrArray *entries; + guint i; + + g_return_val_if_fail (NM_IS_DEFAULT_ROUTE_MANAGER (self), NULL); + + if (!devices) + return NULL; + + priv = NM_DEFAULT_ROUTE_MANAGER_GET_PRIVATE (self); + entries = vtable->get_entries (priv); + + for (i = 0; i < entries->len; i++) { + Entry *entry = g_ptr_array_index (entries, i); + + if (!NM_IS_DEVICE (entry->source.pointer)) + continue; + + if (entry->never_default) + continue; + + if (g_slist_find ((GSList *) devices, entry->source.device)) + return entry->source.pointer; + } + return NULL; +} + +/** _ipx_get_best_activating_device: + * @vtable: the virtual table + * @self: #NMDefaultRouteManager + * @devices: list of devices to be searched. Only devices from this list will be considered + * @fully_activated: if #TRUE, only search for devices that are fully activated. Otherwise, + * search if there is a best device going to be activated. In the latter case, this will + * return NULL if the best device is already activated. + * @preferred_device: if not-NULL, this device is preferred if there are more devices with + * the same priority. + **/ +static NMDevice * +_ipx_get_best_activating_device (const VTableIP *vtable, NMDefaultRouteManager *self, const GSList *devices, NMDevice *preferred_device) +{ + NMDefaultRouteManagerPrivate *priv; + const GSList *iter; + NMDevice *best_device = NULL; + guint32 best_prio = G_MAXUINT32; + NMDevice *best_activated_device; + + g_return_val_if_fail (NM_IS_DEFAULT_ROUTE_MANAGER (self), NULL); + + priv = NM_DEFAULT_ROUTE_MANAGER_GET_PRIVATE (self); + + best_activated_device = _ipx_get_best_device (vtable, self, devices); + + for (iter = devices; iter; iter = g_slist_next (iter)) { + NMDevice *device = NM_DEVICE (iter->data); + guint32 prio; + Entry *entry; + + entry = _entry_find_by_source (vtable->get_entries (priv), device, NULL); + + if (entry) { + /* of all the device that have an entry, we already know that best_activated_device + * is the best. entry cannot be better. */ + if (entry->source.device != best_activated_device) + continue; + prio = entry->effective_metric; + } else { + NMDeviceState state = nm_device_get_state (device); + + if ( state <= NM_DEVICE_STATE_DISCONNECTED + || state >= NM_DEVICE_STATE_DEACTIVATING) + continue; + + if (!_ipx_connection_has_default_route (vtable, self, nm_device_get_connection (device))) + continue; + + prio = nm_device_get_ip4_route_metric (device); + } + prio = vtable->route_metric_normalize (prio); + + if ( !best_device + || prio < best_prio + || (prio == best_prio && preferred_device == device)) { + best_device = device; + best_prio = prio; + } + } + + /* There's only a best activating device if the best device + * among all activating and already-activated devices is a + * still-activating one. + */ + if (best_device && nm_device_get_state (best_device) >= NM_DEVICE_STATE_SECONDARIES) + return NULL; + return best_device; +} + +NMDevice * +nm_default_route_manager_ip4_get_best_device (NMDefaultRouteManager *self, const GSList *devices, gboolean fully_activated, NMDevice *preferred_device) +{ + if (fully_activated) + return _ipx_get_best_device (&vtable_ip4, self, devices); + else + return _ipx_get_best_activating_device (&vtable_ip4, self, devices, preferred_device); +} + +NMDevice * +nm_default_route_manager_ip6_get_best_device (NMDefaultRouteManager *self, const GSList *devices, gboolean fully_activated, NMDevice *preferred_device) +{ + if (fully_activated) + return _ipx_get_best_device (&vtable_ip6, self, devices); + else + return _ipx_get_best_activating_device (&vtable_ip6, self, devices, preferred_device); +} + +/***********************************************************************************/ + +static gpointer +_ipx_get_best_config (const VTableIP *vtable, + NMDefaultRouteManager *self, + gboolean ignore_never_default, + const char **out_ip_iface, + NMActiveConnection **out_ac, + NMDevice **out_device, + NMVpnConnection **out_vpn) +{ + NMDefaultRouteManagerPrivate *priv; + GPtrArray *entries; + guint i; + gpointer config_result = NULL; + + g_return_val_if_fail (NM_IS_DEFAULT_ROUTE_MANAGER (self), NULL); + + if (out_ip_iface) + *out_ip_iface = NULL; + if (out_ac) + *out_ac = NULL; + if (out_device) + *out_device = NULL; + if (out_vpn) + *out_vpn = NULL; + + priv = NM_DEFAULT_ROUTE_MANAGER_GET_PRIVATE (self); + + g_return_val_if_fail (NM_IS_DEFAULT_ROUTE_MANAGER (self), NULL); + + priv = NM_DEFAULT_ROUTE_MANAGER_GET_PRIVATE (self); + entries = vtable->get_entries (priv); + + for (i = 0; i < entries->len; i++) { + Entry *entry = g_ptr_array_index (entries, i); + + if (!NM_IS_DEVICE (entry->source.pointer)) { + NMVpnConnection *vpn = NM_VPN_CONNECTION (entry->source.vpn); + + if (entry->never_default && !ignore_never_default) + continue; + + if (VTABLE_IS_IP4) + config_result = nm_vpn_connection_get_ip4_config (vpn); + else + config_result = nm_vpn_connection_get_ip6_config (vpn); + g_assert (config_result); + + if (out_vpn) + *out_vpn = vpn; + if (out_ac) + *out_ac = NM_ACTIVE_CONNECTION (vpn); + if (out_ip_iface) + *out_ip_iface = nm_vpn_connection_get_ip_iface (vpn); + } else { + NMDevice *device = entry->source.device; + NMActRequest *req; + + if (entry->never_default) + continue; + + if (VTABLE_IS_IP4) + config_result = nm_device_get_ip4_config (device); + else + config_result = nm_device_get_ip6_config (device); + g_assert (config_result); + req = nm_device_get_act_request (device); + g_assert (req); + + if (out_device) + *out_device = device; + if (out_ac) + *out_ac = NM_ACTIVE_CONNECTION (req); + if (out_ip_iface) + *out_ip_iface = nm_device_get_ip_iface (device); + } + break; + } + + return config_result; +} + +NMIP4Config * +nm_default_route_manager_ip4_get_best_config (NMDefaultRouteManager *self, + gboolean ignore_never_default, + const char **out_ip_iface, + NMActiveConnection **out_ac, + NMDevice **out_device, + NMVpnConnection **out_vpn) +{ + return _ipx_get_best_config (&vtable_ip4, + self, + ignore_never_default, + out_ip_iface, + out_ac, + out_device, + out_vpn); +} + +NMIP6Config * +nm_default_route_manager_ip6_get_best_config (NMDefaultRouteManager *self, + gboolean ignore_never_default, + const char **out_ip_iface, + NMActiveConnection **out_ac, + NMDevice **out_device, + NMVpnConnection **out_vpn) +{ + return _ipx_get_best_config (&vtable_ip6, + self, + ignore_never_default, + out_ip_iface, + out_ac, + out_device, + out_vpn); +} + +/***********************************************************************************/ + +static GPtrArray * +_v4_get_entries (NMDefaultRouteManagerPrivate *priv) +{ + return priv->entries_ip4; +} + +static GPtrArray * +_v6_get_entries (NMDefaultRouteManagerPrivate *priv) +{ + return priv->entries_ip6; +} + +static gboolean +_v4_platform_route_delete_default (int ifindex, guint32 metric) +{ + return nm_platform_ip4_route_delete (ifindex, 0, 0, metric); +} + +static gboolean +_v6_platform_route_delete_default (int ifindex, guint32 metric) +{ + return nm_platform_ip6_route_delete (ifindex, in6addr_any, 0, metric); +} + +static guint32 +_v4_route_metric_normalize (guint32 metric) +{ + return metric; +} + +static const VTableIP vtable_ip4 = { + .addr_family = AF_INET, + .get_entries = _v4_get_entries, + .platform_route_to_string = (const char *(*)(const NMPlatformIPRoute *)) nm_platform_ip4_route_to_string, + .platform_route_get_all = nm_platform_ip4_route_get_all, + .platform_route_delete_default = _v4_platform_route_delete_default, + .route_metric_normalize = _v4_route_metric_normalize, +}; + +static const VTableIP vtable_ip6 = { + .addr_family = AF_INET6, + .get_entries = _v6_get_entries, + .platform_route_to_string = (const char *(*)(const NMPlatformIPRoute *)) nm_platform_ip6_route_to_string, + .platform_route_get_all = nm_platform_ip6_route_get_all, + .platform_route_delete_default = _v6_platform_route_delete_default, + .route_metric_normalize = nm_utils_ip6_route_metric_normalize, +}; + +/***********************************************************************************/ + +NMDefaultRouteManager * +nm_default_route_manager_get () +{ + if (G_UNLIKELY (!_instance)) { + _instance = NM_DEFAULT_ROUTE_MANAGER (g_object_new (NM_TYPE_DEFAULT_ROUTE_MANAGER, NULL)); + g_object_add_weak_pointer (G_OBJECT (_instance), (gpointer *) &_instance); + } + return _instance; +} + +/***********************************************************************************/ + +static gboolean +_resync_idle_now (NMDefaultRouteManager *self) +{ + gboolean has_v4_changes, has_v6_changes; + gboolean changed = FALSE; + + NMDefaultRouteManagerPrivate *priv = NM_DEFAULT_ROUTE_MANAGER_GET_PRIVATE (self); + + has_v4_changes = priv->resync.has_v4_changes; + has_v6_changes = priv->resync.has_v6_changes; + + _LOGD (0, "resync: sync now (%u) (IPv4 changes: %s, IPv6 changes: %s)", priv->resync.idle_handle, + has_v4_changes ? "yes" : "no", has_v6_changes ? "yes" : "no"); + + priv->resync.has_v4_changes = FALSE; + priv->resync.has_v6_changes = FALSE; + priv->resync.idle_handle = 0; + priv->resync.backoff_wait_time_ms = + priv->resync.backoff_wait_time_ms == 0 + ? 100 + : priv->resync.backoff_wait_time_ms * 2; + + if (has_v4_changes) + changed |= _resync_all (&vtable_ip4, self, NULL, NULL, TRUE); + + if (has_v6_changes) + changed |= _resync_all (&vtable_ip6, self, NULL, NULL, TRUE); + + if (!changed) { + /* Nothing changed: reset the backoff wait time */ + _resync_idle_cancel (self); + } + + return G_SOURCE_REMOVE; +} + +static void +_resync_idle_cancel (NMDefaultRouteManager *self) +{ + NMDefaultRouteManagerPrivate *priv = NM_DEFAULT_ROUTE_MANAGER_GET_PRIVATE (self); + + if (priv->resync.idle_handle) { + _LOGD (0, "resync: cancelled (%u)", priv->resync.idle_handle); + g_source_remove (priv->resync.idle_handle); + priv->resync.idle_handle = 0; + } + priv->resync.backoff_wait_time_ms = 0; + priv->resync.has_v4_changes = FALSE; + priv->resync.has_v6_changes = FALSE; +} + +static void +_resync_idle_reschedule (NMDefaultRouteManager *self) +{ + NMDefaultRouteManagerPrivate *priv = NM_DEFAULT_ROUTE_MANAGER_GET_PRIVATE (self); + + /* since we react on external changes and readd/remove default routes for + * the interfaces we manage, there could be the erronous situation where two applications + * fight over a certain default route. + * Avoid this, by increasingly wait longer to touch the system (backoff wait time). */ + + if (priv->resync.backoff_wait_time_ms == 0) { + /* for scheduling idle, always reschedule (to process all other events first) */ + if (priv->resync.idle_handle) + g_source_remove (priv->resync.idle_handle); + else + _LOGD (0, "resync: schedule on idle"); + priv->resync.idle_handle = g_idle_add ((GSourceFunc) _resync_idle_now, self); + } else if (!priv->resync.idle_handle) { + priv->resync.idle_handle = g_timeout_add (priv->resync.backoff_wait_time_ms, (GSourceFunc) _resync_idle_now, self); + _LOGD (0, "resync: schedule in %u.%03u seconds (%u)", priv->resync.backoff_wait_time_ms/1000, + priv->resync.backoff_wait_time_ms%1000, priv->resync.idle_handle); + } +} + +static void +_platform_ipx_route_changed_cb (const VTableIP *vtable, + NMDefaultRouteManager *self, + const NMPlatformIPRoute *route) +{ + NMDefaultRouteManagerPrivate *priv; + + if (route && !NM_PLATFORM_IP_ROUTE_IS_DEFAULT (route)) { + /* we only care about address changes or changes of default route. */ + return; + } + + priv = NM_DEFAULT_ROUTE_MANAGER_GET_PRIVATE (self); + + if (priv->resync.guard) { + /* callbacks while executing _resync_all() are ignored. */ + return; + } + + if (VTABLE_IS_IP4) + priv->resync.has_v4_changes = TRUE; + else + priv->resync.has_v6_changes = TRUE; + + _resync_idle_reschedule (self); +} + +static void +_platform_ip4_address_changed_cb (NMPlatform *platform, + int ifindex, + gpointer platform_object, + NMPlatformSignalChangeType change_type, + NMPlatformReason reason, + NMDefaultRouteManager *self) +{ + _platform_ipx_route_changed_cb (&vtable_ip4, self, NULL); +} + +static void +_platform_ip6_address_changed_cb (NMPlatform *platform, + int ifindex, + gpointer platform_object, + NMPlatformSignalChangeType change_type, + NMPlatformReason reason, + NMDefaultRouteManager *self) +{ + _platform_ipx_route_changed_cb (&vtable_ip6, self, NULL); +} + +static void +_platform_ip4_route_changed_cb (NMPlatform *platform, + int ifindex, + gpointer platform_object, + NMPlatformSignalChangeType change_type, + NMPlatformReason reason, + NMDefaultRouteManager *self) +{ + _platform_ipx_route_changed_cb (&vtable_ip4, self, platform_object); +} + +static void +_platform_ip6_route_changed_cb (NMPlatform *platform, + int ifindex, + gpointer platform_object, + NMPlatformSignalChangeType change_type, + NMPlatformReason reason, + NMDefaultRouteManager *self) +{ + _platform_ipx_route_changed_cb (&vtable_ip6, self, platform_object); +} + +/***********************************************************************************/ + +static void +nm_default_route_manager_init (NMDefaultRouteManager *self) +{ + NMDefaultRouteManagerPrivate *priv = NM_DEFAULT_ROUTE_MANAGER_GET_PRIVATE (self); + NMPlatform *platform; + + priv->entries_ip4 = g_ptr_array_new_full (0, (GDestroyNotify) _entry_free); + priv->entries_ip6 = g_ptr_array_new_full (0, (GDestroyNotify) _entry_free); + + platform = nm_platform_get (); + g_signal_connect (platform, NM_PLATFORM_SIGNAL_IP4_ADDRESS_CHANGED, G_CALLBACK (_platform_ip4_address_changed_cb), self); + g_signal_connect (platform, NM_PLATFORM_SIGNAL_IP6_ADDRESS_CHANGED, G_CALLBACK (_platform_ip6_address_changed_cb), self); + g_signal_connect (platform, NM_PLATFORM_SIGNAL_IP4_ROUTE_CHANGED, G_CALLBACK (_platform_ip4_route_changed_cb), self); + g_signal_connect (platform, NM_PLATFORM_SIGNAL_IP6_ROUTE_CHANGED, G_CALLBACK (_platform_ip6_route_changed_cb), self); +} + +static void +dispose (GObject *object) +{ + NMDefaultRouteManager *self = NM_DEFAULT_ROUTE_MANAGER (object); + NMDefaultRouteManagerPrivate *priv = NM_DEFAULT_ROUTE_MANAGER_GET_PRIVATE (self); + + if (priv->entries_ip4) { + g_ptr_array_free (priv->entries_ip4, TRUE); + priv->entries_ip4 = NULL; + } + if (priv->entries_ip6) { + g_ptr_array_free (priv->entries_ip6, TRUE); + priv->entries_ip6 = NULL; + } + + _resync_idle_cancel (self); + + g_signal_handlers_disconnect_by_data (nm_platform_get (), self); + + G_OBJECT_CLASS (nm_default_route_manager_parent_class)->dispose (object); +} + +static void +nm_default_route_manager_class_init (NMDefaultRouteManagerClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + + g_type_class_add_private (klass, sizeof (NMDefaultRouteManagerPrivate)); + + /* virtual methods */ + object_class->dispose = dispose; +} + |