NetworkManager/src/ndisc/nm-ndisc.c
Thomas Haller f0d40525df device: support dynamic "connection.stable-id" in form of text-substitution
Usecase: when connecting to a public Wi-Fi with MAC address randomization
("wifi.cloned-mac-address=random") you get on every re-connect a new
IP address due to the changing MAC address.
"wifi.cloned-mac-address=stable" is the solution for that. But that
means, every time when reconnecting to this network, the same ID will
be reused. We want an ID that is stable for a while, but at a later
point a new ID should e generated when revisiting the Wi-Fi network.

Extend the stable-id to become dynamic and support templates/substitutions.
Currently supported is "${CONNECTION}", "${BOOT}" and "${RANDOM}".
Any unrecognized pattern is treated verbaim/untranslated.

"$$" is treated special to allow escaping the '$' character. This allows
the user to still embed verbatim '$' characters with the guarantee that
future versions of NetworkManager will still generate the same ID.
Of course, a user could just avoid '$' in the stable-id unless using
it for dynamic substitutions.

Later we might want to add more recognized substitutions. For example, it
could be useful to generate new IDs based on the current time. The ${} syntax
is extendable to support arguments like "${PERIODIC:weekly}".

Also allow "connection.stable-id" to be set as global default value.
Previously that made no sense because the stable-id was static
and is anyway strongly tied to the identity of the connection profile.
Now, with dynamic stable-ids it gets much more useful to specify
a global default.

Note that pre-existing stable-ids don't change and still generate
the same addresses -- unless they contain one of the new ${} patterns.
2017-01-09 14:50:33 +01:00

1235 lines
35 KiB
C

