summary refs log tree commit diff
path: root/src/ndisc
diff options
context:
space:
mode:
authorMichael Biebl <biebl@debian.org>2017-01-17 20:25:09 +0100
committerMichael Biebl <biebl@debian.org>2017-01-17 20:25:09 +0100
commit58f8be580039b0575b197b9573a1c92745d96d30 (patch)
tree2c226233f623a0dcb529be0eb8cdf97e4a2ae0c0 /src/ndisc
parent45cb5bb3c0e6edb887cf69b417fcaf7053814a9b (diff)
New upstream version 1.5.90 upstream/1.5.90
Diffstat (limited to 'src/ndisc')
-rw-r--r--src/ndisc/nm-fake-ndisc.c412
-rw-r--r--src/ndisc/nm-fake-ndisc.h81
-rw-r--r--src/ndisc/nm-lndp-ndisc.c629
-rw-r--r--src/ndisc/nm-lndp-ndisc.h48
-rw-r--r--src/ndisc/nm-ndisc-private.h78
-rw-r--r--src/ndisc/nm-ndisc.c1235
-rw-r--r--src/ndisc/nm-ndisc.h189
-rw-r--r--src/ndisc/tests/test-ndisc-fake.c483
-rw-r--r--src/ndisc/tests/test-ndisc-linux.c86
9 files changed, 3241 insertions, 0 deletions
diff --git a/src/ndisc/nm-fake-ndisc.c b/src/ndisc/nm-fake-ndisc.c
new file mode 100644
index 00000000..7a9fb110
--- /dev/null
+++ b/src/ndisc/nm-fake-ndisc.c
@@ -0,0 +1,412 @@
+/* -*- Mode: C; tab-width: 4; indent-tabs-mode: t; c-basic-offset: 4 -*- */
+/* nm-fake-ndisc.c - Fake implementation of 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-fake-ndisc.h"
+
+#include <string.h>
+#include <arpa/inet.h>
+
+#include "nm-ndisc-private.h"
+
+#define _NMLOG_PREFIX_NAME                "ndisc-fake"
+
+/*****************************************************************************/
+
+typedef struct {
+	guint id;
+	guint when;
+
+	NMNDiscDHCPLevel dhcp_level;
+	GArray *gateways;
+	GArray *prefixes;
+	GArray *dns_servers;
+	GArray *dns_domains;
+	int hop_limit;
+	guint32 mtu;
+} FakeRa;
+
+typedef struct {
+        struct in6_addr network;
+        int plen;
+        struct in6_addr gateway;
+        guint32 timestamp;
+        guint32 lifetime;
+        guint32 preferred;
+        NMNDiscPreference preference;
+} FakePrefix;
+
+/*****************************************************************************/
+
+enum {
+	RS_SENT,
+	LAST_SIGNAL,
+};
+static guint signals[LAST_SIGNAL] = { 0 };
+
+typedef struct {
+	guint receive_ra_id;
+	GSList *ras;
+} NMFakeNDiscPrivate;
+
+struct _NMFakeRNDisc {
+	NMNDisc parent;
+	NMFakeNDiscPrivate _priv;
+};
+
+struct _NMFakeRNDiscClass {
+	NMNDiscClass parent;
+};
+
+G_DEFINE_TYPE (NMFakeNDisc, nm_fake_ndisc, NM_TYPE_NDISC)
+
+#define NM_FAKE_NDISC_GET_PRIVATE(self) _NM_GET_PRIVATE (self, NMFakeNDisc, NM_IS_FAKE_NDISC)
+
+/*****************************************************************************/
+
+static void
+fake_ra_free (gpointer data)
+{
+	FakeRa *ra = data;
+
+	g_array_free (ra->gateways, TRUE);
+	g_array_free (ra->prefixes, TRUE);
+	g_array_free (ra->dns_servers, TRUE);
+	g_array_free (ra->dns_domains, TRUE);
+	g_free (ra);
+}
+
+static void
+ra_dns_domain_free (gpointer data)
+{
+	g_free (((NMNDiscDNSDomain *)(data))->domain);
+}
+
+static FakeRa *
+find_ra (GSList *ras, guint id)
+{
+	GSList *iter;
+
+	for (iter = ras; iter; iter = iter->next) {
+		if (((FakeRa *) iter->data)->id == id)
+			return iter->data;
+	}
+	return NULL;
+}
+
+guint
+nm_fake_ndisc_add_ra (NMFakeNDisc *self,
+                      guint seconds_after_previous,
+                      NMNDiscDHCPLevel dhcp_level,
+                      int hop_limit,
+                      guint32 mtu)
+{
+	NMFakeNDiscPrivate *priv = NM_FAKE_NDISC_GET_PRIVATE (self);
+	static guint counter = 1;
+	FakeRa *ra;
+
+	ra = g_malloc0 (sizeof (*ra));
+	ra->id = counter++;
+	ra->when = seconds_after_previous;
+	ra->dhcp_level = dhcp_level;
+	ra->hop_limit = hop_limit;
+	ra->mtu = mtu;
+	ra->gateways = g_array_new (FALSE, FALSE, sizeof (NMNDiscGateway));
+	ra->prefixes = g_array_new (FALSE, FALSE, sizeof (FakePrefix));
+	ra->dns_servers = g_array_new (FALSE, FALSE, sizeof (NMNDiscDNSServer));
+	ra->dns_domains = g_array_new (FALSE, FALSE, sizeof (NMNDiscDNSDomain));
+	g_array_set_clear_func (ra->dns_domains, ra_dns_domain_free);
+
+	priv->ras = g_slist_append (priv->ras, ra);
+	return ra->id;
+}
+
+void
+nm_fake_ndisc_add_gateway (NMFakeNDisc *self,
+                           guint ra_id,
+                           const char *addr,
+                           guint32 timestamp,
+                           guint32 lifetime,
+                           NMNDiscPreference preference)
+{
+	NMFakeNDiscPrivate *priv = NM_FAKE_NDISC_GET_PRIVATE (self);
+	FakeRa *ra = find_ra (priv->ras, ra_id);
+	NMNDiscGateway *gw;
+
+	g_assert (ra);
+	g_array_set_size (ra->gateways, ra->gateways->len + 1);
+	gw = &g_array_index (ra->gateways, NMNDiscGateway, ra->gateways->len - 1);
+	g_assert (inet_pton (AF_INET6, addr, &gw->address) == 1);
+	gw->timestamp = timestamp;
+	gw->lifetime = lifetime;
+	gw->preference = preference;
+}
+
+void
+nm_fake_ndisc_add_prefix (NMFakeNDisc *self,
+                          guint ra_id,
+                          const char *network,
+                          guint plen,
+                          const char *gateway,
+                          guint32 timestamp,
+                          guint32 lifetime,
+                          guint32 preferred,
+                          NMNDiscPreference preference)
+{
+	NMFakeNDiscPrivate *priv = NM_FAKE_NDISC_GET_PRIVATE (self);
+	FakeRa *ra = find_ra (priv->ras, ra_id);
+	FakePrefix *prefix;
+
+	g_assert (ra);
+	g_array_set_size (ra->prefixes, ra->prefixes->len + 1);
+	prefix = &g_array_index (ra->prefixes, FakePrefix, ra->prefixes->len - 1);
+	memset (prefix, 0, sizeof (*prefix));
+	g_assert (inet_pton (AF_INET6, network, &prefix->network) == 1);
+	g_assert (inet_pton (AF_INET6, gateway, &prefix->gateway) == 1);
+	prefix->plen = plen;
+	prefix->timestamp = timestamp;
+	prefix->lifetime = lifetime;
+	prefix->preferred = preferred;
+	prefix->preference = preference;
+}
+
+void
+nm_fake_ndisc_add_dns_server (NMFakeNDisc *self,
+                              guint ra_id,
+                              const char *address,
+                              guint32 timestamp,
+                              guint32 lifetime)
+{
+	NMFakeNDiscPrivate *priv = NM_FAKE_NDISC_GET_PRIVATE (self);
+	FakeRa *ra = find_ra (priv->ras, ra_id);
+	NMNDiscDNSServer *dns;
+
+	g_assert (ra);
+	g_array_set_size (ra->dns_servers, ra->dns_servers->len + 1);
+	dns = &g_array_index (ra->dns_servers, NMNDiscDNSServer, ra->dns_servers->len - 1);
+	g_assert (inet_pton (AF_INET6, address, &dns->address) == 1);
+	dns->timestamp = timestamp;
+	dns->lifetime = lifetime;
+}
+
+void
+nm_fake_ndisc_add_dns_domain (NMFakeNDisc *self,
+                              guint ra_id,
+                              const char *domain,
+                              guint32 timestamp,
+                              guint32 lifetime)
+{
+	NMFakeNDiscPrivate *priv = NM_FAKE_NDISC_GET_PRIVATE (self);
+	FakeRa *ra = find_ra (priv->ras, ra_id);
+	NMNDiscDNSDomain *dns;
+
+	g_assert (ra);
+	g_array_set_size (ra->dns_domains, ra->dns_domains->len + 1);
+	dns = &g_array_index (ra->dns_domains, NMNDiscDNSDomain, ra->dns_domains->len - 1);
+	dns->domain = g_strdup (domain);
+	dns->timestamp = timestamp;
+	dns->lifetime = lifetime;
+}
+
+gboolean
+nm_fake_ndisc_done (NMFakeNDisc *self)
+{
+	return !NM_FAKE_NDISC_GET_PRIVATE (self)->ras;
+}
+
+/*****************************************************************************/
+
+static gboolean
+send_rs (NMNDisc *ndisc, GError **error)
+{
+	g_signal_emit (ndisc, signals[RS_SENT], 0);
+	return TRUE;
+}
+
+static gboolean
+receive_ra (gpointer user_data)
+{
+	NMFakeNDisc *self = user_data;
+	NMFakeNDiscPrivate *priv = NM_FAKE_NDISC_GET_PRIVATE (self);
+	NMNDisc *ndisc = NM_NDISC (self);
+	NMNDiscDataInternal *rdata = ndisc->rdata;
+	FakeRa *ra = priv->ras->data;
+	NMNDiscConfigMap changed = 0;
+	guint32 now = nm_utils_get_monotonic_timestamp_s ();
+	guint i;
+	NMNDiscDHCPLevel dhcp_level;
+
+	priv->receive_ra_id = 0;
+
+	/* preserve the "most managed" level  on updates. */
+	dhcp_level = MAX (rdata->public.dhcp_level, ra->dhcp_level);
+
+	if (rdata->public.dhcp_level != dhcp_level) {
+		rdata->public.dhcp_level = dhcp_level;
+		changed |= NM_NDISC_CONFIG_DHCP_LEVEL;
+	}
+
+	for (i = 0; i < ra->gateways->len; i++) {
+		NMNDiscGateway *item = &g_array_index (ra->gateways, NMNDiscGateway, i);
+
+		if (nm_ndisc_add_gateway (ndisc, item))
+			changed |= NM_NDISC_CONFIG_GATEWAYS;
+	}
+
+	for (i = 0; i < ra->prefixes->len; i++) {
+		FakePrefix *item = &g_array_index (ra->prefixes, FakePrefix, i);
+		NMNDiscRoute route = {
+			.network = item->network,
+			.plen = item->plen,
+			.gateway = item->gateway,
+			.timestamp = item->timestamp,
+			.lifetime = item->lifetime,
+			.preference = item->preference,
+		};
+
+		g_assert (route.plen > 0 && route.plen <= 128);
+
+		if (nm_ndisc_add_route (ndisc, &route))
+			changed |= NM_NDISC_CONFIG_ROUTES;
+
+		if (item->plen == 64) {
+			NMNDiscAddress address = {
+				.address = item->network,
+				.timestamp = item->timestamp,
+				.lifetime = item->lifetime,
+				.preferred = item->preferred,
+				.dad_counter = 0,
+			};
+
+			if (nm_ndisc_complete_and_add_address (ndisc, &address))
+				changed |= NM_NDISC_CONFIG_ADDRESSES;
+		}
+	}
+
+	for (i = 0; i < ra->dns_servers->len; i++) {
+		NMNDiscDNSServer *item = &g_array_index (ra->dns_servers, NMNDiscDNSServer, i);
+
+		if (nm_ndisc_add_dns_server (ndisc, item))
+			changed |= NM_NDISC_CONFIG_DNS_SERVERS;
+	}
+
+	for (i = 0; i < ra->dns_domains->len; i++) {
+		NMNDiscDNSDomain *item = &g_array_index (ra->dns_domains, NMNDiscDNSDomain, i);
+
+		if (nm_ndisc_add_dns_domain (ndisc, item))
+			changed |= NM_NDISC_CONFIG_DNS_DOMAINS;
+	}
+
+	if (rdata->public.mtu != ra->mtu) {
+		rdata->public.mtu = ra->mtu;
+		changed |= NM_NDISC_CONFIG_MTU;
+	}
+
+	if (rdata->public.hop_limit != ra->hop_limit) {
+		rdata->public.hop_limit = ra->hop_limit;
+		changed |= NM_NDISC_CONFIG_HOP_LIMIT;
+	}
+
+	priv->ras = g_slist_remove (priv->ras, priv->ras->data);
+	fake_ra_free (ra);
+
+	nm_ndisc_ra_received (NM_NDISC (self), now, changed);
+
+	/* Schedule next RA */
+	if (priv->ras) {
+		ra = priv->ras->data;
+		priv->receive_ra_id = g_timeout_add_seconds (ra->when, receive_ra, self);
+	}
+
+	return G_SOURCE_REMOVE;
+}
+
+static void
+start (NMNDisc *ndisc)
+{
+	NMFakeNDiscPrivate *priv = NM_FAKE_NDISC_GET_PRIVATE ((NMFakeNDisc *) ndisc);
+	FakeRa *ra;
+
+	/* Queue up the first fake RA */
+	g_assert (priv->ras);
+	ra = priv->ras->data;
+
+	g_assert (!priv->receive_ra_id);
+	priv->receive_ra_id = g_timeout_add_seconds (ra->when, receive_ra, ndisc);
+}
+
+void
+nm_fake_ndisc_emit_new_ras (NMFakeNDisc *self)
+{
+	if (!NM_FAKE_NDISC_GET_PRIVATE (self)->receive_ra_id)
+		start (NM_NDISC (self));
+}
+
+/*****************************************************************************/
+
+static void
+nm_fake_ndisc_init (NMFakeNDisc *fake_ndisc)
+{
+}
+
+NMNDisc *
+nm_fake_ndisc_new (int ifindex, const char *ifname)
+{
+	return g_object_new (NM_TYPE_FAKE_NDISC,
+	                     NM_NDISC_IFINDEX, ifindex,
+	                     NM_NDISC_IFNAME, ifname,
+	                     NM_NDISC_NODE_TYPE, (int) NM_NDISC_NODE_TYPE_HOST,
+	                     NM_NDISC_STABLE_TYPE, (int) NM_UTILS_STABLE_TYPE_UUID,
+	                     NM_NDISC_NETWORK_ID, "fake",
+	                     NULL);
+}
+
+static void
+dispose (GObject *object)
+{
+	NMFakeNDiscPrivate *priv = NM_FAKE_NDISC_GET_PRIVATE ((NMFakeNDisc *) object);
+
+	nm_clear_g_source (&priv->receive_ra_id);
+
+	g_slist_free_full (priv->ras, fake_ra_free);
+	priv->ras = NULL;
+
+	G_OBJECT_CLASS (nm_fake_ndisc_parent_class)->dispose (object);
+}
+
+static void
+nm_fake_ndisc_class_init (NMFakeNDiscClass *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;
+
+	signals[RS_SENT] =
+	    g_signal_new (NM_FAKE_NDISC_RS_SENT,
+	                  G_OBJECT_CLASS_TYPE (klass),
+	                  G_SIGNAL_RUN_FIRST,
+	                  0,  NULL, NULL, NULL,
+	                  G_TYPE_NONE, 0);
+}
diff --git a/src/ndisc/nm-fake-ndisc.h b/src/ndisc/nm-fake-ndisc.h
new file mode 100644
index 00000000..2544c456
--- /dev/null
+++ b/src/ndisc/nm-fake-ndisc.h
@@ -0,0 +1,81 @@
+/* -*- Mode: C; tab-width: 4; indent-tabs-mode: t; c-basic-offset: 4 -*- */
+/* nm-fake-ndisc.h - Fake implementation of 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.
+ */
+
+#ifndef __NETWORKMANAGER_FAKE_NDISC_H__
+#define __NETWORKMANAGER_FAKE_NDISC_H__
+
+#include "nm-ndisc.h"
+
+#define NM_TYPE_FAKE_NDISC            (nm_fake_ndisc_get_type ())
+#define NM_FAKE_NDISC(obj)            (G_TYPE_CHECK_INSTANCE_CAST ((obj), NM_TYPE_FAKE_NDISC, NMFakeNDisc))
+#define NM_FAKE_NDISC_CLASS(klass)    (G_TYPE_CHECK_CLASS_CAST ((klass), NM_TYPE_FAKE_NDISC, NMFakeNDiscClass))
+#define NM_IS_FAKE_NDISC(obj)         (G_TYPE_CHECK_INSTANCE_TYPE ((obj), NM_TYPE_FAKE_NDISC))
+#define NM_IS_FAKE_NDISC_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE ((klass), NM_TYPE_FAKE_NDISC))
+#define NM_FAKE_NDISC_GET_CLASS(obj)  (G_TYPE_INSTANCE_GET_CLASS ((obj), NM_TYPE_FAKE_NDISC, NMFakeNDiscClass))
+
+#define NM_FAKE_NDISC_RS_SENT "rs-sent"
+
+typedef struct _NMFakeRNDisc NMFakeNDisc;
+typedef struct _NMFakeRNDiscClass NMFakeNDiscClass;
+
+GType nm_fake_ndisc_get_type (void);
+
+NMNDisc *nm_fake_ndisc_new (int ifindex, const char *ifname);
+
+guint nm_fake_ndisc_add_ra (NMFakeNDisc *self,
+                            guint seconds,
+                            NMNDiscDHCPLevel dhcp_level,
+                            int hop_limit,
+                            guint32 mtu);
+
+void nm_fake_ndisc_add_gateway    (NMFakeNDisc *self,
+                                   guint ra_id,
+                                   const char *addr,
+                                   guint32 timestamp,
+                                   guint32 lifetime,
+                                   NMNDiscPreference preference);
+
+void nm_fake_ndisc_add_prefix     (NMFakeNDisc *self,
+                                   guint ra_id,
+                                   const char *network,
+                                   guint plen,
+                                   const char *gateway,
+                                   guint32 timestamp,
+                                   guint32 lifetime,
+                                   guint32 preferred,
+                                   NMNDiscPreference preference);
+
+void nm_fake_ndisc_add_dns_server (NMFakeNDisc *self,
+                                   guint ra_id,
+                                   const char *address,
+                                   guint32 timestamp,
+                                   guint32 lifetime);
+
+void nm_fake_ndisc_add_dns_domain (NMFakeNDisc *self,
+                                   guint ra_id,
+                                   const char *domain,
+                                   guint32 timestamp,
+                                   guint32 lifetime);
+
+void nm_fake_ndisc_emit_new_ras (NMFakeNDisc *self);
+
+gboolean nm_fake_ndisc_done (NMFakeNDisc *self);
+
+#endif /* __NETWORKMANAGER_FAKE_NDISC_H__ */
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;
+}
diff --git a/src/ndisc/nm-lndp-ndisc.h b/src/ndisc/nm-lndp-ndisc.h
new file mode 100644
index 00000000..f042fb74
--- /dev/null
+++ b/src/ndisc/nm-lndp-ndisc.h
@@ -0,0 +1,48 @@
+/* -*- Mode: C; tab-width: 4; indent-tabs-mode: t; c-basic-offset: 4 -*- */
+/* nm-lndp-ndisc.h - Implementation of neighbor discovery 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.
+ */
+
+#ifndef __NETWORKMANAGER_LNDP_NDISC_H__
+#define __NETWORKMANAGER_LNDP_NDISC_H__
+
+#include "nm-ndisc.h"
+#include "nm-core-utils.h"
+
+#define NM_TYPE_LNDP_NDISC            (nm_lndp_ndisc_get_type ())
+#define NM_LNDP_NDISC(obj)            (G_TYPE_CHECK_INSTANCE_CAST ((obj), NM_TYPE_LNDP_NDISC, NMLndpNDisc))
+#define NM_LNDP_NDISC_CLASS(klass)    (G_TYPE_CHECK_CLASS_CAST ((klass), NM_TYPE_LNDP_NDISC, NMLndpNDiscClass))
+#define NM_IS_LNDP_NDISC(obj)         (G_TYPE_CHECK_INSTANCE_TYPE ((obj), NM_TYPE_LNDP_NDISC))
+#define NM_IS_LNDP_NDISC_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE ((klass), NM_TYPE_LNDP_NDISC))
+#define NM_LNDP_NDISC_GET_CLASS(obj)  (G_TYPE_INSTANCE_GET_CLASS ((obj), NM_TYPE_LNDP_NDISC, NMLndpNDiscClass))
+
+typedef struct _NMLndpNDisc NMLndpNDisc;
+typedef struct _NMLndpNDiscClass NMLndpNDiscClass;
+
+GType nm_lndp_ndisc_get_type (void);
+
+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);
+
+#endif /* __NETWORKMANAGER_LNDP_NDISC_H__ */
diff --git a/src/ndisc/nm-ndisc-private.h b/src/ndisc/nm-ndisc-private.h
new file mode 100644
index 00000000..2308675a
--- /dev/null
+++ b/src/ndisc/nm-ndisc-private.h
@@ -0,0 +1,78 @@
+/* -*- Mode: C; tab-width: 4; indent-tabs-mode: t; c-basic-offset: 4 -*- */
+/* nm-ndisc.h - 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 2015 Red Hat, Inc.
+ */
+
+#ifndef __NETWORKMANAGER_NDISC_PRIVATE_H__
+#define __NETWORKMANAGER_NDISC_PRIVATE_H__
+
+#include "nm-ndisc.h"
+
+/* Functions only used by ndisc implementations */
+
+struct _NMNDiscDataInternal {
+	NMNDiscData public;
+	GArray *gateways;
+	GArray *addresses;
+	GArray *routes;
+	GArray *dns_servers;
+	GArray *dns_domains;
+};
+
+typedef struct _NMNDiscDataInternal NMNDiscDataInternal;
+
+void nm_ndisc_ra_received (NMNDisc *ndisc, guint32 now, NMNDiscConfigMap changed);
+void nm_ndisc_rs_received (NMNDisc *ndisc);
+
+gboolean nm_ndisc_add_gateway              (NMNDisc *ndisc, const NMNDiscGateway *new);
+gboolean nm_ndisc_complete_and_add_address (NMNDisc *ndisc, NMNDiscAddress *new);
+gboolean nm_ndisc_add_route                (NMNDisc *ndisc, const NMNDiscRoute *new);
+gboolean nm_ndisc_add_dns_server           (NMNDisc *ndisc, const NMNDiscDNSServer *new);
+gboolean nm_ndisc_add_dns_domain           (NMNDisc *ndisc, const NMNDiscDNSDomain *new);
+
+/*****************************************************************************/
+
+#define _NMLOG_DOMAIN                     LOGD_IP6
+#define _NMLOG(level, ...)                _LOG(level, _NMLOG_DOMAIN,  ndisc, __VA_ARGS__)
+
+#define _LOG(level, domain, self, ...) \
+    G_STMT_START { \
+        const NMLogLevel __level = (level); \
+        const NMLogDomain __domain = (domain); \
+        \
+        if (nm_logging_enabled (__level, __domain)) { \
+            NMNDisc *const __self = (self); \
+            char __prefix[64]; \
+            \
+            _nm_log (__level, __domain, 0, \
+                     "%s: " _NM_UTILS_MACRO_FIRST (__VA_ARGS__), \
+                     (__self \
+                        ? ({ \
+                            const char *__ifname = nm_ndisc_get_ifname (__self); \
+                            nm_sprintf_buf (__prefix, "%s[%p,%s%s%s]", \
+                                            _NMLOG_PREFIX_NAME, __self, \
+                                            NM_PRINT_FMT_QUOTE_STRING (__ifname)); \
+                            }) \
+                        : _NMLOG_PREFIX_NAME) \
+                     _NM_UTILS_MACRO_REST (__VA_ARGS__)); \
+        } \
+    } G_STMT_END
+
+/*****************************************************************************/
+
+#endif /* __NETWORKMANAGER_NDISC_PRIVATE_H__ */
diff --git a/src/ndisc/nm-ndisc.c b/src/ndisc/nm-ndisc.c
new file mode 100644
index 00000000..a50bbe43
--- /dev/null
+++ b/src/ndisc/nm-ndisc.c
@@ -0,0 +1,1235 @@
+/* -*- 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);
+}
diff --git a/src/ndisc/nm-ndisc.h b/src/ndisc/nm-ndisc.h
new file mode 100644
index 00000000..7c67289d
--- /dev/null
+++ b/src/ndisc/nm-ndisc.h
@@ -0,0 +1,189 @@
+/* -*- Mode: C; tab-width: 4; indent-tabs-mode: t; c-basic-offset: 4 -*- */
+/* nm-ndisc.h - 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.
+ */
+
+#ifndef __NETWORKMANAGER_NDISC_H__
+#define __NETWORKMANAGER_NDISC_H__
+
+#include <stdlib.h>
+#include <netinet/in.h>
+
+#include "nm-setting-ip6-config.h"
+#include "NetworkManagerUtils.h"
+
+#define NM_TYPE_NDISC            (nm_ndisc_get_type ())
+#define NM_NDISC(obj)            (G_TYPE_CHECK_INSTANCE_CAST ((obj), NM_TYPE_NDISC, NMNDisc))
+#define NM_NDISC_CLASS(klass)    (G_TYPE_CHECK_CLASS_CAST ((klass), NM_TYPE_NDISC, NMNDiscClass))
+#define NM_IS_NDISC(obj)         (G_TYPE_CHECK_INSTANCE_TYPE ((obj), NM_TYPE_NDISC))
+#define NM_IS_NDISC_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE ((klass), NM_TYPE_NDISC))
+#define NM_NDISC_GET_CLASS(obj)  (G_TYPE_INSTANCE_GET_CLASS ((obj), NM_TYPE_NDISC, NMNDiscClass))
+
+#define NM_NDISC_PLATFORM       "platform"
+#define NM_NDISC_IFINDEX        "ifindex"
+#define NM_NDISC_IFNAME         "ifname"
+#define NM_NDISC_NETWORK_ID     "network-id"
+#define NM_NDISC_ADDR_GEN_MODE  "addr-gen-mode"
+#define NM_NDISC_STABLE_TYPE    "stable-type"
+#define NM_NDISC_NODE_TYPE      "node-type"
+#define NM_NDISC_MAX_ADDRESSES  "max-addresses"
+#define NM_NDISC_ROUTER_SOLICITATIONS "router-solicitations"
+#define NM_NDISC_ROUTER_SOLICITATION_INTERVAL "router-solicitation-interval"
+
+#define NM_NDISC_CONFIG_RECEIVED "config-received"
+#define NM_NDISC_RA_TIMEOUT      "ra-timeout"
+
+typedef enum {
+	NM_NDISC_DHCP_LEVEL_UNKNOWN,
+	NM_NDISC_DHCP_LEVEL_NONE,
+	NM_NDISC_DHCP_LEVEL_OTHERCONF,
+	NM_NDISC_DHCP_LEVEL_MANAGED
+} NMNDiscDHCPLevel;
+
+typedef enum {
+	NM_NDISC_PREFERENCE_INVALID,
+	NM_NDISC_PREFERENCE_LOW,
+	NM_NDISC_PREFERENCE_MEDIUM,
+	NM_NDISC_PREFERENCE_HIGH
+} NMNDiscPreference;
+
+typedef struct {
+	struct in6_addr address;
+	guint32 timestamp;
+	guint32 lifetime;
+	NMNDiscPreference preference;
+} NMNDiscGateway;
+
+typedef struct {
+	struct in6_addr address;
+	guint8 dad_counter;
+	guint32 timestamp;
+	guint32 lifetime;
+	guint32 preferred;
+} NMNDiscAddress;
+
+typedef struct {
+	struct in6_addr network;
+	guint8 plen;
+	struct in6_addr gateway;
+	guint32 timestamp;
+	guint32 lifetime;
+	NMNDiscPreference preference;
+} NMNDiscRoute;
+
+typedef struct {
+	struct in6_addr address;
+	guint32 timestamp;
+	guint32 lifetime;
+} NMNDiscDNSServer;
+
+typedef struct {
+	char *domain;
+	guint32 timestamp;
+	guint32 lifetime;
+} NMNDiscDNSDomain;
+
+typedef enum {
+	NM_NDISC_CONFIG_DHCP_LEVEL                          = 1 << 0,
+	NM_NDISC_CONFIG_GATEWAYS                            = 1 << 1,
+	NM_NDISC_CONFIG_ADDRESSES                           = 1 << 2,
+	NM_NDISC_CONFIG_ROUTES                              = 1 << 3,
+	NM_NDISC_CONFIG_DNS_SERVERS                         = 1 << 4,
+	NM_NDISC_CONFIG_DNS_DOMAINS                         = 1 << 5,
+	NM_NDISC_CONFIG_HOP_LIMIT                           = 1 << 6,
+	NM_NDISC_CONFIG_MTU                                 = 1 << 7,
+} NMNDiscConfigMap;
+
+typedef enum {
+	NM_NDISC_NODE_TYPE_INVALID,
+	NM_NDISC_NODE_TYPE_HOST,
+	NM_NDISC_NODE_TYPE_ROUTER,
+} NMNDiscNodeType;
+
+#define NM_NDISC_MAX_ADDRESSES_DEFAULT 16
+#define NM_NDISC_ROUTER_SOLICITATIONS_DEFAULT 3          /* RFC4861 MAX_RTR_SOLICITATIONS */
+#define NM_NDISC_ROUTER_SOLICITATION_INTERVAL_DEFAULT 4  /* RFC4861 RTR_SOLICITATION_INTERVAL */
+#define NM_NDISC_ROUTER_ADVERTISEMENTS_DEFAULT 3         /* RFC4861 MAX_INITIAL_RTR_ADVERTISEMENTS */
+#define NM_NDISC_ROUTER_ADVERT_DELAY 3                   /* RFC4861 MIN_DELAY_BETWEEN_RAS */
+#define NM_NDISC_ROUTER_ADVERT_INITIAL_INTERVAL 16       /* RFC4861 MAX_INITIAL_RTR_ADVERT_INTERVAL */
+#define NM_NDISC_ROUTER_ADVERT_DELAY_MS 500              /* RFC4861 MAX_RA_DELAY_TIME */
+#define NM_NDISC_ROUTER_ADVERT_MAX_INTERVAL 600          /* RFC4861 MaxRtrAdvInterval default */
+#define NM_NDISC_ROUTER_LIFETIME 900                     /* 1.5 * NM_NDISC_ROUTER_ADVERT_MAX_INTERVAL */
+
+struct _NMNDiscPrivate;
+struct _NMNDiscDataInternal;
+
+typedef struct {
+	NMNDiscDHCPLevel dhcp_level;
+	guint32 mtu;
+	int hop_limit;
+
+	guint gateways_n;
+	guint addresses_n;
+	guint routes_n;
+	guint dns_servers_n;
+	guint dns_domains_n;
+
+	const NMNDiscGateway *gateways;
+	const NMNDiscAddress *addresses;
+	const NMNDiscRoute *routes;
+	const NMNDiscDNSServer *dns_servers;
+	const NMNDiscDNSDomain *dns_domains;
+} NMNDiscData;
+
+/**
+ * NMNDisc:
+ *
+ * Interface-specific structure that handles incoming router advertisements,
+ * caches advertised items and removes them when they are obsolete.
+ */
+typedef struct {
+	GObject parent;
+	union {
+		struct _NMNDiscPrivate *_priv;
+		struct _NMNDiscDataInternal *rdata;
+	};
+} NMNDisc;
+
+typedef struct {
+	GObjectClass parent;
+
+	void (*start) (NMNDisc *ndisc);
+	gboolean (*send_rs) (NMNDisc *ndisc, GError **error);
+	gboolean (*send_ra) (NMNDisc *ndisc, GError **error);
+} NMNDiscClass;
+
+GType nm_ndisc_get_type (void);
+
+int nm_ndisc_get_ifindex (NMNDisc *self);
+const char *nm_ndisc_get_ifname (NMNDisc *self);
+NMNDiscNodeType nm_ndisc_get_node_type (NMNDisc *self);
+
+gboolean nm_ndisc_set_iid (NMNDisc *ndisc, const NMUtilsIPv6IfaceId iid);
+void nm_ndisc_start (NMNDisc *ndisc);
+void nm_ndisc_dad_failed (NMNDisc *ndisc, struct in6_addr *address);
+void nm_ndisc_set_config (NMNDisc *ndisc,
+                          const GArray *addresses,
+                          const GArray *dns_servers,
+                          const GArray *dns_domains);
+
+NMPlatform *nm_ndisc_get_platform (NMNDisc *self);
+NMPNetns *nm_ndisc_netns_get (NMNDisc *self);
+gboolean nm_ndisc_netns_push (NMNDisc *self, NMPNetns **netns);
+
+#endif /* __NETWORKMANAGER_NDISC_H__ */
diff --git a/src/ndisc/tests/test-ndisc-fake.c b/src/ndisc/tests/test-ndisc-fake.c
new file mode 100644
index 00000000..006aea7f
--- /dev/null
+++ b/src/ndisc/tests/test-ndisc-fake.c
@@ -0,0 +1,483 @@
+/* -*- Mode: C; tab-width: 4; indent-tabs-mode: t; c-basic-offset: 4 -*- */
+/* ndisc.c - test program
+ *
+ * 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) 2015 Red Hat, Inc.
+ */
+
+#include "nm-default.h"
+
+#include <string.h>
+#include <syslog.h>
+
+#include "ndisc/nm-ndisc.h"
+#include "ndisc/nm-fake-ndisc.h"
+
+#include "platform/nm-fake-platform.h"
+
+#include "nm-test-utils-core.h"
+
+static NMFakeNDisc *
+ndisc_new (void)
+{
+	NMNDisc *ndisc;
+	const int ifindex = 1;
+	const char *ifname = nm_platform_link_get_name (NM_PLATFORM_GET, ifindex);
+	NMUtilsIPv6IfaceId iid = { };
+
+	ndisc = nm_fake_ndisc_new (ifindex, ifname);
+	iid.id_u8[7] = 1;
+	nm_ndisc_set_iid (ndisc, iid);
+	g_assert (ndisc);
+	return NM_FAKE_NDISC (ndisc);
+}
+
+static void
+match_gateway (const NMNDiscData *rdata, guint idx, const char *addr, guint32 ts, guint32 lt, NMNDiscPreference pref)
+{
+	const NMNDiscGateway *gw;
+	char buf[INET6_ADDRSTRLEN];
+
+	g_assert (rdata);
+	g_assert_cmpint (idx, <, rdata->gateways_n);
+	g_assert (rdata->gateways);
+
+	gw = &rdata->gateways[idx];
+
+	g_assert_cmpstr (inet_ntop (AF_INET6, &gw->address, buf, sizeof (buf)), ==, addr);
+	g_assert_cmpint (gw->timestamp, ==, ts);
+	g_assert_cmpint (gw->lifetime, ==, lt);
+	g_assert_cmpint (gw->preference, ==, pref);
+}
+
+static void
+match_address (const NMNDiscData *rdata, guint idx, const char *addr, guint32 ts, guint32 lt, guint32 preferred)
+{
+	const NMNDiscAddress *a;
+	char buf[INET6_ADDRSTRLEN];
+
+	g_assert (rdata);
+	g_assert_cmpint (idx, <, rdata->addresses_n);
+	g_assert (rdata->addresses);
+
+	a = &rdata->addresses[idx];
+
+	g_assert_cmpstr (inet_ntop (AF_INET6, &a->address, buf, sizeof (buf)), ==, addr);
+	g_assert_cmpint (a->timestamp, ==, ts);
+	g_assert_cmpint (a->lifetime, ==, lt);
+	g_assert_cmpint (a->preferred, ==, preferred);
+}
+
+static void
+match_route (const NMNDiscData *rdata, guint idx, const char *nw, int plen, const char *gw, guint32 ts, guint32 lt, NMNDiscPreference pref)
+{
+	const NMNDiscRoute *route;
+	char buf[INET6_ADDRSTRLEN];
+
+	g_assert (rdata);
+	g_assert_cmpint (idx, <, rdata->routes_n);
+	g_assert (rdata->routes);
+	g_assert (plen > 0 && plen <= 128);
+
+	route = &rdata->routes[idx];
+
+	g_assert_cmpstr (inet_ntop (AF_INET6, &route->network, buf, sizeof (buf)), ==, nw);
+	g_assert_cmpint ((int) route->plen, ==, plen);
+	g_assert_cmpstr (inet_ntop (AF_INET6, &route->gateway, buf, sizeof (buf)), ==, gw);
+	g_assert_cmpint (route->timestamp, ==, ts);
+	g_assert_cmpint (route->lifetime, ==, lt);
+	g_assert_cmpint (route->preference, ==, pref);
+}
+
+static void
+match_dns_server (const NMNDiscData *rdata, guint idx, const char *addr, guint32 ts, guint32 lt)
+{
+	const NMNDiscDNSServer *dns;
+	char buf[INET6_ADDRSTRLEN];
+
+	g_assert (rdata);
+	g_assert_cmpint (idx, <, rdata->dns_servers_n);
+	g_assert (rdata->dns_servers);
+
+	dns = &rdata->dns_servers[idx];
+
+	g_assert_cmpstr (inet_ntop (AF_INET6, &dns->address, buf, sizeof (buf)), ==, addr);
+	g_assert_cmpint (dns->timestamp, ==, ts);
+	g_assert_cmpint (dns->lifetime, ==, lt);
+}
+
+static void
+match_dns_domain (const NMNDiscData *rdata, guint idx, const char *domain, guint32 ts, guint32 lt)
+{
+	const NMNDiscDNSDomain *dns;
+
+	g_assert (rdata);
+	g_assert_cmpint (idx, <, rdata->dns_domains_n);
+	g_assert (rdata->dns_domains);
+
+	dns = &rdata->dns_domains[idx];
+
+	g_assert_cmpstr (dns->domain, ==, domain);
+	g_assert_cmpint (dns->timestamp, ==, ts);
+	g_assert_cmpint (dns->lifetime, ==, lt);
+}
+
+typedef struct {
+	GMainLoop *loop;
+	guint counter;
+	guint rs_counter;
+	guint32 timestamp1;
+	guint32 first_solicit;
+	guint32 timeout_id;
+} TestData;
+
+static void
+test_simple_changed (NMNDisc *ndisc, const NMNDiscData *rdata, guint changed_int, TestData *data)
+{
+	NMNDiscConfigMap changed = changed_int;
+
+	g_assert_cmpint (changed, ==, NM_NDISC_CONFIG_DHCP_LEVEL |
+	                              NM_NDISC_CONFIG_GATEWAYS |
+	                              NM_NDISC_CONFIG_ADDRESSES |
+	                              NM_NDISC_CONFIG_ROUTES |
+	                              NM_NDISC_CONFIG_DNS_SERVERS |
+	                              NM_NDISC_CONFIG_DNS_DOMAINS |
+	                              NM_NDISC_CONFIG_HOP_LIMIT |
+	                              NM_NDISC_CONFIG_MTU);
+	g_assert_cmpint (rdata->dhcp_level, ==, NM_NDISC_DHCP_LEVEL_OTHERCONF);
+	match_gateway (rdata, 0, "fe80::1", data->timestamp1, 10, NM_NDISC_PREFERENCE_MEDIUM);
+	match_address (rdata, 0, "2001:db8:a:a::1", data->timestamp1, 10, 10);
+	match_route (rdata, 0, "2001:db8:a:a::", 64, "fe80::1", data->timestamp1, 10, 10);
+	match_dns_server (rdata, 0, "2001:db8:c:c::1", data->timestamp1, 10);
+	match_dns_domain (rdata, 0, "foobar.com", data->timestamp1, 10);
+
+	g_assert (nm_fake_ndisc_done (NM_FAKE_NDISC (ndisc)));
+	data->counter++;
+	g_main_loop_quit (data->loop);
+}
+
+static void
+test_simple (void)
+{
+	NMFakeNDisc *ndisc = ndisc_new ();
+	guint32 now = nm_utils_get_monotonic_timestamp_s ();
+	TestData data = { g_main_loop_new (NULL, FALSE), 0, 0, now };
+	guint id;
+
+	id = nm_fake_ndisc_add_ra (ndisc, 1, NM_NDISC_DHCP_LEVEL_OTHERCONF, 4, 1500);
+	g_assert (id);
+	nm_fake_ndisc_add_gateway (ndisc, id, "fe80::1", now, 10, NM_NDISC_PREFERENCE_MEDIUM);
+	nm_fake_ndisc_add_prefix (ndisc, id, "2001:db8:a:a::", 64, "fe80::1", now, 10, 10, 10);
+	nm_fake_ndisc_add_dns_server (ndisc, id, "2001:db8:c:c::1", now, 10);
+	nm_fake_ndisc_add_dns_domain (ndisc, id, "foobar.com", now, 10);
+
+	g_signal_connect (ndisc,
+	                  NM_NDISC_CONFIG_RECEIVED,
+	                  G_CALLBACK (test_simple_changed),
+	                  &data);
+
+	nm_ndisc_start (NM_NDISC (ndisc));
+	g_main_loop_run (data.loop);
+	g_assert_cmpint (data.counter, ==, 1);
+
+	g_object_unref (ndisc);
+	g_main_loop_unref (data.loop);
+}
+
+static void
+test_everything_rs_sent (NMNDisc *ndisc, TestData *data)
+{
+	g_assert_cmpint (data->rs_counter, ==, 0);
+	data->rs_counter++;
+}
+
+static void
+test_everything_changed (NMNDisc *ndisc, const NMNDiscData *rdata, guint changed_int, TestData *data)
+{
+	NMNDiscConfigMap changed = changed_int;
+
+	if (data->counter == 0) {
+		g_assert_cmpint (data->rs_counter, ==, 1);
+		g_assert_cmpint (changed, ==, NM_NDISC_CONFIG_DHCP_LEVEL |
+		                              NM_NDISC_CONFIG_GATEWAYS |
+		                              NM_NDISC_CONFIG_ADDRESSES |
+		                              NM_NDISC_CONFIG_ROUTES |
+		                              NM_NDISC_CONFIG_DNS_SERVERS |
+		                              NM_NDISC_CONFIG_DNS_DOMAINS |
+		                              NM_NDISC_CONFIG_HOP_LIMIT |
+		                              NM_NDISC_CONFIG_MTU);
+		match_gateway (rdata, 0, "fe80::1", data->timestamp1, 10, NM_NDISC_PREFERENCE_MEDIUM);
+		match_address (rdata, 0, "2001:db8:a:a::1", data->timestamp1, 10, 10);
+		match_route (rdata, 0, "2001:db8:a:a::", 64, "fe80::1", data->timestamp1, 10, 10);
+		match_dns_server (rdata, 0, "2001:db8:c:c::1", data->timestamp1, 10);
+		match_dns_domain (rdata, 0, "foobar.com", data->timestamp1, 10);
+	} else if (data->counter == 1) {
+		g_assert_cmpint (changed, ==, NM_NDISC_CONFIG_GATEWAYS |
+		                              NM_NDISC_CONFIG_ADDRESSES |
+		                              NM_NDISC_CONFIG_ROUTES |
+		                              NM_NDISC_CONFIG_DNS_SERVERS |
+		                              NM_NDISC_CONFIG_DNS_DOMAINS);
+
+		g_assert_cmpint (rdata->gateways_n, ==, 1);
+		match_gateway (rdata, 0, "fe80::2", data->timestamp1, 10, NM_NDISC_PREFERENCE_MEDIUM);
+		g_assert_cmpint (rdata->addresses_n, ==, 1);
+		match_address (rdata, 0, "2001:db8:a:b::1", data->timestamp1, 10, 10);
+		g_assert_cmpint (rdata->routes_n, ==, 1);
+		match_route (rdata, 0, "2001:db8:a:b::", 64, "fe80::2", data->timestamp1, 10, 10);
+		g_assert_cmpint (rdata->dns_servers_n, ==, 1);
+		match_dns_server (rdata, 0, "2001:db8:c:c::2", data->timestamp1, 10);
+		g_assert_cmpint (rdata->dns_domains_n, ==, 1);
+		match_dns_domain (rdata, 0, "foobar2.com", data->timestamp1, 10);
+
+		g_assert (nm_fake_ndisc_done (NM_FAKE_NDISC (ndisc)));
+		g_main_loop_quit (data->loop);
+	} else
+		g_assert_not_reached ();
+
+	data->counter++;
+}
+
+static void
+test_everything (void)
+{
+	NMFakeNDisc *ndisc = ndisc_new ();
+	guint32 now = nm_utils_get_monotonic_timestamp_s ();
+	TestData data = { g_main_loop_new (NULL, FALSE), 0, 0, now };
+	guint id;
+
+	id = nm_fake_ndisc_add_ra (ndisc, 1, NM_NDISC_DHCP_LEVEL_NONE, 4, 1500);
+	g_assert (id);
+	nm_fake_ndisc_add_gateway (ndisc, id, "fe80::1", now, 10, NM_NDISC_PREFERENCE_MEDIUM);
+	nm_fake_ndisc_add_prefix (ndisc, id, "2001:db8:a:a::", 64, "fe80::1", now, 10, 10, 10);
+	nm_fake_ndisc_add_dns_server (ndisc, id, "2001:db8:c:c::1", now, 10);
+	nm_fake_ndisc_add_dns_domain (ndisc, id, "foobar.com", now, 10);
+
+	/* expire everything from the first RA in the second */
+	id = nm_fake_ndisc_add_ra (ndisc, 1, NM_NDISC_DHCP_LEVEL_NONE, 4, 1500);
+	g_assert (id);
+	nm_fake_ndisc_add_gateway (ndisc, id, "fe80::1", now, 0, NM_NDISC_PREFERENCE_MEDIUM);
+	nm_fake_ndisc_add_prefix (ndisc, id, "2001:db8:a:a::", 64, "fe80::1", now, 0, 0, 0);
+	nm_fake_ndisc_add_dns_server (ndisc, id, "2001:db8:c:c::1", now, 0);
+	nm_fake_ndisc_add_dns_domain (ndisc, id, "foobar.com", now, 0);
+
+	/* and add some new stuff */
+	nm_fake_ndisc_add_gateway (ndisc, id, "fe80::2", now, 10, NM_NDISC_PREFERENCE_MEDIUM);
+	nm_fake_ndisc_add_prefix (ndisc, id, "2001:db8:a:b::", 64, "fe80::2", now, 10, 10, 10);
+	nm_fake_ndisc_add_dns_server (ndisc, id, "2001:db8:c:c::2", now, 10);
+	nm_fake_ndisc_add_dns_domain (ndisc, id, "foobar2.com", now, 10);
+
+	g_signal_connect (ndisc,
+	                  NM_NDISC_CONFIG_RECEIVED,
+	                  G_CALLBACK (test_everything_changed),
+	                  &data);
+	g_signal_connect (ndisc,
+	                  NM_FAKE_NDISC_RS_SENT,
+	                  G_CALLBACK (test_everything_rs_sent),
+	                  &data);
+
+	nm_ndisc_start (NM_NDISC (ndisc));
+	g_main_loop_run (data.loop);
+	g_assert_cmpint (data.counter, ==, 2);
+	g_assert_cmpint (data.rs_counter, ==, 1);
+
+	g_object_unref (ndisc);
+	g_main_loop_unref (data.loop);
+}
+
+static void
+test_preference_changed (NMNDisc *ndisc, const NMNDiscData *rdata, guint changed_int, TestData *data)
+{
+	NMNDiscConfigMap changed = changed_int;
+
+	if (data->counter == 1) {
+		g_assert_cmpint (changed, ==, NM_NDISC_CONFIG_GATEWAYS |
+		                              NM_NDISC_CONFIG_ADDRESSES |
+		                              NM_NDISC_CONFIG_ROUTES);
+		g_assert_cmpint (rdata->gateways_n, ==, 2);
+		match_gateway (rdata, 0, "fe80::2", data->timestamp1 + 1, 10, NM_NDISC_PREFERENCE_MEDIUM);
+		match_gateway (rdata, 1, "fe80::1", data->timestamp1, 10, NM_NDISC_PREFERENCE_LOW);
+		g_assert_cmpint (rdata->addresses_n, ==, 2);
+		match_address (rdata, 0, "2001:db8:a:a::1", data->timestamp1, 10, 10);
+		match_address (rdata, 1, "2001:db8:a:b::1", data->timestamp1 + 1, 10, 10);
+		g_assert_cmpint (rdata->routes_n, ==, 2);
+		match_route (rdata, 0, "2001:db8:a:b::", 64, "fe80::2", data->timestamp1 + 1, 10, 10);
+		match_route (rdata, 1, "2001:db8:a:a::", 64, "fe80::1", data->timestamp1, 10, 5);
+	} else if (data->counter == 2) {
+		g_assert_cmpint (changed, ==, NM_NDISC_CONFIG_GATEWAYS |
+		                              NM_NDISC_CONFIG_ADDRESSES |
+		                              NM_NDISC_CONFIG_ROUTES);
+
+		g_assert_cmpint (rdata->gateways_n, ==, 2);
+		match_gateway (rdata, 0, "fe80::1", data->timestamp1 + 2, 10, NM_NDISC_PREFERENCE_HIGH);
+		match_gateway (rdata, 1, "fe80::2", data->timestamp1 + 1, 10, NM_NDISC_PREFERENCE_MEDIUM);
+		g_assert_cmpint (rdata->addresses_n, ==, 2);
+		match_address (rdata, 0, "2001:db8:a:a::1", data->timestamp1 + 2, 10, 10);
+		match_address (rdata, 1, "2001:db8:a:b::1", data->timestamp1 + 1, 10, 10);
+		g_assert_cmpint (rdata->routes_n, ==, 2);
+		match_route (rdata, 0, "2001:db8:a:a::", 64, "fe80::1", data->timestamp1 + 2, 10, 15);
+		match_route (rdata, 1, "2001:db8:a:b::", 64, "fe80::2", data->timestamp1 + 1, 10, 10);
+
+		g_assert (nm_fake_ndisc_done (NM_FAKE_NDISC (ndisc)));
+		g_main_loop_quit (data->loop);
+	}
+
+	data->counter++;
+}
+
+static void
+test_preference (void)
+{
+	NMFakeNDisc *ndisc = ndisc_new ();
+	guint32 now = nm_utils_get_monotonic_timestamp_s ();
+	TestData data = { g_main_loop_new (NULL, FALSE), 0, 0, now };
+	guint id;
+
+	/* Test that when a low-preference and medium gateway send advertisements,
+	 * that if the low-preference gateway switches to high-preference, we do
+	 * not get duplicates in the gateway list.
+	 */
+
+	id = nm_fake_ndisc_add_ra (ndisc, 1, NM_NDISC_DHCP_LEVEL_NONE, 4, 1500);
+	g_assert (id);
+	nm_fake_ndisc_add_gateway (ndisc, id, "fe80::1", now, 10, NM_NDISC_PREFERENCE_LOW);
+	nm_fake_ndisc_add_prefix (ndisc, id, "2001:db8:a:a::", 64, "fe80::1", now, 10, 10, 5);
+
+	id = nm_fake_ndisc_add_ra (ndisc, 1, NM_NDISC_DHCP_LEVEL_NONE, 4, 1500);
+	g_assert (id);
+	nm_fake_ndisc_add_gateway (ndisc, id, "fe80::2", ++now, 10, NM_NDISC_PREFERENCE_MEDIUM);
+	nm_fake_ndisc_add_prefix (ndisc, id, "2001:db8:a:b::", 64, "fe80::2", now, 10, 10, 10);
+
+	id = nm_fake_ndisc_add_ra (ndisc, 1, NM_NDISC_DHCP_LEVEL_NONE, 4, 1500);
+	g_assert (id);
+	nm_fake_ndisc_add_gateway (ndisc, id, "fe80::1", ++now, 10, NM_NDISC_PREFERENCE_HIGH);
+	nm_fake_ndisc_add_prefix (ndisc, id, "2001:db8:a:a::", 64, "fe80::1", now, 10, 10, 15);
+
+	g_signal_connect (ndisc,
+	                  NM_NDISC_CONFIG_RECEIVED,
+	                  G_CALLBACK (test_preference_changed),
+	                  &data);
+
+	nm_ndisc_start (NM_NDISC (ndisc));
+	g_main_loop_run (data.loop);
+	g_assert_cmpint (data.counter, ==, 3);
+
+	g_object_unref (ndisc);
+	g_main_loop_unref (data.loop);
+}
+
+static void
+test_dns_solicit_loop_changed (NMNDisc *ndisc, const NMNDiscData *rdata, guint changed_int, TestData *data)
+{
+	data->counter++;
+}
+
+static gboolean
+success_timeout (TestData *data)
+{
+	data->timeout_id = 0;
+	g_main_loop_quit (data->loop);
+	return G_SOURCE_REMOVE;
+}
+
+static void
+test_dns_solicit_loop_rs_sent (NMFakeNDisc *ndisc, TestData *data)
+{
+	guint32 now = nm_utils_get_monotonic_timestamp_s ();
+	guint id;
+
+	if (data->rs_counter > 0 && data->rs_counter < 6) {
+		if (data->rs_counter == 1) {
+			data->first_solicit = now;
+			/* Kill the test after 10 seconds if it hasn't failed yet */
+			data->timeout_id = g_timeout_add_seconds (10, (GSourceFunc) success_timeout, data);
+		}
+
+		/* On all but the first solicitation, which should be triggered by the
+		 * DNS servers reaching 1/2 lifetime, emit a new RA without the DNS
+		 * servers again.
+		 */
+		id = nm_fake_ndisc_add_ra (ndisc, 0, NM_NDISC_DHCP_LEVEL_NONE, 4, 1500);
+		g_assert (id);
+		nm_fake_ndisc_add_gateway (ndisc, id, "fe80::1", now, 10, NM_NDISC_PREFERENCE_MEDIUM);
+
+		nm_fake_ndisc_emit_new_ras (ndisc);
+	} else if (data->rs_counter >= 6) {
+		/* Fail if we've sent too many solicitations in the past 4 seconds */
+		g_assert_cmpint (now - data->first_solicit, >, 4);
+		g_source_remove (data->timeout_id);
+		g_main_loop_quit (data->loop);
+	}
+	data->rs_counter++;
+}
+
+static void
+test_dns_solicit_loop (void)
+{
+	NMFakeNDisc *ndisc = ndisc_new ();
+	guint32 now = nm_utils_get_monotonic_timestamp_s ();
+	TestData data = { g_main_loop_new (NULL, FALSE), 0, 0, now, 0 };
+	guint id;
+
+	/* Ensure that no solicitation loop happens when DNS servers or domains
+	 * stop being sent in advertisements.  This can happen if two routers
+	 * send RAs, but the one sending DNS info stops responding, or if one
+	 * router removes the DNS info from the RA without zero-lifetiming them
+	 * first.
+	 */
+
+	id = nm_fake_ndisc_add_ra (ndisc, 1, NM_NDISC_DHCP_LEVEL_NONE, 4, 1500);
+	g_assert (id);
+	nm_fake_ndisc_add_gateway (ndisc, id, "fe80::1", now, 10, NM_NDISC_PREFERENCE_LOW);
+	nm_fake_ndisc_add_dns_server (ndisc, id, "2001:db8:c:c::1", now, 6);
+
+	g_signal_connect (ndisc,
+	                  NM_NDISC_CONFIG_RECEIVED,
+	                  G_CALLBACK (test_dns_solicit_loop_changed),
+	                  &data);
+	g_signal_connect (ndisc,
+	                  NM_FAKE_NDISC_RS_SENT,
+	                  G_CALLBACK (test_dns_solicit_loop_rs_sent),
+	                  &data);
+
+	nm_ndisc_start (NM_NDISC (ndisc));
+	g_main_loop_run (data.loop);
+	g_assert_cmpint (data.counter, ==, 3);
+
+	g_object_unref (ndisc);
+	g_main_loop_unref (data.loop);
+}
+
+NMTST_DEFINE ();
+
+int
+main (int argc, char **argv)
+{
+	nmtst_init_with_logging (&argc, &argv, NULL, "DEFAULT");
+
+	if (nmtst_test_quick ()) {
+		g_print ("Skipping test: don't run long running test %s (NMTST_DEBUG=slow)\n", g_get_prgname () ?: "test-ndisc-fake");
+		return g_test_run ();
+	}
+
+	nm_fake_platform_setup ();
+
+	g_test_add_func ("/ndisc/simple", test_simple);
+	g_test_add_func ("/ndisc/everything-changed", test_everything);
+	g_test_add_func ("/ndisc/preference-changed", test_preference);
+	g_test_add_func ("/ndisc/dns-solicit-loop", test_dns_solicit_loop);
+
+	return g_test_run ();
+}
diff --git a/src/ndisc/tests/test-ndisc-linux.c b/src/ndisc/tests/test-ndisc-linux.c
new file mode 100644
index 00000000..2764b6c0
--- /dev/null
+++ b/src/ndisc/tests/test-ndisc-linux.c
@@ -0,0 +1,86 @@
+/* -*- Mode: C; tab-width: 4; indent-tabs-mode: t; c-basic-offset: 4 -*- */
+/* ndisc.c - test program
+ *
+ * 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 <string.h>
+#include <syslog.h>
+
+#include "ndisc/nm-ndisc.h"
+#include "ndisc/nm-lndp-ndisc.h"
+
+#include "platform/nm-linux-platform.h"
+
+#include "nm-test-utils-core.h"
+
+NMTST_DEFINE ();
+
+int
+main (int argc, char **argv)
+{
+	GMainLoop *loop;
+	NMNDisc *ndisc;
+	int ifindex = 1;
+	const char *ifname;
+	NMUtilsIPv6IfaceId iid = { };
+	GError *error = NULL;
+
+	nmtst_init_with_logging (&argc, &argv, NULL, "DEFAULT");
+
+	if (getuid () != 0) {
+		g_print ("Missing permission: must run as root\n");
+		return EXIT_FAILURE;
+	}
+
+	loop = g_main_loop_new (NULL, FALSE);
+
+	nm_linux_platform_setup ();
+
+	if (argv[1]) {
+		ifname = argv[1];
+		ifindex = nm_platform_link_get_ifindex (NM_PLATFORM_GET, ifname);
+	} else {
+		g_print ("Missing command line argument \"interface-name\"\n");
+		return EXIT_FAILURE;
+	}
+
+	ndisc = nm_lndp_ndisc_new (NM_PLATFORM_GET,
+	                           ifindex,
+	                           ifname,
+	                           NM_UTILS_STABLE_TYPE_UUID,
+	                           "8ce666e8-d34d-4fb1-b858-f15a7al28086",
+	                           NM_SETTING_IP6_CONFIG_ADDR_GEN_MODE_EUI64,
+	                           NM_NDISC_NODE_TYPE_HOST,
+	                           &error);
+	if (!ndisc) {
+		g_print ("Failed to create NMNDisc instance: %s\n", error->message);
+		g_error_free (error);
+		return EXIT_FAILURE;
+	}
+
+	iid.id_u8[7] = 1;
+	nm_ndisc_set_iid (ndisc, iid);
+	nm_ndisc_start (ndisc);
+	g_main_loop_run (loop);
+
+	g_clear_object (&ndisc);
+
+	return EXIT_SUCCESS;
+}