summary refs log tree commit diff
path: root/src/ndisc/nm-lndp-ndisc.c
diff options
context:
space:
mode:
Diffstat (limited to 'src/ndisc/nm-lndp-ndisc.c')
-rw-r--r--src/ndisc/nm-lndp-ndisc.c629
1 files changed, 629 insertions, 0 deletions
diff --git a/src/ndisc/nm-lndp-ndisc.c b/src/ndisc/nm-lndp-ndisc.c
new file mode 100644
index 00000000..3bc1590e
--- /dev/null
+++ b/src/ndisc/nm-lndp-ndisc.c
@@ -0,0 +1,629 @@
+/* -*- Mode: C; tab-width: 4; indent-tabs-mode: t; c-basic-offset: 4 -*- */
+/* nm-lndp-ndisc.c - Router discovery implementation using libndp
+ *
+ * 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-lndp-ndisc.h"
+
+#include <string.h>
+#include <arpa/inet.h>
+#include <netinet/icmp6.h>
+/* stdarg.h included because of a bug in ndp.h */
+#include <stdarg.h>
+#include <ndp.h>
+
+#include "nm-ndisc-private.h"
+#include "NetworkManagerUtils.h"
+#include "platform/nm-platform.h"
+#include "platform/nmp-netns.h"
+
+#define _NMLOG_PREFIX_NAME                "ndisc-lndp"
+
+/*****************************************************************************/
+
+typedef struct {
+	struct ndp *ndp;
+
+	GIOChannel *event_channel;
+	guint event_id;
+} NMLndpNDiscPrivate;
+
+/*****************************************************************************/
+
+struct _NMLndpNDisc {
+	NMNDisc parent;
+	NMLndpNDiscPrivate _priv;
+};
+
+struct _NMLndpNDiscClass {
+	NMNDiscClass parent;
+};
+
+/*****************************************************************************/
+
+G_DEFINE_TYPE (NMLndpNDisc, nm_lndp_ndisc, NM_TYPE_NDISC)
+
+#define NM_LNDP_NDISC_GET_PRIVATE(self) _NM_GET_PRIVATE(self, NMLndpNDisc, NM_IS_LNDP_NDISC)
+
+/*****************************************************************************/
+
+static gboolean
+send_rs (NMNDisc *ndisc, GError **error)
+{
+	NMLndpNDiscPrivate *priv = NM_LNDP_NDISC_GET_PRIVATE ((NMLndpNDisc *) ndisc);
+	struct ndp_msg *msg;
+	int errsv;
+
+	errsv = ndp_msg_new (&msg, NDP_MSG_RS);
+	if (errsv) {
+		errsv = errsv > 0 ? errsv : -errsv;
+		g_set_error_literal (error, NM_UTILS_ERROR, NM_UTILS_ERROR_UNKNOWN,
+		                     "cannot create router solicitation");
+		return FALSE;
+	}
+	ndp_msg_ifindex_set (msg, nm_ndisc_get_ifindex (ndisc));
+
+	errsv = ndp_msg_send (priv->ndp, msg);
+	ndp_msg_destroy (msg);
+	if (errsv) {
+		errsv = errsv > 0 ? errsv : -errsv;
+		g_set_error (error, NM_UTILS_ERROR, NM_UTILS_ERROR_UNKNOWN,
+		             "%s (%d)",
+		             g_strerror (errsv), errsv);
+		return FALSE;
+	}
+
+	return TRUE;
+}
+
+_NM_UTILS_LOOKUP_DEFINE (static, translate_preference, enum ndp_route_preference, NMNDiscPreference,
+	NM_UTILS_LOOKUP_DEFAULT (NM_NDISC_PREFERENCE_INVALID),
+	NM_UTILS_LOOKUP_ITEM (NDP_ROUTE_PREF_LOW,    NM_NDISC_PREFERENCE_LOW),
+	NM_UTILS_LOOKUP_ITEM (NDP_ROUTE_PREF_MEDIUM, NM_NDISC_PREFERENCE_MEDIUM),
+	NM_UTILS_LOOKUP_ITEM (NDP_ROUTE_PREF_HIGH,   NM_NDISC_PREFERENCE_HIGH),
+);
+
+static int
+receive_ra (struct ndp *ndp, struct ndp_msg *msg, gpointer user_data)
+{
+	NMNDisc *ndisc = (NMNDisc *) user_data;
+	NMNDiscDataInternal *rdata = ndisc->rdata;
+	NMNDiscConfigMap changed = 0;
+	struct ndp_msgra *msgra = ndp_msgra (msg);
+	struct in6_addr gateway_addr;
+	guint32 now = nm_utils_get_monotonic_timestamp_s ();
+	int offset;
+	int hop_limit;
+
+	/* Router discovery is subject to the following RFC documents:
+	 *
+	 * http://tools.ietf.org/html/rfc4861
+	 * http://tools.ietf.org/html/rfc4862
+	 *
+	 * The biggest difference from good old DHCP is that all configuration
+	 * items have their own lifetimes and they are merged from various
+	 * sources. Router discovery is *not* contract-based, so there is *no*
+	 * single time when the configuration is finished and updates can
+	 * come at any time.
+	 */
+	_LOGD ("received router advertisement at %u", now);
+
+	/* DHCP level:
+	 *
+	 * The problem with DHCP level is what to do if subsequent
+	 * router advertisements carry different flags. Currently we just
+	 * rewrite the flag with every inbound RA.
+	 */
+	{
+		NMNDiscDHCPLevel dhcp_level;
+
+		if (ndp_msgra_flag_managed (msgra))
+			dhcp_level = NM_NDISC_DHCP_LEVEL_MANAGED;
+		else if (ndp_msgra_flag_other (msgra))
+			dhcp_level = NM_NDISC_DHCP_LEVEL_OTHERCONF;
+		else
+			dhcp_level = NM_NDISC_DHCP_LEVEL_NONE;
+
+		/* when receiving multiple RA (possibly from different routers),
+		 * let's keep the "most managed" level. */
+		G_STATIC_ASSERT_EXPR (NM_NDISC_DHCP_LEVEL_MANAGED > NM_NDISC_DHCP_LEVEL_OTHERCONF);
+		G_STATIC_ASSERT_EXPR (NM_NDISC_DHCP_LEVEL_OTHERCONF > NM_NDISC_DHCP_LEVEL_NONE);
+		dhcp_level = MAX (dhcp_level, rdata->public.dhcp_level);
+
+		if (dhcp_level != rdata->public.dhcp_level) {
+			rdata->public.dhcp_level = dhcp_level;
+			changed |= NM_NDISC_CONFIG_DHCP_LEVEL;
+		}
+	}
+
+	/* Default gateway:
+	 *
+	 * Subsequent router advertisements can represent new default gateways
+	 * on the network. We should present all of them in router preference
+	 * order.
+	 */
+	gateway_addr = *ndp_msg_addrto (msg);
+	{
+		NMNDiscGateway gateway = {
+		    .address = gateway_addr,
+		    .timestamp = now,
+		    .lifetime = ndp_msgra_router_lifetime (msgra),
+		    .preference = translate_preference (ndp_msgra_route_preference (msgra)),
+		};
+
+		if (nm_ndisc_add_gateway (ndisc, &gateway))
+			changed |= NM_NDISC_CONFIG_GATEWAYS;
+	}
+
+	/* Addresses & Routes */
+	ndp_msg_opt_for_each_offset (offset, msg, NDP_MSG_OPT_PREFIX) {
+		guint8 r_plen;
+		struct in6_addr r_network;
+
+		/* Device route */
+
+		r_plen = ndp_msg_opt_prefix_len (msg, offset);
+		if (r_plen == 0 || r_plen > 128)
+			continue;
+		nm_utils_ip6_address_clear_host_address (&r_network, ndp_msg_opt_prefix (msg, offset), r_plen);
+
+		if (ndp_msg_opt_prefix_flag_on_link (msg, offset)) {
+			NMNDiscRoute route = {
+			    .network = r_network,
+			    .plen = r_plen,
+			    .timestamp = now,
+			    .lifetime = ndp_msg_opt_prefix_valid_time (msg, offset),
+			};
+
+			if (nm_ndisc_add_route (ndisc, &route))
+				changed |= NM_NDISC_CONFIG_ROUTES;
+		}
+
+		/* Address */
+		if (   r_plen == 64
+		    && ndp_msg_opt_prefix_flag_auto_addr_conf (msg, offset)) {
+			NMNDiscAddress address = {
+			    .address = r_network,
+			    .timestamp = now,
+			    .lifetime = ndp_msg_opt_prefix_valid_time (msg, offset),
+			    .preferred = ndp_msg_opt_prefix_preferred_time (msg, offset),
+			};
+
+			if (address.preferred > address.lifetime)
+				address.preferred = address.lifetime;
+			if (nm_ndisc_complete_and_add_address (ndisc, &address))
+				changed |= NM_NDISC_CONFIG_ADDRESSES;
+		}
+	}
+	ndp_msg_opt_for_each_offset(offset, msg, NDP_MSG_OPT_ROUTE) {
+		NMNDiscRoute route = {
+		    .gateway = gateway_addr,
+		    .plen = ndp_msg_opt_route_prefix_len (msg, offset),
+		    .timestamp = now,
+		    .lifetime = ndp_msg_opt_route_lifetime (msg, offset),
+		    .preference = translate_preference (ndp_msg_opt_route_preference (msg, offset)),
+		};
+
+		if (route.plen == 0 || route.plen > 128)
+			continue;
+
+		/* Routers through this particular gateway */
+		nm_utils_ip6_address_clear_host_address (&route.network, ndp_msg_opt_route_prefix (msg, offset), route.plen);
+		if (nm_ndisc_add_route (ndisc, &route))
+			changed |= NM_NDISC_CONFIG_ROUTES;
+	}
+
+	/* DNS information */
+	ndp_msg_opt_for_each_offset(offset, msg, NDP_MSG_OPT_RDNSS) {
+		static struct in6_addr *addr;
+		int addr_index;
+
+		ndp_msg_opt_rdnss_for_each_addr (addr, addr_index, msg, offset) {
+			NMNDiscDNSServer dns_server = {
+			    .address = *addr,
+			    .timestamp = now,
+			    .lifetime = ndp_msg_opt_rdnss_lifetime (msg, offset),
+			};
+
+			/* Pad the lifetime somewhat to give a bit of slack in cases
+			 * where one RA gets lost or something (which can happen on unreliable
+			 * links like WiFi where certain types of frames are not retransmitted).
+			 * Note that 0 has special meaning and is therefore not adjusted.
+			 */
+			if (dns_server.lifetime && dns_server.lifetime < 7200)
+				dns_server.lifetime = 7200;
+			if (nm_ndisc_add_dns_server (ndisc, &dns_server))
+				changed |= NM_NDISC_CONFIG_DNS_SERVERS;
+		}
+	}
+	ndp_msg_opt_for_each_offset(offset, msg, NDP_MSG_OPT_DNSSL) {
+		char *domain;
+		int domain_index;
+
+		ndp_msg_opt_dnssl_for_each_domain (domain, domain_index, msg, offset) {
+			NMNDiscDNSDomain dns_domain = {
+			    .domain = domain,
+			    .timestamp = now,
+			    .lifetime = ndp_msg_opt_rdnss_lifetime (msg, offset),
+			};
+
+			/* Pad the lifetime somewhat to give a bit of slack in cases
+			 * where one RA gets lost or something (which can happen on unreliable
+			 * links like WiFi where certain types of frames are not retransmitted).
+			 * Note that 0 has special meaning and is therefore not adjusted.
+			 */
+			if (dns_domain.lifetime && dns_domain.lifetime < 7200)
+				dns_domain.lifetime = 7200;
+			if (nm_ndisc_add_dns_domain (ndisc, &dns_domain))
+				changed |= NM_NDISC_CONFIG_DNS_DOMAINS;
+		}
+	}
+
+	hop_limit = ndp_msgra_curhoplimit (msgra);
+	if (rdata->public.hop_limit != hop_limit) {
+		rdata->public.hop_limit = hop_limit;
+		changed |= NM_NDISC_CONFIG_HOP_LIMIT;
+	}
+
+	/* MTU */
+	ndp_msg_opt_for_each_offset(offset, msg, NDP_MSG_OPT_MTU) {
+		guint32 mtu = ndp_msg_opt_mtu(msg, offset);
+		if (mtu >= 1280) {
+			if (rdata->public.mtu != mtu) {
+				rdata->public.mtu = mtu;
+				changed |= NM_NDISC_CONFIG_MTU;
+			}
+		} else {
+			/* All sorts of bad things would happen if we accepted this.
+			 * Kernel would set it, but would flush out all IPv6 addresses away
+			 * from the link, even the link-local, and we wouldn't be able to
+			 * listen for further RAs that could fix the MTU. */
+			_LOGW ("MTU too small for IPv6 ignored: %d", mtu);
+		}
+	}
+
+	nm_ndisc_ra_received (ndisc, now, changed);
+	return 0;
+}
+
+static void *
+_ndp_msg_add_option (struct ndp_msg *msg, int len)
+{
+	void *ret = (uint8_t *)msg + ndp_msg_payload_len (msg);
+
+	len += ndp_msg_payload_len (msg);
+	if (len > ndp_msg_payload_maxlen (msg))
+		return NULL;
+
+	ndp_msg_payload_len_set (msg, len);
+	return ret;
+}
+
+#define NM_ND_OPT_RDNSS 25
+typedef struct {
+	struct nd_opt_hdr header;
+	uint16_t reserved;
+	uint32_t lifetime;;
+	struct in6_addr addrs[0];
+} NMLndpRdnssOption;
+
+#define NM_ND_OPT_DNSSL 31
+typedef struct {
+	struct nd_opt_hdr header;
+	uint16_t reserved;
+	uint32_t lifetime;
+	char search_list[0];
+} NMLndpDnsslOption;
+
+static gboolean
+send_ra (NMNDisc *ndisc, GError **error)
+{
+	NMLndpNDiscPrivate *priv = NM_LNDP_NDISC_GET_PRIVATE ((NMLndpNDisc *) ndisc);
+	NMNDiscDataInternal *rdata = ndisc->rdata;
+	guint32 now = nm_utils_get_monotonic_timestamp_s ();
+	int errsv;
+	struct in6_addr *addr;
+	struct ndp_msg *msg;
+	struct nd_opt_prefix_info *prefix;
+	int i;
+
+	errsv = ndp_msg_new (&msg, NDP_MSG_RA);
+	if (errsv) {
+		errsv = errsv > 0 ? errsv : -errsv;
+		g_set_error_literal (error, NM_UTILS_ERROR, NM_UTILS_ERROR_UNKNOWN,
+		                     "cannot create a router advertisement");
+		return FALSE;
+	}
+
+	ndp_msg_ifindex_set (msg, nm_ndisc_get_ifindex (ndisc));
+
+	/* Multicast to all nodes. */
+	addr = ndp_msg_addrto (msg);
+	addr->s6_addr32[0] = htonl(0xff020000);
+	addr->s6_addr32[1] = 0;
+	addr->s6_addr32[2] = 0;
+	addr->s6_addr32[3] = htonl(0x1);
+
+	ndp_msgra_router_lifetime_set (ndp_msgra (msg), NM_NDISC_ROUTER_LIFETIME);
+
+	/* The device let us know about all addresses that the device got
+	 * whose prefixes are suitable for delegating. Let's announce them. */
+	for (i = 0; i < rdata->addresses->len; i++) {
+		NMNDiscAddress *address = &g_array_index (rdata->addresses, NMNDiscAddress, i);
+		guint32 age = now - address->timestamp;
+		guint32 lifetime = address->lifetime;
+		guint32 preferred = address->preferred;
+
+		/* Clamp the life times if they're not forever. */
+		if (lifetime != 0xffffffff)
+			lifetime = lifetime > age ? lifetime - age : 0;
+		if (preferred != 0xffffffff)
+			preferred = preferred > age ? preferred - age : 0;
+
+		prefix = _ndp_msg_add_option (msg, sizeof(*prefix));
+		if (!prefix) {
+			/* Maybe we could sent separate RAs, but why bother... */
+			_LOGW ("The RA is too big, had to omit some some prefixes.");
+			break;
+		}
+
+		prefix->nd_opt_pi_type = ND_OPT_PREFIX_INFORMATION;
+		prefix->nd_opt_pi_len = 4;
+		prefix->nd_opt_pi_prefix_len = 64;
+		prefix->nd_opt_pi_flags_reserved |= ND_OPT_PI_FLAG_ONLINK;
+		prefix->nd_opt_pi_flags_reserved |= ND_OPT_PI_FLAG_AUTO;
+		prefix->nd_opt_pi_valid_time = htonl(lifetime);
+		prefix->nd_opt_pi_preferred_time = htonl(preferred);
+		prefix->nd_opt_pi_prefix.s6_addr32[0] = address->address.s6_addr32[0];
+		prefix->nd_opt_pi_prefix.s6_addr32[1] = address->address.s6_addr32[1];
+		prefix->nd_opt_pi_prefix.s6_addr32[2] = 0;
+		prefix->nd_opt_pi_prefix.s6_addr32[3] = 0;
+	}
+
+	if (rdata->dns_servers->len) {
+		NMLndpRdnssOption *option;
+		int len = sizeof(*option) + sizeof(option->addrs[0]) * rdata->dns_servers->len;
+
+		option = _ndp_msg_add_option (msg, len);
+		if (option) {
+			option->header.nd_opt_type = NM_ND_OPT_RDNSS;
+			option->header.nd_opt_len = len / 8;
+			option->lifetime = htonl (900);
+
+			for (i = 0; i < rdata->dns_servers->len; i++) {
+				NMNDiscDNSServer *dns_server = &g_array_index (rdata->dns_servers, NMNDiscDNSServer, i);
+				option->addrs[i] = dns_server->address;
+			}
+		} else {
+			_LOGW ("The RA is too big, had to omit DNS information.");
+		}
+
+	}
+
+	if (rdata->dns_domains->len) {
+		NMLndpDnsslOption *option;
+		NMNDiscDNSDomain *dns_server;
+		int len = sizeof(*option);
+		char *search_list;
+
+		for (i = 0; i < rdata->dns_domains->len; i++) {
+			dns_server = &g_array_index (rdata->dns_domains, NMNDiscDNSDomain, i);
+			len += strlen (dns_server->domain) + 2;
+		}
+		len = (len + 8) & ~0x7;
+
+		option = _ndp_msg_add_option (msg, len);
+		if (option) {
+			option->header.nd_opt_type = NM_ND_OPT_DNSSL;
+			option->header.nd_opt_len = len / 8;
+			option->lifetime = htonl (900);
+
+			search_list = option->search_list;
+			for (i = 0; i < rdata->dns_domains->len; i++) {
+				NMNDiscDNSDomain *dns_domain = &g_array_index (rdata->dns_domains, NMNDiscDNSDomain, i);
+				uint8_t domain_len = strlen (dns_domain->domain);
+
+				*search_list++ = domain_len;
+				memcpy (search_list, dns_domain->domain, domain_len);
+				search_list += domain_len;
+				*search_list++ = '\0';
+			}
+		} else {
+			_LOGW ("The RA is too big, had to omit DNS search list.");
+		}
+	}
+
+	errsv = ndp_msg_send (priv->ndp, msg);
+
+	ndp_msg_destroy (msg);
+	if (errsv) {
+		errsv = errsv > 0 ? errsv : -errsv;
+		g_set_error (error, NM_UTILS_ERROR, NM_UTILS_ERROR_UNKNOWN,
+		             "%s (%d)",
+		             g_strerror (errsv), errsv);
+		return FALSE;
+	}
+
+	return TRUE;
+}
+
+static int
+receive_rs (struct ndp *ndp, struct ndp_msg *msg, gpointer user_data)
+{
+	NMNDisc *ndisc = user_data;
+
+	nm_ndisc_rs_received (ndisc);
+	return 0;
+}
+
+static gboolean
+event_ready (GIOChannel *source, GIOCondition condition, NMNDisc *ndisc)
+{
+	nm_auto_pop_netns NMPNetns *netns = NULL;
+	NMLndpNDiscPrivate *priv = NM_LNDP_NDISC_GET_PRIVATE ((NMLndpNDisc *) ndisc);
+
+	_LOGD ("processing libndp events");
+
+	if (!nm_ndisc_netns_push (ndisc, &netns))
+		return G_SOURCE_CONTINUE;
+
+	ndp_callall_eventfd_handler (priv->ndp);
+	return G_SOURCE_CONTINUE;
+}
+
+static void
+start (NMNDisc *ndisc)
+{
+	NMLndpNDiscPrivate *priv = NM_LNDP_NDISC_GET_PRIVATE ((NMLndpNDisc *) ndisc);
+	int fd = ndp_get_eventfd (priv->ndp);
+
+	g_return_if_fail (!priv->event_channel);
+	g_return_if_fail (!priv->event_id);
+
+	priv->event_channel = g_io_channel_unix_new (fd);
+	priv->event_id = g_io_add_watch (priv->event_channel, G_IO_IN, (GIOFunc) event_ready, ndisc);
+
+	/* Flush any pending messages to avoid using obsolete information */
+	event_ready (priv->event_channel, 0, ndisc);
+
+	switch (nm_ndisc_get_node_type (ndisc)) {
+	case NM_NDISC_NODE_TYPE_HOST:
+		ndp_msgrcv_handler_register (priv->ndp, receive_ra, NDP_MSG_RA, nm_ndisc_get_ifindex (ndisc), ndisc);
+		break;
+	case NM_NDISC_NODE_TYPE_ROUTER:
+		ndp_msgrcv_handler_register (priv->ndp, receive_rs, NDP_MSG_RS, nm_ndisc_get_ifindex (ndisc), ndisc);
+		break;
+	default:
+		g_assert_not_reached ();
+	}
+}
+
+/*****************************************************************************/
+
+static inline int
+ipv6_sysctl_get (NMPlatform *platform, const char *ifname, const char *property, int min, int max, int defval)
+{
+	return (int) nm_platform_sysctl_get_int_checked (platform,
+	                                                 NMP_SYSCTL_PATHID_ABSOLUTE (nm_utils_ip6_property_path (ifname, property)),
+	                                                 10,
+	                                                 min,
+	                                                 max,
+	                                                 defval);
+}
+
+static void
+nm_lndp_ndisc_init (NMLndpNDisc *lndp_ndisc)
+{
+}
+
+NMNDisc *
+nm_lndp_ndisc_new (NMPlatform *platform,
+                   int ifindex,
+                   const char *ifname,
+                   NMUtilsStableType stable_type,
+                   const char *network_id,
+                   NMSettingIP6ConfigAddrGenMode addr_gen_mode,
+                   NMNDiscNodeType node_type,
+                   GError **error)
+{
+	nm_auto_pop_netns NMPNetns *netns = NULL;
+	NMNDisc *ndisc;
+	NMLndpNDiscPrivate *priv;
+	int errsv;
+
+	g_return_val_if_fail (NM_IS_PLATFORM (platform), NULL);
+	g_return_val_if_fail (!error || !*error, NULL);
+	g_return_val_if_fail (network_id, NULL);
+
+	if (!nm_platform_netns_push (platform, &netns))
+		return NULL;
+
+	ndisc = g_object_new (NM_TYPE_LNDP_NDISC,
+	                      NM_NDISC_PLATFORM, platform,
+	                      NM_NDISC_STABLE_TYPE, (int) stable_type,
+	                      NM_NDISC_IFINDEX, ifindex,
+	                      NM_NDISC_IFNAME, ifname,
+	                      NM_NDISC_NETWORK_ID, network_id,
+	                      NM_NDISC_ADDR_GEN_MODE, (int) addr_gen_mode,
+	                      NM_NDISC_NODE_TYPE, (int) node_type,
+	                      NM_NDISC_MAX_ADDRESSES, ipv6_sysctl_get (platform, ifname,
+	                                                               "max_addresses",
+	                                                               0, G_MAXINT32, NM_NDISC_MAX_ADDRESSES_DEFAULT),
+	                      NM_NDISC_ROUTER_SOLICITATIONS, ipv6_sysctl_get (platform, ifname,
+	                                                                      "router_solicitations",
+	                                                                      1, G_MAXINT32, NM_NDISC_ROUTER_SOLICITATIONS_DEFAULT),
+	                      NM_NDISC_ROUTER_SOLICITATION_INTERVAL, ipv6_sysctl_get (platform, ifname,
+	                                                                              "router_solicitation_interval",
+	                                                                              1, G_MAXINT32, NM_NDISC_ROUTER_SOLICITATION_INTERVAL_DEFAULT),
+	                      NULL);
+
+	priv = NM_LNDP_NDISC_GET_PRIVATE ((NMLndpNDisc *) ndisc);
+
+	errsv = ndp_open (&priv->ndp);
+
+	if (errsv != 0) {
+		errsv = errsv > 0 ? errsv : -errsv;
+		g_set_error (error, NM_UTILS_ERROR, NM_UTILS_ERROR_UNKNOWN,
+		             "failure creating libndp socket: %s (%d)",
+		             g_strerror (errsv), errsv);
+		g_object_unref (ndisc);
+		return NULL;
+	}
+	return ndisc;
+}
+
+static void
+dispose (GObject *object)
+{
+	NMNDisc *ndisc = (NMNDisc *) object;
+	NMLndpNDiscPrivate *priv = NM_LNDP_NDISC_GET_PRIVATE ((NMLndpNDisc *) ndisc);
+
+	nm_clear_g_source (&priv->event_id);
+	g_clear_pointer (&priv->event_channel, g_io_channel_unref);
+
+	if (priv->ndp) {
+		switch (nm_ndisc_get_node_type (ndisc)) {
+		case NM_NDISC_NODE_TYPE_HOST:
+			ndp_msgrcv_handler_unregister (priv->ndp, receive_ra, NDP_MSG_RA, nm_ndisc_get_ifindex (ndisc), ndisc);
+			break;
+		case NM_NDISC_NODE_TYPE_ROUTER:
+			ndp_msgrcv_handler_unregister (priv->ndp, receive_rs, NDP_MSG_RS, nm_ndisc_get_ifindex (ndisc), ndisc);
+			break;
+		default:
+			g_assert_not_reached ();
+		}
+		ndp_close (priv->ndp);
+		priv->ndp = NULL;
+	}
+
+	G_OBJECT_CLASS (nm_lndp_ndisc_parent_class)->dispose (object);
+}
+
+static void
+nm_lndp_ndisc_class_init (NMLndpNDiscClass *klass)
+{
+	GObjectClass *object_class = G_OBJECT_CLASS (klass);
+	NMNDiscClass *ndisc_class = NM_NDISC_CLASS (klass);
+
+	object_class->dispose = dispose;
+	ndisc_class->start = start;
+	ndisc_class->send_rs = send_rs;
+	ndisc_class->send_ra = send_ra;
+}