/* -*- Mode: C; tab-width: 4; indent-tabs-mode: t; c-basic-offset: 4 -*- */
/* nm-ndisc.c - Perform IPv6 neighbor discovery
*
* 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, 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) 2013 Red Hat, Inc.
*/
#include "nm-default.h"
#include "nm-ndisc.h"
#include <stdlib.h>
#include <arpa/inet.h>
#include <string.h>
#include "nm-setting-ip6-config.h"
#include "nm-ndisc-private.h"
#include "nm-utils.h"
#include "platform/nm-platform.h"
#include "platform/nmp-netns.h"
#define _NMLOG_PREFIX_NAME "ndisc"
/*****************************************************************************/
struct _NMNDiscPrivate {
/* this *must* be the first field. */
NMNDiscDataInternal rdata;
union {
gint32 solicitations_left;
gint32 announcements_left;
};
union {
guint send_rs_id;
guint send_ra_id;
};
union {
gint32 last_rs;
gint32 last_ra;
};
guint ra_timeout_id; /* first RA timeout */
guint timeout_id; /* prefix/dns/etc lifetime timeout */
char *last_error;
NMUtilsIPv6IfaceId iid;
/* immutable values: */
int ifindex;
char *ifname;
char *network_id;
NMSettingIP6ConfigAddrGenMode addr_gen_mode;
NMUtilsStableType stable_type;
gint32 max_addresses;
gint32 router_solicitations;
gint32 router_solicitation_interval;
NMNDiscNodeType node_type;
NMPlatform *platform;
NMPNetns *netns;
};
typedef struct _NMNDiscPrivate NMNDiscPrivate;
NM_GOBJECT_PROPERTIES_DEFINE_BASE (
PROP_PLATFORM,
PROP_IFINDEX,
PROP_IFNAME,
PROP_STABLE_TYPE,
PROP_NETWORK_ID,
PROP_ADDR_GEN_MODE,
PROP_MAX_ADDRESSES,
PROP_ROUTER_SOLICITATIONS,
PROP_ROUTER_SOLICITATION_INTERVAL,
PROP_NODE_TYPE,
);
enum {
CONFIG_CHANGED,
RA_TIMEOUT,
LAST_SIGNAL
};
static guint signals[LAST_SIGNAL] = { 0 };
G_DEFINE_TYPE (NMNDisc, nm_ndisc, G_TYPE_OBJECT)
#define NM_NDISC_GET_PRIVATE(self) _NM_GET_PRIVATE_PTR(self, NMNDisc, NM_IS_NDISC)
/*****************************************************************************/
static void _config_changed_log (NMNDisc *ndisc, NMNDiscConfigMap changed);
/*****************************************************************************/
NMPNetns *
nm_ndisc_netns_get (NMNDisc *self)
{
g_return_val_if_fail (NM_IS_NDISC (self), NULL);
return NM_NDISC_GET_PRIVATE (self)->netns;
}
gboolean
nm_ndisc_netns_push (NMNDisc *self, NMPNetns **netns)
{
NMNDiscPrivate *priv;
g_return_val_if_fail (NM_IS_NDISC (self), FALSE);
priv = NM_NDISC_GET_PRIVATE (self);
if ( priv->netns
&& !nmp_netns_push (priv->netns)) {
NM_SET_OUT (netns, NULL);
return FALSE;
}
NM_SET_OUT (netns, priv->netns);
return TRUE;
}
/*****************************************************************************/
int
nm_ndisc_get_ifindex (NMNDisc *self)
{
g_return_val_if_fail (NM_IS_NDISC (self), 0);
return NM_NDISC_GET_PRIVATE (self)->ifindex;
}
const char *
nm_ndisc_get_ifname (NMNDisc *self)
{
g_return_val_if_fail (NM_IS_NDISC (self), NULL);
return NM_NDISC_GET_PRIVATE (self)->ifname;
}
NMNDiscNodeType
nm_ndisc_get_node_type (NMNDisc *self)
{
g_return_val_if_fail (NM_IS_NDISC (self), NM_NDISC_NODE_TYPE_INVALID);
return NM_NDISC_GET_PRIVATE (self)->node_type;
}
/*****************************************************************************/
static const NMNDiscData *
_data_complete (NMNDiscDataInternal *data)
{
#define _SET(data, field) \
G_STMT_START { \
if ((data->public.field##_n = data->field->len) > 0) \
data->public.field = (gpointer) data->field->data; \
else \
data->public.field = NULL; \
} G_STMT_END
_SET (data, gateways);
_SET (data, addresses);
_SET (data, routes);
_SET (data, dns_servers);
_SET (data, dns_domains);
#undef _SET
return &data->public;
}
static void
_emit_config_change (NMNDisc *self, NMNDiscConfigMap changed)
{
_config_changed_log (self, changed);
g_signal_emit (self, signals[CONFIG_CHANGED], 0,
_data_complete (&NM_NDISC_GET_PRIVATE (self)->rdata),
(guint) changed);
}
/*****************************************************************************/
gboolean
nm_ndisc_add_gateway (NMNDisc *ndisc, const NMNDiscGateway *new)
{
NMNDiscDataInternal *rdata = &NM_NDISC_GET_PRIVATE(ndisc)->rdata;
int i, insert_idx = -1;
for (i = 0; i < rdata->gateways->len; i++) {
NMNDiscGateway *item = &g_array_index (rdata->gateways, NMNDiscGateway, i);
if (IN6_ARE_ADDR_EQUAL (&item->address, &new->address)) {
if (new->lifetime == 0) {
g_array_remove_index (rdata->gateways, i--);
return TRUE;
}
if (item->preference != new->preference) {
g_array_remove_index (rdata->gateways, i--);
continue;
}
memcpy (item, new, sizeof (*new));
return FALSE;
}
/* Put before less preferable gateways. */
if (item->preference < new->preference && insert_idx < 0)
insert_idx = i;
}
if (new->lifetime)
g_array_insert_val (rdata->gateways, MAX (insert_idx, 0), *new);
return !!new->lifetime;
}
/**
* complete_address:
* @ndisc: the #NMNDisc
* @addr: the #NMNDiscAddress
*
* Adds the host part to the address that has network part set.
* If the address already has a host part, add a different host part
* if possible (this is useful in case DAD failed).
*
* Can fail if a different address can not be generated (DAD failure
* for an EUI-64 address or DAD counter overflow).
*
* Returns: %TRUE if the address could be completed, %FALSE otherwise.
**/
static gboolean
complete_address (NMNDisc *ndisc, NMNDiscAddress *addr)
{
NMNDiscPrivate *priv;
GError *error = NULL;
g_return_val_if_fail (NM_IS_NDISC (ndisc), FALSE);
priv = NM_NDISC_GET_PRIVATE (ndisc);
if (priv->addr_gen_mode == NM_SETTING_IP6_CONFIG_ADDR_GEN_MODE_STABLE_PRIVACY) {
if (!nm_utils_ipv6_addr_set_stable_privacy (priv->stable_type,
&addr->address,
priv->ifname,
priv->network_id,
addr->dad_counter++,
&error)) {
_LOGW ("complete-address: failed to generate an stable-privacy address: %s",
error->message);
g_clear_error (&error);
return FALSE;
}
_LOGD ("complete-address: using an stable-privacy address");
return TRUE;
}
if (!priv->iid.id) {
_LOGW ("complete-address: can't generate an EUI-64 address: no interface identifier");
return FALSE;
}
if (addr->address.s6_addr32[2] == 0x0 && addr->address.s6_addr32[3] == 0x0) {
_LOGD ("complete-address: adding an EUI-64 address");
nm_utils_ipv6_addr_set_interface_identifier (&addr->address, priv->iid);
return TRUE;
}
_LOGW ("complete-address: can't generate a new EUI-64 address");
return FALSE;
}
static gboolean
nm_ndisc_add_address (NMNDisc *ndisc, const NMNDiscAddress *new)
{
NMNDiscPrivate *priv = NM_NDISC_GET_PRIVATE (ndisc);
NMNDiscDataInternal *rdata = &priv->rdata;
int i;
for (i = 0; i < rdata->addresses->len; i++) {
NMNDiscAddress *item = &g_array_index (rdata->addresses, NMNDiscAddress, i);
if (IN6_ARE_ADDR_EQUAL (&item->address, &new->address)) {
gboolean changed;
if (new->lifetime == 0) {
g_array_remove_index (rdata->addresses, i--);
return TRUE;
}
changed = item->timestamp + item->lifetime != new->timestamp + new->lifetime ||
item->timestamp + item->preferred != new->timestamp + new->preferred;
*item = *new;
return changed;
}
}
/* we create at most max_addresses autoconf addresses. This is different from
* what the kernel does, because it considers *all* addresses (including
* static and other temporary addresses).
**/
if (priv->max_addresses && rdata->addresses->len >= priv->max_addresses)
return FALSE;
if (new->lifetime)
g_array_insert_val (rdata->addresses, i, *new);
return !!new->lifetime;
}
gboolean
nm_ndisc_complete_and_add_address (NMNDisc *ndisc, NMNDiscAddress *new)
{
if (!complete_address (ndisc, new))
return FALSE;
return nm_ndisc_add_address (ndisc, new);
}
gboolean
nm_ndisc_add_route (NMNDisc *ndisc, const NMNDiscRoute *new)
{
NMNDiscPrivate *priv;
NMNDiscDataInternal *rdata;
int i, insert_idx = -1;
if (new->plen == 0 || new->plen > 128) {
/* Only expect non-default routes. The router has no idea what the
* local configuration or user preferences are, so sending routes
* with a prefix length of 0 must be ignored by NMNDisc.
*
* Also, upper layers also don't expect that NMNDisc exposes routes
* with a plen or zero or larger then 128.
*/
g_return_val_if_reached (FALSE);
}
priv = NM_NDISC_GET_PRIVATE (ndisc);
rdata = &priv->rdata;
for (i = 0; i < rdata->routes->len; i++) {
NMNDiscRoute *item = &g_array_index (rdata->routes, NMNDiscRoute, i);
if (IN6_ARE_ADDR_EQUAL (&item->network, &new->network) && item->plen == new->plen) {
if (new->lifetime == 0) {
g_array_remove_index (rdata->routes, i--);
return TRUE;
}
if (item->preference != new->preference) {
g_array_remove_index (rdata->routes, i--);
continue;
}
memcpy (item, new, sizeof (*new));
return FALSE;
}
/* Put before less preferable routes. */
if (item->preference < new->preference && insert_idx < 0)
insert_idx = i;
}
if (new->lifetime)
g_array_insert_val (rdata->routes, CLAMP (insert_idx, 0, G_MAXINT), *new);
return !!new->lifetime;
}
gboolean
nm_ndisc_add_dns_server (NMNDisc *ndisc, const NMNDiscDNSServer *new)
{
NMNDiscPrivate *priv;
NMNDiscDataInternal *rdata;
int i;
priv = NM_NDISC_GET_PRIVATE (ndisc);
rdata = &priv->rdata;
for (i = 0; i < rdata->dns_servers->len; i++) {
NMNDiscDNSServer *item = &g_array_index (rdata->dns_servers, NMNDiscDNSServer, i);
if (IN6_ARE_ADDR_EQUAL (&item->address, &new->address)) {
if (new->lifetime == 0) {
g_array_remove_index (rdata->dns_servers, i);
return TRUE;
}
if (item->timestamp != new->timestamp || item->lifetime != new->lifetime) {
*item = *new;
return TRUE;
}
return FALSE;
}
}
if (new->lifetime)
g_array_insert_val (rdata->dns_servers, i, *new);
return !!new->lifetime;
}
/* Copies new->domain if 'new' is added to the dns_domains list */
gboolean
nm_ndisc_add_dns_domain (NMNDisc *ndisc, const NMNDiscDNSDomain *new)
{
NMNDiscPrivate *priv;
NMNDiscDataInternal *rdata;
NMNDiscDNSDomain *item;
int i;
priv = NM_NDISC_GET_PRIVATE (ndisc);
rdata = &priv->rdata;
for (i = 0; i < rdata->dns_domains->len; i++) {
item = &g_array_index (rdata->dns_domains, NMNDiscDNSDomain, i);
if (!g_strcmp0 (item->domain, new->domain)) {
gboolean changed;
if (new->lifetime == 0) {
g_array_remove_index (rdata->dns_domains, i);
return TRUE;
}
changed = (item->timestamp != new->timestamp ||
item->lifetime != new->lifetime);
if (changed) {
item->timestamp = new->timestamp;
item->lifetime = new->lifetime;
}
return changed;
}
}
if (new->lifetime) {
g_array_insert_val (rdata->dns_domains, i, *new);
item = &g_array_index (rdata->dns_domains, NMNDiscDNSDomain, i);
item->domain = g_strdup (new->domain);
}
return !!new->lifetime;
}
/*****************************************************************************/
#define _MAYBE_WARN(...) G_STMT_START { \
gboolean _different_message; \
\
_different_message = g_strcmp0 (priv->last_error, error->message) != 0; \
_NMLOG (_different_message ? LOGL_WARN : LOGL_DEBUG, __VA_ARGS__); \
if (_different_message) { \
g_clear_pointer (&priv->last_error, g_free); \
priv->last_error = g_strdup (error->message); \
} \
} G_STMT_END
static gboolean
send_rs_timeout (NMNDisc *ndisc)
{
nm_auto_pop_netns NMPNetns *netns = NULL;
NMNDiscClass *klass = NM_NDISC_GET_CLASS (ndisc);
NMNDiscPrivate *priv = NM_NDISC_GET_PRIVATE (ndisc);
GError *error = NULL;
priv->send_rs_id = 0;
if (!nm_ndisc_netns_push (ndisc, &netns))
return G_SOURCE_REMOVE;
if (klass->send_rs (ndisc, &error)) {
_LOGD ("router solicitation sent");
priv->solicitations_left--;
g_clear_pointer (&priv->last_error, g_free);
} else {
_MAYBE_WARN ("failure sending router solicitation: %s", error->message);
g_clear_error (&error);
}
priv->last_rs = nm_utils_get_monotonic_timestamp_s ();
if (priv->solicitations_left > 0) {
_LOGD ("scheduling router solicitation retry in %d seconds.",
(int) priv->router_solicitation_interval);
priv->send_rs_id = g_timeout_add_seconds (priv->router_solicitation_interval,
(GSourceFunc) send_rs_timeout, ndisc);
} else {
_LOGD ("did not receive a router advertisement after %d solicitations.",
(int) priv->router_solicitations);
}
return G_SOURCE_REMOVE;
}
static void
solicit_routers (NMNDisc *ndisc)
{
NMNDiscPrivate *priv = NM_NDISC_GET_PRIVATE (ndisc);
gint64 next, now;
if (priv->send_rs_id)
return;
now = nm_utils_get_monotonic_timestamp_s ();
priv->solicitations_left = priv->router_solicitations;
next = (((gint64) priv->last_rs) + priv->router_solicitation_interval) - now;
next = CLAMP (next, 0, G_MAXINT32);
_LOGD ("scheduling explicit router solicitation request in %" G_GINT64_FORMAT " seconds.",
next);
priv->send_rs_id = g_timeout_add_seconds ((guint32) next, (GSourceFunc) send_rs_timeout, ndisc);
}
static gboolean
announce_router (NMNDisc *ndisc)
{
nm_auto_pop_netns NMPNetns *netns = NULL;
NMNDiscClass *klass = NM_NDISC_GET_CLASS (ndisc);
NMNDiscPrivate *priv = NM_NDISC_GET_PRIVATE (ndisc);
GError *error = NULL;
if (!nm_ndisc_netns_push (ndisc, &netns))
return G_SOURCE_REMOVE;
priv->last_ra = nm_utils_get_monotonic_timestamp_s ();
if (klass->send_ra (ndisc, &error)) {
_LOGD ("router advertisement sent");
g_clear_pointer (&priv->last_error, g_free);
} else {
_MAYBE_WARN ("failure sending router advertisement: %s", error->message);
g_clear_error (&error);
}
if (--priv->announcements_left) {
_LOGD ("will resend an initial router advertisement");
/* Schedule next initial announcement retransmit. */
priv->send_ra_id = g_timeout_add_seconds (g_random_int_range (NM_NDISC_ROUTER_ADVERT_DELAY,
NM_NDISC_ROUTER_ADVERT_INITIAL_INTERVAL),
(GSourceFunc) announce_router, ndisc);
} else {
_LOGD ("will send an unsolicited router advertisement");
/* Schedule next unsolicited announcement. */
priv->announcements_left = 1;
priv->send_ra_id = g_timeout_add_seconds (NM_NDISC_ROUTER_ADVERT_MAX_INTERVAL,
(GSourceFunc) announce_router,
ndisc);
}
return G_SOURCE_REMOVE;
}
static void
announce_router_initial (NMNDisc *ndisc)
{
NMNDiscPrivate *priv = NM_NDISC_GET_PRIVATE (ndisc);
_LOGD ("will send an initial router advertisement");
/* Retry three more times. */
priv->announcements_left = NM_NDISC_ROUTER_ADVERTISEMENTS_DEFAULT;
/* Unschedule an unsolicited resend if we are allowed to send now. */
if (G_LIKELY (nm_utils_get_monotonic_timestamp_s () - priv->last_ra > NM_NDISC_ROUTER_ADVERT_DELAY))
nm_clear_g_source (&priv->send_ra_id);
/* Schedule the initial send rather early. Clamp the delay by minimal
* delay and not the initial advert internal so that we start fast. */
if (G_LIKELY (!priv->send_ra_id)) {
priv->send_ra_id = g_timeout_add_seconds (g_random_int_range (0, NM_NDISC_ROUTER_ADVERT_DELAY),
(GSourceFunc) announce_router, ndisc);
}
}
static void
announce_router_solicited (NMNDisc *ndisc)
{
NMNDiscPrivate *priv = NM_NDISC_GET_PRIVATE (ndisc);
_LOGD ("will send an solicited router advertisement");
/* Unschedule an unsolicited resend if we are allowed to send now. */
if (nm_utils_get_monotonic_timestamp_s () - priv->last_ra > NM_NDISC_ROUTER_ADVERT_DELAY)
nm_clear_g_source (&priv->send_ra_id);
if (!priv->send_ra_id) {
priv->send_ra_id = g_timeout_add (g_random_int_range (0, NM_NDISC_ROUTER_ADVERT_DELAY_MS),
(GSourceFunc) announce_router, ndisc);
}
}
/*****************************************************************************/
void
nm_ndisc_set_config (NMNDisc *ndisc,
const GArray *addresses,
const GArray *dns_servers,
const GArray *dns_domains)
{
int changed = FALSE;
guint i;
for (i = 0; i < addresses->len; i++) {
if (nm_ndisc_add_address (ndisc, &g_array_index (addresses, NMNDiscAddress, i)))
changed = TRUE;
}
for (i = 0; i < dns_servers->len; i++) {
if (nm_ndisc_add_dns_server (ndisc, &g_array_index (dns_servers, NMNDiscDNSServer, i)))
changed = TRUE;
}
for (i = 0; i < dns_domains->len; i++) {
if (nm_ndisc_add_dns_domain (ndisc, &g_array_index (dns_domains, NMNDiscDNSDomain, i)))
changed = TRUE;
}
if (changed)
announce_router_initial (ndisc);
}
/**
* nm_ndisc_set_iid:
* @ndisc: the #NMNDisc
* @iid: the new interface ID
*
* Sets the "Modified EUI-64" interface ID to be used when generating
* IPv6 addresses using received prefixes. Identifiers are either generated
* from the hardware addresses or manually set by the operator with
* "ip token" command.
*
* Upon token change (or initial setting) all addresses generated using
* the old identifier are removed. The caller should ensure the addresses
* will be reset by soliciting router advertisements.
*
* In case the stable privacy addressing is used %FALSE is returned and
* addresses are left untouched.
*
* Returns: %TRUE if addresses need to be regenerated, %FALSE otherwise.
**/
gboolean
nm_ndisc_set_iid (NMNDisc *ndisc, const NMUtilsIPv6IfaceId iid)
{
NMNDiscPrivate *priv;
NMNDiscDataInternal *rdata;
g_return_val_if_fail (NM_IS_NDISC (ndisc), FALSE);
priv = NM_NDISC_GET_PRIVATE (ndisc);
rdata = &priv->rdata;
if (priv->iid.id != iid.id) {
priv->iid = iid;
if (priv->addr_gen_mode == NM_SETTING_IP6_CONFIG_ADDR_GEN_MODE_STABLE_PRIVACY)
return FALSE;
if (rdata->addresses->len) {
_LOGD ("IPv6 interface identifier changed, flushing addresses");
g_array_remove_range (rdata->addresses, 0, rdata->addresses->len);
_emit_config_change (ndisc, NM_NDISC_CONFIG_ADDRESSES);
solicit_routers (ndisc);
}
return TRUE;
}
return FALSE;
}
static gboolean
ndisc_ra_timeout_cb (gpointer user_data)
{
NMNDisc *ndisc = NM_NDISC (user_data);
NM_NDISC_GET_PRIVATE (ndisc)->ra_timeout_id = 0;
g_signal_emit (ndisc, signals[RA_TIMEOUT], 0);
return G_SOURCE_REMOVE;
}
void
nm_ndisc_start (NMNDisc *ndisc)
{
nm_auto_pop_netns NMPNetns *netns = NULL;
NMNDiscPrivate *priv = NM_NDISC_GET_PRIVATE (ndisc);
NMNDiscClass *klass = NM_NDISC_GET_CLASS (ndisc);
gint64 ra_wait_secs;
g_return_if_fail (klass->start);
g_return_if_fail (!priv->ra_timeout_id);
_LOGD ("starting neighbor discovery: %d", priv->ifindex);
if (!nm_ndisc_netns_push (ndisc, &netns))
return;
klass->start (ndisc);
switch (priv->node_type) {
case NM_NDISC_NODE_TYPE_HOST:
ra_wait_secs = (((gint64) priv->router_solicitations) * priv->router_solicitation_interval) + 1;
ra_wait_secs = CLAMP (ra_wait_secs, 30, 120);
priv->ra_timeout_id = g_timeout_add_seconds (ra_wait_secs, ndisc_ra_timeout_cb, ndisc);
_LOGD ("scheduling RA timeout in %d seconds", (int) ra_wait_secs);
solicit_routers (ndisc);
break;
case NM_NDISC_NODE_TYPE_ROUTER:
announce_router_initial (ndisc);
break;
default:
g_assert_not_reached ();
}
}
void
nm_ndisc_dad_failed (NMNDisc *ndisc, struct in6_addr *address)
{
NMNDiscDataInternal *rdata;
int i;
gboolean changed = FALSE;
rdata = &NM_NDISC_GET_PRIVATE (ndisc)->rdata;
for (i = 0; i < rdata->addresses->len; i++) {
NMNDiscAddress *item = &g_array_index (rdata->addresses, NMNDiscAddress, i);
if (!IN6_ARE_ADDR_EQUAL (&item->address, address))
continue;
_LOGD ("DAD failed for discovered address %s", nm_utils_inet6_ntop (address, NULL));
if (!complete_address (ndisc, item))
g_array_remove_index (rdata->addresses, i--);
changed = TRUE;
}
if (changed)
_emit_config_change (ndisc, NM_NDISC_CONFIG_ADDRESSES);
}
#define CONFIG_MAP_MAX_STR 7
static void
config_map_to_string (NMNDiscConfigMap map, char *p)
{
if (map & NM_NDISC_CONFIG_DHCP_LEVEL)
*p++ = 'd';
if (map & NM_NDISC_CONFIG_GATEWAYS)
*p++ = 'G';
if (map & NM_NDISC_CONFIG_ADDRESSES)
*p++ = 'A';
if (map & NM_NDISC_CONFIG_ROUTES)
*p++ = 'R';
if (map & NM_NDISC_CONFIG_DNS_SERVERS)
*p++ = 'S';
if (map & NM_NDISC_CONFIG_DNS_DOMAINS)
*p++ = 'D';
*p = '\0';
}
static const char *
dhcp_level_to_string (NMNDiscDHCPLevel dhcp_level)
{
switch (dhcp_level) {
case NM_NDISC_DHCP_LEVEL_NONE:
return "none";
case NM_NDISC_DHCP_LEVEL_OTHERCONF:
return "otherconf";
case NM_NDISC_DHCP_LEVEL_MANAGED:
return "managed";
default:
return "INVALID";
}
}
#define expiry(item) (item->timestamp + item->lifetime)
static void
_config_changed_log (NMNDisc *ndisc, NMNDiscConfigMap changed)
{
NMNDiscPrivate *priv;
NMNDiscDataInternal *rdata;
int i;
char changedstr[CONFIG_MAP_MAX_STR];
char addrstr[INET6_ADDRSTRLEN];
if (!_LOGD_ENABLED ())
return;
priv = NM_NDISC_GET_PRIVATE (ndisc);
rdata = &priv->rdata;
config_map_to_string (changed, changedstr);
_LOGD ("neighbor discovery configuration changed [%s]:", changedstr);
_LOGD (" dhcp-level %s", dhcp_level_to_string (priv->rdata.public.dhcp_level));
for (i = 0; i < rdata->gateways->len; i++) {
NMNDiscGateway *gateway = &g_array_index (rdata->gateways, NMNDiscGateway, i);
inet_ntop (AF_INET6, &gateway->address, addrstr, sizeof (addrstr));
_LOGD (" gateway %s pref %d exp %u", addrstr, gateway->preference, expiry (gateway));
}
for (i = 0; i < rdata->addresses->len; i++) {
NMNDiscAddress *address = &g_array_index (rdata->addresses, NMNDiscAddress, i);
inet_ntop (AF_INET6, &address->address, addrstr, sizeof (addrstr));
_LOGD (" address %s exp %u", addrstr, expiry (address));
}
for (i = 0; i < rdata->routes->len; i++) {
NMNDiscRoute *route = &g_array_index (rdata->routes, NMNDiscRoute, i);
inet_ntop (AF_INET6, &route->network, addrstr, sizeof (addrstr));
_LOGD (" route %s/%d via %s pref %d exp %u", addrstr, (int) route->plen,
nm_utils_inet6_ntop (&route->gateway, NULL), route->preference,
expiry (route));
}
for (i = 0; i < rdata->dns_servers->len; i++) {
NMNDiscDNSServer *dns_server = &g_array_index (rdata->dns_servers, NMNDiscDNSServer, i);
inet_ntop (AF_INET6, &dns_server->address, addrstr, sizeof (addrstr));
_LOGD (" dns_server %s exp %u", addrstr, expiry (dns_server));
}
for (i = 0; i < rdata->dns_domains->len; i++) {
NMNDiscDNSDomain *dns_domain = &g_array_index (rdata->dns_domains, NMNDiscDNSDomain, i);
_LOGD (" dns_domain %s exp %u", dns_domain->domain, expiry (dns_domain));
}
}
static void
clean_gateways (NMNDisc *ndisc, guint32 now, NMNDiscConfigMap *changed, guint32 *nextevent)
{
NMNDiscDataInternal *rdata;
guint i;
rdata = &NM_NDISC_GET_PRIVATE (ndisc)->rdata;
for (i = 0; i < rdata->gateways->len; i++) {
NMNDiscGateway *item = &g_array_index (rdata->gateways, NMNDiscGateway, i);
guint64 expiry = (guint64) item->timestamp + item->lifetime;
if (item->lifetime == G_MAXUINT32)
continue;
if (now >= expiry) {
g_array_remove_index (rdata->gateways, i--);
*changed |= NM_NDISC_CONFIG_GATEWAYS;
} else if (*nextevent > expiry)
*nextevent = expiry;
}
}
static void
clean_addresses (NMNDisc *ndisc, guint32 now, NMNDiscConfigMap *changed, guint32 *nextevent)
{
NMNDiscDataInternal *rdata;
guint i;
rdata = &NM_NDISC_GET_PRIVATE (ndisc)->rdata;
for (i = 0; i < rdata->addresses->len; i++) {
NMNDiscAddress *item = &g_array_index (rdata->addresses, NMNDiscAddress, i);
guint64 expiry = (guint64) item->timestamp + item->lifetime;
if (item->lifetime == G_MAXUINT32)
continue;
if (now >= expiry) {
g_array_remove_index (rdata->addresses, i--);
*changed |= NM_NDISC_CONFIG_ADDRESSES;
} else if (*nextevent > expiry)
*nextevent = expiry;
}
}
static void
clean_routes (NMNDisc *ndisc, guint32 now, NMNDiscConfigMap *changed, guint32 *nextevent)
{
NMNDiscDataInternal *rdata;
guint i;
rdata = &NM_NDISC_GET_PRIVATE (ndisc)->rdata;
for (i = 0; i < rdata->routes->len; i++) {
NMNDiscRoute *item = &g_array_index (rdata->routes, NMNDiscRoute, i);
guint64 expiry = (guint64) item->timestamp + item->lifetime;
if (item->lifetime == G_MAXUINT32)
continue;
if (now >= expiry) {
g_array_remove_index (rdata->routes, i--);
*changed |= NM_NDISC_CONFIG_ROUTES;
} else if (*nextevent > expiry)
*nextevent = expiry;
}
}
static void
clean_dns_servers (NMNDisc *ndisc, guint32 now, NMNDiscConfigMap *changed, guint32 *nextevent)
{
NMNDiscDataInternal *rdata;
guint i;
rdata = &NM_NDISC_GET_PRIVATE (ndisc)->rdata;
for (i = 0; i < rdata->dns_servers->len; i++) {
NMNDiscDNSServer *item = &g_array_index (rdata->dns_servers, NMNDiscDNSServer, i);
guint64 expiry = (guint64) item->timestamp + item->lifetime;
guint64 refresh = (guint64) item->timestamp + item->lifetime / 2;
if (item->lifetime == G_MAXUINT32)
continue;
if (now >= expiry) {
g_array_remove_index (rdata->dns_servers, i--);
*changed |= NM_NDISC_CONFIG_DNS_SERVERS;
} else if (now >= refresh)
solicit_routers (ndisc);
else if (*nextevent > refresh)
*nextevent = refresh;
}
}
static void
clean_dns_domains (NMNDisc *ndisc, guint32 now, NMNDiscConfigMap *changed, guint32 *nextevent)
{
NMNDiscDataInternal *rdata;
guint i;
rdata = &NM_NDISC_GET_PRIVATE (ndisc)->rdata;
for (i = 0; i < rdata->dns_domains->len; i++) {
NMNDiscDNSDomain *item = &g_array_index (rdata->dns_domains, NMNDiscDNSDomain, i);
guint64 expiry = (guint64) item->timestamp + item->lifetime;
guint64 refresh = (guint64) item->timestamp + item->lifetime / 2;
if (item->lifetime == G_MAXUINT32)
continue;
if (now >= expiry) {
g_array_remove_index (rdata->dns_domains, i--);
*changed |= NM_NDISC_CONFIG_DNS_DOMAINS;
} else if (now >= refresh)
solicit_routers (ndisc);
else if (*nextevent > refresh)
*nextevent = refresh;
}
}
static gboolean timeout_cb (gpointer user_data);
static void
check_timestamps (NMNDisc *ndisc, guint32 now, NMNDiscConfigMap changed)
{
NMNDiscPrivate *priv = NM_NDISC_GET_PRIVATE (ndisc);
/* Use a magic date in the distant future (~68 years) */
guint32 never = G_MAXINT32;
guint32 nextevent = never;
nm_clear_g_source (&priv->timeout_id);
clean_gateways (ndisc, now, &changed, &nextevent);
clean_addresses (ndisc, now, &changed, &nextevent);
clean_routes (ndisc, now, &changed, &nextevent);
clean_dns_servers (ndisc, now, &changed, &nextevent);
clean_dns_domains (ndisc, now, &changed, &nextevent);
if (changed)
_emit_config_change (ndisc, changed);
if (nextevent != never) {
g_return_if_fail (nextevent > now);
_LOGD ("scheduling next now/lifetime check: %u seconds",
nextevent - now);
priv->timeout_id = g_timeout_add_seconds (nextevent - now, timeout_cb, ndisc);
}
}
static gboolean
timeout_cb (gpointer user_data)
{
NMNDisc *self = user_data;
NM_NDISC_GET_PRIVATE (self)->timeout_id = 0;
check_timestamps (self, nm_utils_get_monotonic_timestamp_s (), 0);
return G_SOURCE_REMOVE;
}
void
nm_ndisc_ra_received (NMNDisc *ndisc, guint32 now, NMNDiscConfigMap changed)
{
NMNDiscPrivate *priv = NM_NDISC_GET_PRIVATE (ndisc);
nm_clear_g_source (&priv->ra_timeout_id);
nm_clear_g_source (&priv->send_rs_id);
g_clear_pointer (&priv->last_error, g_free);
check_timestamps (ndisc, now, changed);
}
void
nm_ndisc_rs_received (NMNDisc *ndisc)
{
NMNDiscPrivate *priv = NM_NDISC_GET_PRIVATE (ndisc);
g_clear_pointer (&priv->last_error, g_free);
announce_router_solicited (ndisc);
}
/*****************************************************************************/
static void
dns_domain_free (gpointer data)
{
g_free (((NMNDiscDNSDomain *)(data))->domain);
}
static void
set_property (GObject *object, guint prop_id,
const GValue *value, GParamSpec *pspec)
{
NMNDisc *self = NM_NDISC (object);
NMNDiscPrivate *priv = NM_NDISC_GET_PRIVATE (self);
switch (prop_id) {
case PROP_PLATFORM:
/* construct-only */
priv->platform = g_value_get_object (value) ? : NM_PLATFORM_GET;
if (!priv->platform)
g_return_if_reached ();
g_object_ref (priv->platform);
priv->netns = nm_platform_netns_get (priv->platform);
if (priv->netns)
g_object_ref (priv->netns);
g_return_if_fail (!priv->netns || priv->netns == nmp_netns_get_current ());
break;
case PROP_IFINDEX:
/* construct-only */
priv->ifindex = g_value_get_int (value);
g_return_if_fail (priv->ifindex > 0);
break;
case PROP_IFNAME:
/* construct-only */
priv->ifname = g_value_dup_string (value);
g_return_if_fail (priv->ifname && priv->ifname[0]);
break;
case PROP_STABLE_TYPE:
/* construct-only */
priv->stable_type = g_value_get_int (value);
break;
case PROP_NETWORK_ID:
/* construct-only */
priv->network_id = g_value_dup_string (value);
g_return_if_fail (priv->network_id);
break;
case PROP_ADDR_GEN_MODE:
/* construct-only */
priv->addr_gen_mode = g_value_get_int (value);
break;
case PROP_MAX_ADDRESSES:
/* construct-only */
priv->max_addresses = g_value_get_int (value);
break;
case PROP_ROUTER_SOLICITATIONS:
/* construct-only */
priv->router_solicitations = g_value_get_int (value);
break;
case PROP_ROUTER_SOLICITATION_INTERVAL:
/* construct-only */
priv->router_solicitation_interval = g_value_get_int (value);
break;
case PROP_NODE_TYPE:
/* construct-only */
priv->node_type = g_value_get_int (value);
break;
default:
G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
break;
}
}
static void
nm_ndisc_init (NMNDisc *ndisc)
{
NMNDiscPrivate *priv;
NMNDiscDataInternal *rdata;
priv = G_TYPE_INSTANCE_GET_PRIVATE (ndisc, NM_TYPE_NDISC, NMNDiscPrivate);
ndisc->_priv = priv;
rdata = &priv->rdata;
rdata->gateways = g_array_new (FALSE, FALSE, sizeof (NMNDiscGateway));
rdata->addresses = g_array_new (FALSE, FALSE, sizeof (NMNDiscAddress));
rdata->routes = g_array_new (FALSE, FALSE, sizeof (NMNDiscRoute));
rdata->dns_servers = g_array_new (FALSE, FALSE, sizeof (NMNDiscDNSServer));
rdata->dns_domains = g_array_new (FALSE, FALSE, sizeof (NMNDiscDNSDomain));
g_array_set_clear_func (rdata->dns_domains, dns_domain_free);
priv->rdata.public.hop_limit = 64;
/* Start at very low number so that last_rs - router_solicitation_interval
* is much lower than nm_utils_get_monotonic_timestamp_s() at startup.
*/
priv->last_rs = G_MININT32;
}
static void
dispose (GObject *object)
{
NMNDisc *ndisc = NM_NDISC (object);
NMNDiscPrivate *priv = NM_NDISC_GET_PRIVATE (ndisc);
nm_clear_g_source (&priv->ra_timeout_id);
nm_clear_g_source (&priv->send_rs_id);
nm_clear_g_source (&priv->send_ra_id);
g_clear_pointer (&priv->last_error, g_free);
nm_clear_g_source (&priv->timeout_id);
G_OBJECT_CLASS (nm_ndisc_parent_class)->dispose (object);
}
static void
finalize (GObject *object)
{
NMNDisc *ndisc = NM_NDISC (object);
NMNDiscPrivate *priv = NM_NDISC_GET_PRIVATE (ndisc);
NMNDiscDataInternal *rdata = &priv->rdata;
g_free (priv->ifname);
g_free (priv->network_id);
g_array_unref (rdata->gateways);
g_array_unref (rdata->addresses);
g_array_unref (rdata->routes);
g_array_unref (rdata->dns_servers);
g_array_unref (rdata->dns_domains);
g_clear_object (&priv->netns);
g_clear_object (&priv->platform);
G_OBJECT_CLASS (nm_ndisc_parent_class)->finalize (object);
}
static void
nm_ndisc_class_init (NMNDiscClass *klass)
{
GObjectClass *object_class = G_OBJECT_CLASS (klass);
g_type_class_add_private (klass, sizeof (NMNDiscPrivate));
object_class->set_property = set_property;
object_class->dispose = dispose;
object_class->finalize = finalize;
obj_properties[PROP_PLATFORM] =
g_param_spec_object (NM_NDISC_PLATFORM, "", "",
NM_TYPE_PLATFORM,
G_PARAM_WRITABLE |
G_PARAM_CONSTRUCT_ONLY |
G_PARAM_STATIC_STRINGS);
obj_properties[PROP_IFINDEX] =
g_param_spec_int (NM_NDISC_IFINDEX, "", "",
0, G_MAXINT, 0,
G_PARAM_WRITABLE |
G_PARAM_CONSTRUCT_ONLY |
G_PARAM_STATIC_STRINGS);
obj_properties[PROP_IFNAME] =
g_param_spec_string (NM_NDISC_IFNAME, "", "",
NULL,
G_PARAM_WRITABLE |
G_PARAM_CONSTRUCT_ONLY |
G_PARAM_STATIC_STRINGS);
obj_properties[PROP_STABLE_TYPE] =
g_param_spec_int (NM_NDISC_STABLE_TYPE, "", "",
NM_UTILS_STABLE_TYPE_UUID, NM_UTILS_STABLE_TYPE_RANDOM, NM_UTILS_STABLE_TYPE_UUID,
G_PARAM_WRITABLE |
G_PARAM_CONSTRUCT_ONLY |
G_PARAM_STATIC_STRINGS);
obj_properties[PROP_NETWORK_ID] =
g_param_spec_string (NM_NDISC_NETWORK_ID, "", "",
NULL,
G_PARAM_WRITABLE |
G_PARAM_CONSTRUCT_ONLY |
G_PARAM_STATIC_STRINGS);
obj_properties[PROP_ADDR_GEN_MODE] =
g_param_spec_int (NM_NDISC_ADDR_GEN_MODE, "", "",
NM_SETTING_IP6_CONFIG_ADDR_GEN_MODE_EUI64, NM_SETTING_IP6_CONFIG_ADDR_GEN_MODE_STABLE_PRIVACY, NM_SETTING_IP6_CONFIG_ADDR_GEN_MODE_EUI64,
G_PARAM_WRITABLE |
G_PARAM_CONSTRUCT_ONLY |
G_PARAM_STATIC_STRINGS);
obj_properties[PROP_MAX_ADDRESSES] =
g_param_spec_int (NM_NDISC_MAX_ADDRESSES, "", "",
0, G_MAXINT32, NM_NDISC_MAX_ADDRESSES_DEFAULT,
G_PARAM_WRITABLE |
G_PARAM_CONSTRUCT_ONLY |
G_PARAM_STATIC_STRINGS);
obj_properties[PROP_ROUTER_SOLICITATIONS] =
g_param_spec_int (NM_NDISC_ROUTER_SOLICITATIONS, "", "",
1, G_MAXINT32, NM_NDISC_ROUTER_SOLICITATIONS_DEFAULT,
G_PARAM_WRITABLE |
G_PARAM_CONSTRUCT_ONLY |
G_PARAM_STATIC_STRINGS);
obj_properties[PROP_ROUTER_SOLICITATION_INTERVAL] =
g_param_spec_int (NM_NDISC_ROUTER_SOLICITATION_INTERVAL, "", "",
1, G_MAXINT32, NM_NDISC_ROUTER_SOLICITATION_INTERVAL_DEFAULT,
G_PARAM_WRITABLE |
G_PARAM_CONSTRUCT_ONLY |
G_PARAM_STATIC_STRINGS);
obj_properties[PROP_NODE_TYPE] =
g_param_spec_int (NM_NDISC_NODE_TYPE, "", "",
NM_NDISC_NODE_TYPE_INVALID, NM_NDISC_NODE_TYPE_ROUTER, NM_NDISC_NODE_TYPE_INVALID,
G_PARAM_WRITABLE |
G_PARAM_CONSTRUCT_ONLY |
G_PARAM_STATIC_STRINGS);
g_object_class_install_properties (object_class, _PROPERTY_ENUMS_LAST, obj_properties);
signals[CONFIG_CHANGED] =
g_signal_new (NM_NDISC_CONFIG_RECEIVED,
G_OBJECT_CLASS_TYPE (klass),
G_SIGNAL_RUN_FIRST,
0,
NULL, NULL, NULL,
G_TYPE_NONE, 2, G_TYPE_POINTER, G_TYPE_UINT);
signals[RA_TIMEOUT] =
g_signal_new (NM_NDISC_RA_TIMEOUT,
G_OBJECT_CLASS_TYPE (klass),
G_SIGNAL_RUN_FIRST,
0,
NULL, NULL, NULL,
G_TYPE_NONE, 0);
}