ip6: pad RDNSS and DNSSL lifetimes to a minimum lifetime value (rh #60055)
[NetworkManager.git] / src / ip6-manager / nm-ip6-manager.c
index f41169c..8f05ddc 100644 (file)
  * with this program; if not, write to the Free Software Foundation, Inc.,
  * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
  *
- * Copyright (C) 2009 Red Hat, Inc.
+ * Copyright (C) 2009 - 2010 Red Hat, Inc.
  */
 
 #include <errno.h>
 #include <netinet/icmp6.h>
 
+#include <netlink/route/addr.h>
 #include <netlink/route/rtnl.h>
 #include <netlink/route/route.h>
 
 #include "nm-ip6-manager.h"
-#include "nm-netlink-listener.h"
+#include "nm-netlink-monitor.h"
+#include "nm-netlink-utils.h"
+#include "nm-netlink-compat.h"
 #include "NetworkManagerUtils.h"
 #include "nm-marshal.h"
-#include "nm-utils.h"
+#include "nm-logging.h"
 
 /* Pre-DHCP addrconf timeout, in seconds */
-#define NM_IP6_TIMEOUT 10
+#define NM_IP6_TIMEOUT 20
 
 /* FIXME? Stolen from the kernel sources */
 #define IF_RA_OTHERCONF 0x80
 #define IF_RS_SENT      0x10
 
 typedef struct {
-       NMNetlinkListener *netlink;
-       GHashTable *devices_by_iface, *devices_by_index;
+       NMNetlinkMonitor *monitor;
+       GHashTable *devices;
 
-       struct nl_handle *nlh;
+       struct nl_sock *nlh;
        struct nl_cache *addr_cache, *route_cache;
+
+       guint netlink_id;
 } NMIP6ManagerPrivate;
 
 #define NM_IP6_MANAGER_GET_PRIVATE(o) (G_TYPE_INSTANCE_GET_PRIVATE ((o), NM_TYPE_IP6_MANAGER, NMIP6ManagerPrivate))
 
+G_DEFINE_TYPE (NMIP6Manager, nm_ip6_manager, G_TYPE_OBJECT)
+
+enum {
+       ADDRCONF_COMPLETE,
+       CONFIG_CHANGED,
+       LAST_SIGNAL
+};
+
+static guint signals[LAST_SIGNAL] = { 0 };
+
 typedef enum {
        NM_IP6_DEVICE_UNCONFIGURED,
        NM_IP6_DEVICE_GOT_LINK_LOCAL,
        NM_IP6_DEVICE_GOT_ROUTER_ADVERTISEMENT,
        NM_IP6_DEVICE_GOT_ADDRESS,
-       NM_IP6_DEVICE_WAITING_FOR_DHCP,
-       NM_IP6_DEVICE_GOT_DHCP,
        NM_IP6_DEVICE_TIMED_OUT
 } NMIP6DeviceState;
 
@@ -65,13 +78,24 @@ typedef struct {
 } NMIP6RDNSS;
 
 typedef struct {
+       char domain[256];
+       time_t expires;
+} NMIP6DNSSL;
+
+/******************************************************************/
+
+typedef struct {
        NMIP6Manager *manager;
        char *iface;
-       int index;
+       int ifindex;
+
+       gboolean has_linklocal;
+       gboolean has_nonlinklocal;
+       guint dhcp_opts;
 
-       char *accept_ra_path;
-       gboolean accept_ra_save_valid;
-       guint32 accept_ra_save;
+       char *disable_ip6_path;
+       gboolean disable_ip6_save_valid;
+       gint32 disable_ip6_save;
 
        guint finish_addrconf_id;
        guint config_changed_id;
@@ -82,103 +106,21 @@ typedef struct {
 
        GArray *rdnss_servers;
        guint rdnss_timeout_id;
-} NMIP6Device;
-
-G_DEFINE_TYPE (NMIP6Manager, nm_ip6_manager, G_TYPE_OBJECT)
-
-enum {
-       ADDRCONF_COMPLETE,
-       CONFIG_CHANGED,
-       LAST_SIGNAL
-};
-
-static guint signals[LAST_SIGNAL] = { 0 };
-
-static NMIP6Manager *nm_ip6_manager_new (void);
-
-static void netlink_notification (NMNetlinkListener *listener, struct nl_msg *msg, gpointer user_data);
-
-static void nm_ip6_device_destroy (NMIP6Device *device);
-
-NMIP6Manager *
-nm_ip6_manager_get (void)
-{
-       static NMIP6Manager *singleton = NULL;
-
-       if (!singleton)
-               singleton = nm_ip6_manager_new ();
-       g_assert (singleton);
-
-       return g_object_ref (singleton);
-}
-
-static void
-nm_ip6_manager_init (NMIP6Manager *manager)
-{
-       NMIP6ManagerPrivate *priv = NM_IP6_MANAGER_GET_PRIVATE (manager);
-
-       priv->devices_by_iface = g_hash_table_new_full (g_str_hash, g_str_equal,
-                                                                                                       NULL,
-                                                                                                       (GDestroyNotify) nm_ip6_device_destroy);
-       priv->devices_by_index = g_hash_table_new (NULL, NULL);
-
-       priv->netlink = nm_netlink_listener_get ();
-       g_signal_connect (priv->netlink, "notification",
-                                         G_CALLBACK (netlink_notification), manager);
-       nm_netlink_listener_subscribe (priv->netlink, RTNLGRP_IPV6_IFADDR, NULL);
-       nm_netlink_listener_subscribe (priv->netlink, RTNLGRP_IPV6_PREFIX, NULL);
-       nm_netlink_listener_subscribe (priv->netlink, RTNLGRP_ND_USEROPT, NULL);
-
-       priv->nlh = nm_netlink_get_default_handle ();
-       priv->addr_cache = rtnl_addr_alloc_cache (priv->nlh);
-       priv->route_cache = rtnl_route_alloc_cache (priv->nlh);
-}
 
-static void
-finalize (GObject *object)
-{
-       NMIP6ManagerPrivate *priv = NM_IP6_MANAGER_GET_PRIVATE (object);
+       GArray *dnssl_domains;
+       guint dnssl_timeout_id;
 
-       g_hash_table_destroy (priv->devices_by_iface);
-       g_hash_table_destroy (priv->devices_by_index);
-       g_object_unref (priv->netlink);
-       nl_cache_free (priv->addr_cache);
-       nl_cache_free (priv->route_cache);
+       guint ip6flags_poll_id;
 
-       G_OBJECT_CLASS (nm_ip6_manager_parent_class)->finalize (object);
-}
+       guint32 ra_flags;
+} NMIP6Device;
 
 static void
-nm_ip6_manager_class_init (NMIP6ManagerClass *manager_class)
+clear_config_changed (NMIP6Device *device)
 {
-       GObjectClass *object_class = G_OBJECT_CLASS (manager_class);
-
-       g_type_class_add_private (manager_class, sizeof (NMIP6ManagerPrivate));
-
-       /* virtual methods */
-       object_class->finalize = finalize;
-
-       /* signals */
-       signals[ADDRCONF_COMPLETE] =
-               g_signal_new ("addrconf-complete",
-                                         G_OBJECT_CLASS_TYPE (object_class),
-                                         G_SIGNAL_RUN_FIRST,
-                                         G_STRUCT_OFFSET (NMIP6ManagerClass, addrconf_complete),
-                                         NULL, NULL,
-                                         _nm_marshal_VOID__STRING_BOOLEAN,
-                                         G_TYPE_NONE, 2,
-                                         G_TYPE_STRING,
-                                         G_TYPE_BOOLEAN);
-
-       signals[CONFIG_CHANGED] =
-               g_signal_new ("config-changed",
-                                         G_OBJECT_CLASS_TYPE (object_class),
-                                         G_SIGNAL_RUN_FIRST,
-                                         G_STRUCT_OFFSET (NMIP6ManagerClass, config_changed),
-                                         NULL, NULL,
-                                         g_cclosure_marshal_VOID__STRING,
-                                         G_TYPE_NONE, 1,
-                                         G_TYPE_STRING);
+       if (device->config_changed_id)
+               g_source_remove (device->config_changed_id);
+       device->config_changed_id = 0;
 }
 
 static void
@@ -186,77 +128,166 @@ nm_ip6_device_destroy (NMIP6Device *device)
 {
        g_return_if_fail (device != NULL);
 
-       /* reset the saved RA value */
-       if (device->accept_ra_save_valid) {
-               nm_utils_do_sysctl (device->accept_ra_path,
-                                   device->accept_ra_save ? "1\n" : "0\n");
+       /* reset the saved IPv6 value */
+       if (device->disable_ip6_save_valid) {
+               nm_utils_do_sysctl (device->disable_ip6_path,
+                                   device->disable_ip6_save ? "1\n" : "0\n");
        }
 
        if (device->finish_addrconf_id)
                g_source_remove (device->finish_addrconf_id);
-       if (device->config_changed_id)
-               g_source_remove (device->config_changed_id);
+
+       clear_config_changed (device);
+
        g_free (device->iface);
        if (device->rdnss_servers)
                g_array_free (device->rdnss_servers, TRUE);
        if (device->rdnss_timeout_id)
                g_source_remove (device->rdnss_timeout_id);
+       if (device->dnssl_domains)
+               g_array_free (device->dnssl_domains, TRUE);
+       if (device->dnssl_timeout_id)
+               g_source_remove (device->dnssl_timeout_id);
+       if (device->ip6flags_poll_id)
+               g_source_remove (device->ip6flags_poll_id);
 
-       g_free (device->accept_ra_path);
        g_slice_free (NMIP6Device, device);
 }
 
-static NMIP6Manager *
-nm_ip6_manager_new (void)
+static NMIP6Device *
+nm_ip6_device_new (NMIP6Manager *manager, int ifindex)
 {
-       NMIP6Manager *manager;
-       NMIP6ManagerPrivate *priv;
+       NMIP6ManagerPrivate *priv = NM_IP6_MANAGER_GET_PRIVATE (manager);
+       NMIP6Device *device;
 
-       manager = g_object_new (NM_TYPE_IP6_MANAGER, NULL);
-       priv = NM_IP6_MANAGER_GET_PRIVATE (manager);
+       g_return_val_if_fail (ifindex > 0, NULL);
 
-       if (!priv->devices_by_iface || !priv->devices_by_index) {
-               nm_warning ("Error: not enough memory to initialize IP6 manager tables");
-               g_object_unref (manager);
-               manager = NULL;
+       device = g_slice_new0 (NMIP6Device);
+       if (!device) {
+               nm_log_err (LOGD_IP6, "(%d): out of memory creating IP6 addrconf object.",
+                           ifindex);
+               return NULL;
        }
 
-       return manager;
+       device->ifindex = ifindex;
+       device->iface = nm_netlink_index_to_iface (ifindex);
+       if (!device->iface) {
+               nm_log_err (LOGD_IP6, "(%d): could not find interface name from index.",
+                           ifindex);
+               goto error;
+       }
+
+       device->manager = manager;
+
+       device->rdnss_servers = g_array_new (FALSE, FALSE, sizeof (NMIP6RDNSS));
+
+       device->dnssl_domains = g_array_new (FALSE, FALSE, sizeof (NMIP6DNSSL));
+
+       g_hash_table_replace (priv->devices, GINT_TO_POINTER (device->ifindex), device);
+
+       /* and the original value of IPv6 enable/disable */
+       device->disable_ip6_path = g_strdup_printf ("/proc/sys/net/ipv6/conf/%s/disable_ipv6",
+                                                   device->iface);
+       g_assert (device->disable_ip6_path);
+       device->disable_ip6_save_valid = nm_utils_get_proc_sys_net_value_with_bounds (device->disable_ip6_path,
+                                                                                     device->iface,
+                                                                                     &device->disable_ip6_save,
+                                                                                     0, 1);
+
+       return device;
+
+error:
+       nm_ip6_device_destroy (device);
+       return NULL;
 }
 
 static NMIP6Device *
 nm_ip6_manager_get_device (NMIP6Manager *manager, int ifindex)
 {
-       NMIP6ManagerPrivate *priv = NM_IP6_MANAGER_GET_PRIVATE (manager);
+       NMIP6ManagerPrivate *priv;
+
+       g_return_val_if_fail (manager != NULL, NULL);
+       g_return_val_if_fail (NM_IS_IP6_MANAGER (manager), NULL);
+
+       priv = NM_IP6_MANAGER_GET_PRIVATE (manager);
+       return g_hash_table_lookup (priv->devices, GINT_TO_POINTER (ifindex));
+}
+
+static char *
+device_get_iface (NMIP6Device *device)
+{
+       return device ? device->iface : "unknown";
+}
+
+static const char *
+state_to_string (NMIP6DeviceState state)
+{
+       switch (state) {
+       case NM_IP6_DEVICE_UNCONFIGURED:
+               return "unconfigured";
+       case NM_IP6_DEVICE_GOT_LINK_LOCAL:
+               return "got-link-local";
+       case NM_IP6_DEVICE_GOT_ROUTER_ADVERTISEMENT:
+               return "got-ra";
+       case NM_IP6_DEVICE_GOT_ADDRESS:
+               return "got-address";
+       case NM_IP6_DEVICE_TIMED_OUT:
+               return "timed-out";
+       default:
+               return "unknown";
+       }
+}
+
+static void
+device_set_state (NMIP6Device *device, NMIP6DeviceState state)
+{
+       NMIP6DeviceState oldstate;
+
+       g_return_if_fail (device != NULL);
 
-       return g_hash_table_lookup (priv->devices_by_index,
-                                                               GINT_TO_POINTER (ifindex));
+       oldstate = device->state;
+       device->state = state;
+       nm_log_dbg (LOGD_IP6, "(%s) IP6 device state: %s -> %s",
+                   device_get_iface (device), state_to_string (oldstate), state_to_string (state));
 }
 
+/******************************************************************/
+
+typedef struct {
+       NMIP6Device *device;
+       guint dhcp_opts;
+       gboolean success;
+} CallbackInfo;
+
 static gboolean
 finish_addrconf (gpointer user_data)
 {
-       NMIP6Device *device = user_data;
+       CallbackInfo *info = user_data;
+       NMIP6Device *device = info->device;
        NMIP6Manager *manager = device->manager;
-       char *iface_copy;
+       int ifindex;
 
        device->finish_addrconf_id = 0;
        device->addrconf_complete = TRUE;
+       ifindex = device->ifindex;
+
+       /* We're done, stop polling IPv6 flags */
+       if (device->ip6flags_poll_id) {
+               g_source_remove (device->ip6flags_poll_id);
+               device->ip6flags_poll_id = 0;
+       }
 
-       if (device->state >= device->target_state) {
+       /* And tell listeners that addrconf is complete */
+       if (info->success) {
                g_signal_emit (manager, signals[ADDRCONF_COMPLETE], 0,
-                                          device->iface, TRUE);
+                              ifindex, info->dhcp_opts, TRUE);
        } else {
-               nm_info ("Device '%s' IP6 addrconf timed out or failed.",
-                                device->iface);
+               nm_log_info (LOGD_IP6, "(%s): IP6 addrconf timed out or failed.",
+                            device->iface);
 
-               iface_copy = g_strdup (device->iface);
-
-               nm_ip6_manager_cancel_addrconf (manager, device->iface);
+               nm_ip6_manager_cancel_addrconf (manager, ifindex);
                g_signal_emit (manager, signals[ADDRCONF_COMPLETE], 0,
-                                          iface_copy, FALSE);
-
-               g_free (iface_copy);
+                              ifindex, info->dhcp_opts, FALSE);
        }
 
        return FALSE;
@@ -265,11 +296,15 @@ finish_addrconf (gpointer user_data)
 static gboolean
 emit_config_changed (gpointer user_data)
 {
-       NMIP6Device *device = user_data;
+       CallbackInfo *info = user_data;
+       NMIP6Device *device = info->device;
        NMIP6Manager *manager = device->manager;
 
        device->config_changed_id = 0;
-       g_signal_emit (manager, signals[CONFIG_CHANGED], 0, device->iface);
+       g_signal_emit (manager, signals[CONFIG_CHANGED], 0,
+                      device->ifindex,
+                      info->dhcp_opts,
+                      info->success);
        return FALSE;
 }
 
@@ -279,9 +314,13 @@ static gboolean
 rdnss_expired (gpointer user_data)
 {
        NMIP6Device *device = user_data;
+       CallbackInfo info = { device, IP6_DHCP_OPT_NONE, FALSE };
+
+       nm_log_dbg (LOGD_IP6, "(%s): IPv6 RDNSS information expired", device->iface);
 
        set_rdnss_timeout (device);
-       emit_config_changed (device);
+       clear_config_changed (device);
+       emit_config_changed (&info);
        return FALSE;
 }
 
@@ -308,7 +347,13 @@ set_rdnss_timeout (NMIP6Device *device)
                 * bit.
                 */
                if (rdnss->expires <= now + 1) {
-                       g_array_remove_index_fast (device->rdnss_servers, i--);
+                       char buf[INET6_ADDRSTRLEN + 1];
+
+                       if (inet_ntop (AF_INET6, &(rdnss->addr), buf, sizeof (buf)) > 0) {
+                               nm_log_dbg (LOGD_IP6, "(%s): removing expired RA-provided nameserver %s",
+                                           device->iface, buf);
+                       }
+                       g_array_remove_index (device->rdnss_servers, i--);
                        continue;
                }
 
@@ -317,27 +362,100 @@ set_rdnss_timeout (NMIP6Device *device)
        }
 
        if (expires) {
-               device->rdnss_timeout_id = g_timeout_add_seconds (expires - now,
-                                                                                                                 rdnss_expired,
-                                                                                                                 device);
+               device->rdnss_timeout_id = g_timeout_add_seconds (MIN (expires - now, G_MAXUINT32 - 1),
+                                                                 rdnss_expired,
+                                                                 device);
+       }
+}
+
+static void set_dnssl_timeout (NMIP6Device *device);
+
+static gboolean
+dnssl_expired (gpointer user_data)
+{
+       NMIP6Device *device = user_data;
+       CallbackInfo info = { device, IP6_DHCP_OPT_NONE, FALSE };
+
+       nm_log_dbg (LOGD_IP6, "(%s): IPv6 DNSSL information expired", device->iface);
+
+       set_dnssl_timeout (device);
+       clear_config_changed (device);
+       emit_config_changed (&info);
+       return FALSE;
+}
+
+static void
+set_dnssl_timeout (NMIP6Device *device)
+{
+       time_t expires = 0, now = time (NULL);
+       NMIP6DNSSL *dnssl;
+       int i;
+
+       if (device->dnssl_timeout_id) {
+               g_source_remove (device->dnssl_timeout_id);
+               device->dnssl_timeout_id = 0;
+       }
+
+       /* Find the soonest expiration time. */
+       for (i = 0; i < device->dnssl_domains->len; i++) {
+               dnssl = &g_array_index (device->dnssl_domains, NMIP6DNSSL, i);
+               if (dnssl->expires == 0)
+                       continue;
+
+               /* If the entry has already expired, remove it; the "+ 1" is
+                * because g_timeout_add_seconds() might fudge the timing a
+                * bit.
+                */
+               if (dnssl->expires <= now + 1) {
+                       nm_log_dbg (LOGD_IP6, "(%s): removing expired RA-provided domain %s",
+                                   device->iface, dnssl->domain);
+                       g_array_remove_index (device->dnssl_domains, i--);
+                       continue;
+               }
+
+               if (!expires || dnssl->expires < expires)
+                       expires = dnssl->expires;
+       }
+
+       if (expires) {
+               device->dnssl_timeout_id = g_timeout_add_seconds (MIN (expires - now, G_MAXUINT32 - 1),
+                                                                 dnssl_expired,
+                                                                 device);
        }
 }
 
+static CallbackInfo *
+callback_info_new (NMIP6Device *device, guint dhcp_opts, gboolean success)
+{
+       CallbackInfo *info;
+
+       info = g_malloc0 (sizeof (CallbackInfo));
+       info->device = device;
+       info->dhcp_opts = dhcp_opts;
+       info->success = success;
+       return info;
+}
+
 static void
-nm_ip6_device_sync_from_netlink (NMIP6Device *device, gboolean config_changed)
+check_addresses (NMIP6Device *device)
 {
        NMIP6Manager *manager = device->manager;
        NMIP6ManagerPrivate *priv = NM_IP6_MANAGER_GET_PRIVATE (manager);
        struct rtnl_addr *rtnladdr;
        struct nl_addr *nladdr;
        struct in6_addr *addr;
-       struct rtnl_link *link;
-       guint flags;
 
-       for (rtnladdr = (struct rtnl_addr *)nl_cache_get_first (priv->addr_cache);
+       /* Reset address information */
+       device->has_linklocal = FALSE;
+       device->has_nonlinklocal = FALSE;
+
+       /* Look for any IPv6 addresses the kernel may have set for the device */
+       for (rtnladdr = (struct rtnl_addr *) nl_cache_get_first (priv->addr_cache);
                 rtnladdr;
-                rtnladdr = (struct rtnl_addr *)nl_cache_get_next ((struct nl_object *)rtnladdr)) {
-               if (rtnl_addr_get_ifindex (rtnladdr) != device->index)
+                rtnladdr = (struct rtnl_addr *) nl_cache_get_next ((struct nl_object *) rtnladdr)) {
+               char buf[INET6_ADDRSTRLEN];
+
+               if (rtnl_addr_get_ifindex (rtnladdr) != device->ifindex)
                        continue;
 
                nladdr = rtnl_addr_get_local (rtnladdr);
@@ -345,46 +463,125 @@ nm_ip6_device_sync_from_netlink (NMIP6Device *device, gboolean config_changed)
                        continue;
 
                addr = nl_addr_get_binary_addr (nladdr);
+
+               if (inet_ntop (AF_INET6, addr, buf, INET6_ADDRSTRLEN) > 0) {
+                       nm_log_dbg (LOGD_IP6, "(%s): netlink address: %s/%d",
+                                   device->iface, buf,
+                                   rtnl_addr_get_prefixlen (rtnladdr));
+               }
+
                if (IN6_IS_ADDR_LINKLOCAL (addr)) {
                        if (device->state == NM_IP6_DEVICE_UNCONFIGURED)
-                               device->state = NM_IP6_DEVICE_GOT_LINK_LOCAL;
+                               device_set_state (device, NM_IP6_DEVICE_GOT_LINK_LOCAL);
+                       device->has_linklocal = TRUE;
                } else {
                        if (device->state < NM_IP6_DEVICE_GOT_ADDRESS)
-                               device->state = NM_IP6_DEVICE_GOT_ADDRESS;
+                               device_set_state (device, NM_IP6_DEVICE_GOT_ADDRESS);
+                       device->has_nonlinklocal = TRUE;
                }
        }
 
-       /* Note: we don't want to keep a cache of links, because the
-        * kernel doesn't send notifications when the flags change, so the
-        * cached rtnl_links would have out-of-date flags.
+       /* There might be a LL address hanging around on the interface from
+        * before in the initial run, but if it goes away later, make sure we
+        * regress from GOT_LINK_LOCAL back to UNCONFIGURED.
         */
-       link = nm_netlink_index_to_rtnl_link (device->index);
-       flags = rtnl_link_get_flags (link);
-       rtnl_link_put (link);
+       if ((device->state == NM_IP6_DEVICE_GOT_LINK_LOCAL) && !device->has_linklocal)
+               device_set_state (device, NM_IP6_DEVICE_UNCONFIGURED);
+
+       nm_log_dbg (LOGD_IP6, "(%s): addresses checked (state %s)",
+                   device->iface, state_to_string (device->state));
+}
+
+static void
+check_ra_flags (NMIP6Device *device)
+{
+       device->dhcp_opts = IP6_DHCP_OPT_NONE;
 
-       if ((flags & IF_RA_RCVD) && device->state < NM_IP6_DEVICE_GOT_ROUTER_ADVERTISEMENT)
-               device->state = NM_IP6_DEVICE_GOT_ROUTER_ADVERTISEMENT;
+       /* We only care about router advertisements if we want a real IPv6 address */
+       if (   (device->target_state == NM_IP6_DEVICE_GOT_ADDRESS)
+           && (device->ra_flags & IF_RA_RCVD)) {
+
+               if (device->state < NM_IP6_DEVICE_GOT_ROUTER_ADVERTISEMENT)
+                       device_set_state (device, NM_IP6_DEVICE_GOT_ROUTER_ADVERTISEMENT);
+
+               if (device->ra_flags & IF_RA_MANAGED) {
+                       device->dhcp_opts = IP6_DHCP_OPT_MANAGED;
+                       nm_log_dbg (LOGD_IP6, "router advertisement deferred to DHCPv6");
+               } else if (device->ra_flags & IF_RA_OTHERCONF) {
+                       device->dhcp_opts = IP6_DHCP_OPT_OTHERCONF;
+                       nm_log_dbg (LOGD_IP6, "router advertisement requests parallel DHCPv6");
+               }
+       }
+       nm_log_dbg (LOGD_IP6, "(%s): router advertisement checked (state %s)",
+                   device->iface, state_to_string (device->state));
+}
 
-//     if (flags & (IF_RA_MANAGED | IF_RA_OTHERCONF))
-//             device->need_dhcp = TRUE;
+static void
+check_addrconf_complete (NMIP6Device *device)
+{
+       CallbackInfo *info;
 
        if (!device->addrconf_complete) {
-               if (device->state >= device->target_state ||
-                       device->state == NM_IP6_DEVICE_GOT_ROUTER_ADVERTISEMENT) {
+               /* Managed mode (ie DHCP only) short-circuits automatic addrconf, so
+                * we don't bother waiting for the device's target state to be reached
+                * when the RA requests managed mode.
+                */
+               if (   (device->state >= device->target_state)
+                   || (device->dhcp_opts == IP6_DHCP_OPT_MANAGED)) {
                        /* device->finish_addrconf_id may currently be a timeout
                         * rather than an idle, so we remove the existing source.
                         */
                        if (device->finish_addrconf_id)
                                g_source_remove (device->finish_addrconf_id);
-                       device->finish_addrconf_id = g_idle_add (finish_addrconf,
-                                                                                                        device);
+
+                       nm_log_dbg (LOGD_IP6, "(%s): reached target state or Managed-mode requested (state '%s') (dhcp opts 0x%X)",
+                                   device->iface, state_to_string (device->state),
+                                   device->dhcp_opts);
+
+                       info = callback_info_new (device, device->dhcp_opts, TRUE);
+                       device->finish_addrconf_id = g_idle_add_full (G_PRIORITY_DEFAULT_IDLE,
+                                                                     finish_addrconf,
+                                                                     info,
+                                                                     (GDestroyNotify) g_free);
                }
-       } else if (config_changed) {
+       } else {
                if (!device->config_changed_id) {
-                       device->config_changed_id = g_idle_add (emit_config_changed,
-                                                                                                       device);
+                       gboolean success = TRUE;
+
+                       /* If for some reason an RA-provided address disappeared, we need
+                        * to make sure we fail the connection as it's no longer valid.
+                        */
+                       if (   (device->state == NM_IP6_DEVICE_GOT_ADDRESS)
+                           && (device->target_state == NM_IP6_DEVICE_GOT_ADDRESS)
+                           && !device->has_nonlinklocal) {
+                               nm_log_dbg (LOGD_IP6, "(%s): RA-provided address no longer found",
+                                           device->iface);
+                               success = FALSE;
+                       }
+
+                       info = callback_info_new (device, device->dhcp_opts, success);
+                       device->config_changed_id = g_idle_add_full (G_PRIORITY_DEFAULT_IDLE,
+                                                                    emit_config_changed,
+                                                                    info,
+                                                                    (GDestroyNotify) g_free);
                }
        }
+
+       nm_log_dbg (LOGD_IP6, "(%s): dhcp_opts checked (state %s)",
+                   device->iface, state_to_string (device->state));
+}
+
+static void
+nm_ip6_device_sync_from_netlink (NMIP6Device *device)
+{
+       nm_log_dbg (LOGD_IP6, "(%s): syncing with netlink (ra_flags 0x%X) (state/target '%s'/'%s')",
+                   device->iface, device->ra_flags,
+                   state_to_string (device->state),
+                   state_to_string (device->target_state));
+
+       check_addresses (device);
+       check_ra_flags (device);
+       check_addrconf_complete (device);
 }
 
 static void
@@ -396,29 +593,71 @@ ref_object (struct nl_object *obj, void *data)
        *out = obj;
 }
 
+static void
+dump_address_change (NMIP6Device *device, struct nlmsghdr *hdr, struct rtnl_addr *rtnladdr)
+{
+       char *event;
+       struct nl_addr *addr;
+       char addr_str[40] = "none";
+
+       event = hdr->nlmsg_type == RTM_NEWADDR ? "new" : "lost";
+       addr = rtnl_addr_get_local (rtnladdr);
+       if (addr)
+               nl_addr2str (addr, addr_str, 40);
+
+       nm_log_dbg (LOGD_IP6, "(%s) %s address: %s", device_get_iface (device), event, addr_str);
+}
+
+static void
+dump_route_change (NMIP6Device *device, struct nlmsghdr *hdr, struct rtnl_route *rtnlroute)
+{
+       char *event;
+       struct nl_addr *dst;
+       char dst_str[40] = "none";
+       struct nl_addr *gateway;
+       char gateway_str[40] = "none";
+
+       event = hdr->nlmsg_type == RTM_NEWROUTE ? "new" : "lost";
+       dst = rtnl_route_get_dst (rtnlroute);
+       gateway = rtnl_route_get_gateway (rtnlroute);
+       if (dst)
+               nl_addr2str (dst, dst_str, 40);
+       if (gateway)
+               nl_addr2str (gateway, gateway_str, 40);
+
+       nm_log_dbg (LOGD_IP6, "(%s) %s route: %s via %s",device_get_iface (device), event, dst_str, gateway_str);
+}
+
 static NMIP6Device *
-process_addr (NMIP6Manager *manager, struct nl_msg *msg)
+process_address_change (NMIP6Manager *manager, struct nl_msg *msg)
 {
        NMIP6ManagerPrivate *priv = NM_IP6_MANAGER_GET_PRIVATE (manager);
        NMIP6Device *device;
+       struct nlmsghdr *hdr;
        struct rtnl_addr *rtnladdr;
        int old_size;
 
+       hdr = nlmsg_hdr (msg);
        rtnladdr = NULL;
        nl_msg_parse (msg, ref_object, &rtnladdr);
-       if (!rtnladdr)
+       if (!rtnladdr) {
+               nm_log_dbg (LOGD_IP6, "error processing netlink new/del address message");
                return NULL;
+       }
 
        device = nm_ip6_manager_get_device (manager, rtnl_addr_get_ifindex (rtnladdr));
 
        old_size = nl_cache_nitems (priv->addr_cache);
-       nl_cache_include (priv->addr_cache, (struct nl_object *)rtnladdr, NULL);
-       rtnl_addr_put (rtnladdr);
+       nl_cache_include (priv->addr_cache, (struct nl_object *)rtnladdr, NULL, NULL);
 
        /* The kernel will re-notify us of automatically-added addresses
         * every time it gets another router advertisement. We only want
         * to notify higher levels if we actually changed something.
         */
+       nm_log_dbg (LOGD_IP6, "(%s): address cache size: %d -> %d:",
+                   device_get_iface (device), old_size, nl_cache_nitems (priv->addr_cache));
+       dump_address_change (device, hdr, rtnladdr);
+       rtnl_addr_put (rtnladdr);
        if (nl_cache_nitems (priv->addr_cache) == old_size)
                return NULL;
 
@@ -426,91 +665,336 @@ process_addr (NMIP6Manager *manager, struct nl_msg *msg)
 }
 
 static NMIP6Device *
-process_route (NMIP6Manager *manager, struct nl_msg *msg)
+process_route_change (NMIP6Manager *manager, struct nl_msg *msg)
 {
        NMIP6ManagerPrivate *priv = NM_IP6_MANAGER_GET_PRIVATE (manager);
        NMIP6Device *device;
+       struct nlmsghdr *hdr;
        struct rtnl_route *rtnlroute;
        int old_size;
 
+       hdr = nlmsg_hdr (msg);
        rtnlroute = NULL;
        nl_msg_parse (msg, ref_object, &rtnlroute);
-       if (!rtnlroute)
+       if (!rtnlroute) {
+               nm_log_dbg (LOGD_IP6, "error processing netlink new/del route message");
                return NULL;
+       }
 
        device = nm_ip6_manager_get_device (manager, rtnl_route_get_oif (rtnlroute));
 
        old_size = nl_cache_nitems (priv->route_cache);
-       nl_cache_include (priv->route_cache, (struct nl_object *)rtnlroute, NULL);
-       rtnl_route_put (rtnlroute);
+       nl_cache_include (priv->route_cache, (struct nl_object *)rtnlroute, NULL, NULL);
 
-       /* As above in process_addr */
+       /* As above in process_address_change */
+       nm_log_dbg (LOGD_IP6, "(%s): route cache size: %d -> %d:",
+                   device_get_iface (device), old_size, nl_cache_nitems (priv->route_cache));
+       dump_route_change (device, hdr, rtnlroute);
+       rtnl_route_put (rtnlroute);
        if (nl_cache_nitems (priv->route_cache) == old_size)
                return NULL;
 
        return device;
 }
 
-static NMIP6Device *
-process_prefix (NMIP6Manager *manager, struct nl_msg *msg)
-{
-       struct prefixmsg *pmsg;
-       NMIP6Device *device;
-
-       /* We don't care about the prefix itself, but if we receive a
-        * router advertisement telling us to use DHCP, we might not
-        * get any RTM_NEWADDRs or RTM_NEWROUTEs, so this is our only
-        * way to notice immediately that an RA was received.
-        */
-
-       pmsg = (struct prefixmsg *) NLMSG_DATA (nlmsg_hdr (msg));
-       device = nm_ip6_manager_get_device (manager, pmsg->prefix_ifindex);
-
-       if (!device || device->addrconf_complete)
-               return NULL;
-
-       return device;
-}
-
 /* RDNSS parsing code based on rdnssd, Copyright 2007 Pierre Ynard,
  * RĂ©mi Denis-Courmont. GPLv2/3
  */
 
 #define ND_OPT_RDNSS 25
+#define ND_OPT_DNSSL 31
+
 struct nd_opt_rdnss {
        uint8_t nd_opt_rdnss_type;
        uint8_t nd_opt_rdnss_len;
        uint16_t nd_opt_rdnss_reserved1;
        uint32_t nd_opt_rdnss_lifetime;
        /* followed by one or more IPv6 addresses */
-};
+} __attribute__ ((packed));
 
-static NMIP6Device *
-process_nduseropt (NMIP6Manager *manager, struct nl_msg *msg)
+struct nd_opt_dnssl {
+       uint8_t nd_opt_dnssl_type;
+       uint8_t nd_opt_dnssl_len;
+       uint16_t nd_opt_dnssl_reserved1;
+       uint32_t nd_opt_dnssl_lifetime;
+       /* followed by one or more suffixes */
+} __attribute__ ((packed));
+
+static gboolean
+process_nduseropt_rdnss (NMIP6Device *device, struct nd_opt_hdr *opt)
 {
-       NMIP6Device *device;
-       struct nduseroptmsg *ndmsg;
-       struct nd_opt_hdr *opt;
-       guint opts_len, i;
-       time_t now = time (NULL);
+       size_t opt_len;
        struct nd_opt_rdnss *rdnss_opt;
+       time_t now = time (NULL);
        struct in6_addr *addr;
-       GArray *servers;
-       NMIP6RDNSS server, *sa, *sb;
+       GArray *new_servers;
+       NMIP6RDNSS server, *cur_server;
+       gboolean changed = FALSE;
+       guint i;
+
+       opt_len = opt->nd_opt_len;
+
+       if (opt_len < 3 || (opt_len & 1) == 0)
+               return FALSE;
+
+       rdnss_opt = (struct nd_opt_rdnss *) opt;
+
+       new_servers = g_array_new (FALSE, FALSE, sizeof (NMIP6RDNSS));
+
+       /* Pad the DNS server expiry 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.
+        */
+       server.expires = ntohl (rdnss_opt->nd_opt_rdnss_lifetime);
+       if (server.expires > 0)
+               if (server.expires < 7200)
+                       server.expires = 7200;
+               server.expires += now;
+
+       for (addr = (struct in6_addr *) (rdnss_opt + 1); opt_len >= 2; addr++, opt_len -= 2) {
+               char buf[INET6_ADDRSTRLEN + 1];
+
+               if (!inet_ntop (AF_INET6, addr, buf, sizeof (buf))) {
+                       nm_log_warn (LOGD_IP6, "(%s): received invalid RA-provided nameserver", device->iface);
+                       continue;
+               }
+
+               /* Update the cached timeout if we already saw this server */
+               for (i = 0; i < device->rdnss_servers->len; i++) {
+                       cur_server = &(g_array_index (device->rdnss_servers, NMIP6RDNSS, i));
+
+                       if (!IN6_ARE_ADDR_EQUAL (addr, &cur_server->addr))
+                               continue;
+
+                       cur_server->expires = server.expires;
+
+                       if (server.expires > 0) {
+                               nm_log_dbg (LOGD_IP6, "(%s): refreshing RA-provided nameserver %s (expires in %ld seconds)",
+                                           device->iface, buf,
+                                           server.expires - now);
+                               break;
+                       }
+
+                       nm_log_dbg (LOGD_IP6, "(%s): removing RA-provided nameserver %s on router request",
+                                   device->iface, buf);
+
+                       g_array_remove_index (device->rdnss_servers, i);
+                       changed = TRUE;
+                       break;
+               }
+
+               if (server.expires == 0)
+                       continue;
+               if (i < device->rdnss_servers->len)
+                       continue;
+
+               nm_log_dbg (LOGD_IP6, "(%s): found RA-provided nameserver %s (expires in %ld seconds)",
+                           device->iface, buf, server.expires - now);
+
+               server.addr = *addr;
+               g_array_append_val (new_servers, server);
+       }
+
+       /* New servers must be added in the order they are listed in the
+        * RA option and before any existing servers.
+        *
+        * Note: This is the place to remove servers if we want to cap the
+        *       number of resolvers. The RFC states that the one to expire
+        *       first of the existing servers should be removed.
+        */
+       if (new_servers->len) {
+               g_array_prepend_vals (device->rdnss_servers,
+                                     new_servers->data, new_servers->len);
+               changed = TRUE;
+       }
+
+       g_array_free (new_servers, TRUE);
+
+       /* Timeouts may have changed even if IPs didn't */
+       set_rdnss_timeout (device);
+
+       return changed;
+}
+
+static const char *
+parse_dnssl_domain (const unsigned char *buffer, size_t maxlen)
+{
+       static char domain[256];
+       size_t label_len;
+
+       domain[0] = '\0';
+
+       while (maxlen > 0) {
+               label_len = *buffer;
+               buffer++;
+               maxlen--;
+
+               if (label_len == 0)
+                       return domain;
+
+               if (label_len > maxlen)
+                       return NULL;
+               if ((sizeof (domain) - strlen (domain)) < (label_len + 2))
+                       return NULL;
+
+               if (domain[0] != '\0')
+                       strcat (domain, ".");
+               strncat (domain, (const char *)buffer, label_len);
+               buffer += label_len;
+               maxlen -= label_len;
+       }
+
+       return NULL;
+}
+
+static gboolean
+process_nduseropt_dnssl (NMIP6Device *device, struct nd_opt_hdr *opt)
+{
+       size_t opt_len;
+       struct nd_opt_dnssl *dnssl_opt;
+       unsigned char *opt_ptr;
+       time_t now = time (NULL);
+       GArray *new_domains;
+       NMIP6DNSSL domain, *cur_domain;
        gboolean changed;
+       guint i;
+
+       opt_len = opt->nd_opt_len;
+
+       if (opt_len < 2)
+               return FALSE;
+
+       dnssl_opt = (struct nd_opt_dnssl *) opt;
+
+       opt_ptr = (unsigned char *)(dnssl_opt + 1);
+       opt_len = (opt_len - 1) * 8; /* prefer bytes for later handling */
+
+       new_domains = g_array_new (FALSE, FALSE, sizeof (NMIP6DNSSL));
+
+       changed = FALSE;
+
+       /* Pad the DNS server expiry 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.
+        */
+       domain.expires = ntohl (dnssl_opt->nd_opt_dnssl_lifetime);
+       if (domain.expires > 0)
+               if (domain.expires < 7200)
+                       domain.expires = 7200;
+               domain.expires += now;
+
+       while (opt_len) {
+               const char *domain_str;
+
+               domain_str = parse_dnssl_domain (opt_ptr, opt_len);
+               if (domain_str == NULL) {
+                       nm_log_dbg (LOGD_IP6, "(%s): invalid DNSSL option, parsing aborted",
+                                   device->iface);
+                       break;
+               }
+
+               /* The DNSSL encoding of domains happen to occupy the same size
+                * as the length of the resulting string, including terminating
+                * null. */
+               opt_ptr += strlen (domain_str) + 1;
+               opt_len -= strlen (domain_str) + 1;
+
+               /* Ignore empty domains. They're probably just padding... */
+               if (domain_str[0] == '\0')
+                       continue;
+
+               /* Update cached domain information if we've seen this domain before */
+               for (i = 0; i < device->dnssl_domains->len; i++) {
+                       cur_domain = &(g_array_index (device->dnssl_domains, NMIP6DNSSL, i));
+
+                       if (strcmp (domain_str, cur_domain->domain) != 0)
+                               continue;
+
+                       cur_domain->expires = domain.expires;
+
+                       if (domain.expires > 0) {
+                               nm_log_dbg (LOGD_IP6, "(%s): refreshing RA-provided domain %s (expires in %ld seconds)",
+                                           device->iface, domain_str,
+                                           domain.expires - now);
+                               break;
+                       }
+
+                       nm_log_dbg (LOGD_IP6, "(%s): removing RA-provided domain %s on router request",
+                                   device->iface, domain_str);
+
+                       g_array_remove_index (device->dnssl_domains, i);
+                       changed = TRUE;
+                       break;
+               }
+
+               if (domain.expires == 0)
+                       continue;
+               if (i < device->dnssl_domains->len)
+                       continue;
+
+               nm_log_dbg (LOGD_IP6, "(%s): found RA-provided domain %s (expires in %ld seconds)",
+                           device->iface, domain_str, domain.expires - now);
+
+               g_assert (strlen (domain_str) < sizeof (domain.domain));
+               strcpy (domain.domain, domain_str);
+               g_array_append_val (new_domains, domain);
+       }
+
+       /* New domains must be added in the order they are listed in the
+        * RA option and before any existing domains.
+        *
+        * Note: This is the place to remove domains if we want to cap the
+        *       number of domains. The RFC states that the one to expire
+        *       first of the existing domains should be removed.
+        */
+       if (new_domains->len) {
+               g_array_prepend_vals (device->dnssl_domains,
+                                     new_domains->data, new_domains->len);
+               changed = TRUE;
+       }
+
+       g_array_free (new_domains, TRUE);
+
+       /* Timeouts may have changed even if domains didn't */
+       set_dnssl_timeout (device);
+
+       return changed;
+}
+
+static NMIP6Device *
+process_nduseropt (NMIP6Manager *manager, struct nl_msg *msg)
+{
+       NMIP6Device *device;
+       struct nduseroptmsg *ndmsg;
+       struct nd_opt_hdr *opt;
+       guint opts_len;
+       gboolean changed = FALSE;
+
+       nm_log_dbg (LOGD_IP6, "processing netlink nduseropt message");
 
        ndmsg = (struct nduseroptmsg *) NLMSG_DATA (nlmsg_hdr (msg));
 
+       if (!nlmsg_valid_hdr (nlmsg_hdr (msg), sizeof (*ndmsg)) ||
+           nlmsg_datalen (nlmsg_hdr (msg)) <
+               (ndmsg->nduseropt_opts_len + sizeof (*ndmsg))) {
+               nm_log_dbg (LOGD_IP6, "ignoring invalid nduseropt message");
+               return NULL;
+       }
+
        if (ndmsg->nduseropt_family != AF_INET6 ||
                ndmsg->nduseropt_icmp_type != ND_ROUTER_ADVERT ||
-               ndmsg->nduseropt_icmp_code != 0)
+               ndmsg->nduseropt_icmp_code != 0) {
+               nm_log_dbg (LOGD_IP6, "ignoring non-Router Advertisement message");
                return NULL;
+       }
 
        device = nm_ip6_manager_get_device (manager, ndmsg->nduseropt_ifindex);
-       if (!device)
+       if (!device) {
+               nm_log_dbg (LOGD_IP6, "ignoring message for unknown device");
                return NULL;
-
-       servers = g_array_new (FALSE, FALSE, sizeof (NMIP6RDNSS));
+       }
 
        opt = (struct nd_opt_hdr *) (ndmsg + 1);
        opts_len = ndmsg->nduseropt_opts_len;
@@ -521,249 +1005,264 @@ process_nduseropt (NMIP6Manager *manager, struct nl_msg *msg)
                if (nd_opt_len == 0 || opts_len < (nd_opt_len << 3))
                        break;
 
-               if (opt->nd_opt_type != ND_OPT_RDNSS)
-                       goto next;
-
-               if (nd_opt_len < 3 || (nd_opt_len & 1) == 0)
-                       goto next;
-
-               rdnss_opt = (struct nd_opt_rdnss *) opt;
-
-               server.expires = now + ntohl (rdnss_opt->nd_opt_rdnss_lifetime);
-               for (addr = (struct in6_addr *) (rdnss_opt + 1); nd_opt_len >= 2; addr++, nd_opt_len -= 2) {
-                       server.addr = *addr;
-                       g_array_append_val (servers, server);
+               switch (opt->nd_opt_type) {
+               case ND_OPT_RDNSS:
+                       changed = process_nduseropt_rdnss (device, opt);
+                       break;
+               case ND_OPT_DNSSL:
+                       changed = process_nduseropt_dnssl (device, opt);
+                       break;
                }
 
-       next:
                opts_len -= opt->nd_opt_len << 3;
                opt = (struct nd_opt_hdr *) ((uint8_t *) opt + (opt->nd_opt_len << 3));
        }
 
-       /* See if anything (other than expiration time) changed */
-       if (servers->len != device->rdnss_servers->len)
-               changed = TRUE;
-       else {
-               for (i = 0; i < servers->len; i++) {
-                       sa = &(g_array_index (servers, NMIP6RDNSS, i));
-                       sb = &(g_array_index (device->rdnss_servers, NMIP6RDNSS, i));
-                       if (memcmp (&sa->addr, &sb->addr, sizeof (struct in6_addr)) != 0) {
-                               changed = TRUE;
-                               break;
-                       }
-               }
-               changed = FALSE;
-       }
-
-       if (changed) {
-               g_array_free (device->rdnss_servers, TRUE);
-               device->rdnss_servers = servers;
-       } else
-               g_array_free (servers, TRUE);
-
-       /* Timeouts may have changed even if IPs didn't */
-       set_rdnss_timeout (device);
-
        if (changed)
                return device;
        else
                return NULL;
 }
 
-static void
-netlink_notification (NMNetlinkListener *listener, struct nl_msg *msg, gpointer user_data)
-{
-       NMIP6Manager *manager = (NMIP6Manager *) user_data;
-       NMIP6Device *device;
-       struct nlmsghdr *hdr;
-       gboolean config_changed = FALSE;
+static struct nla_policy link_policy[IFLA_MAX + 1] = {
+       [IFLA_PROTINFO] = { .type = NLA_NESTED },
+};
 
-       hdr = nlmsg_hdr (msg);
-       switch (hdr->nlmsg_type) {
-       case RTM_NEWADDR:
-       case RTM_DELADDR:
-               device = process_addr (manager, msg);
-               config_changed = TRUE;
-               break;
+static struct nla_policy link_prot_policy[IFLA_INET6_MAX + 1] = {
+       [IFLA_INET6_FLAGS]      = { .type = NLA_U32 },
+};
 
-       case RTM_NEWROUTE:
-       case RTM_DELROUTE:
-               device = process_route (manager, msg);
-               config_changed = TRUE;
-               break;
+static char *
+ra_flags_to_string (guint32 ra_flags)
+{
+       GString *s = g_string_sized_new (20);
 
-       case RTM_NEWPREFIX:
-               device = process_prefix (manager, msg);
-               break;
+       g_string_append (s, " (");
+       if (ra_flags & IF_RS_SENT)
+               g_string_append_c (s, 'S');
 
-       case RTM_NEWNDUSEROPT:
-               device = process_nduseropt (manager, msg);
-               config_changed = TRUE;
-               break;
+       if (ra_flags & IF_RA_RCVD)
+               g_string_append_c (s, 'R');
 
-       default:
-               return;
-       }
+       if (ra_flags & IF_RA_OTHERCONF)
+               g_string_append_c (s, 'O');
+
+       if (ra_flags & IF_RA_MANAGED)
+               g_string_append_c (s, 'M');
 
-       if (device)
-               nm_ip6_device_sync_from_netlink (device, config_changed);
+       g_string_append_c (s, ')');
+       return g_string_free (s, FALSE);
 }
 
 static NMIP6Device *
-nm_ip6_device_new (NMIP6Manager *manager, const char *iface)
+process_newlink (NMIP6Manager *manager, struct nl_msg *msg)
 {
-       NMIP6ManagerPrivate *priv = NM_IP6_MANAGER_GET_PRIVATE (manager);
+       struct nlmsghdr *hdr = nlmsg_hdr (msg);
+       struct ifinfomsg *ifi;
        NMIP6Device *device;
-       GError *error = NULL;
-       char *contents = NULL;
+       struct nlattr *tb[IFLA_MAX + 1];
+       struct nlattr *pi[IFLA_INET6_MAX + 1];
+       int err;
+       char *flags_str = NULL;
+
+       /* FIXME: we have to do this manually for now since libnl doesn't yet
+        * support the IFLA_PROTINFO attribute of NEWLINK messages.  When it does,
+        * we can get rid of this function and just grab IFLA_PROTINFO from
+        * nm_ip6_device_sync_from_netlink(), then get the IFLA_INET6_FLAGS out of
+        * the PROTINFO.
+        */
+       err = nlmsg_parse (hdr, sizeof (*ifi), tb, IFLA_MAX, link_policy);
+       if (err < 0) {
+               nm_log_dbg (LOGD_IP6, "ignoring invalid newlink netlink message "
+                                     "while parsing PROTINFO attribute");
+               return NULL;
+       }
 
-       device = g_slice_new0 (NMIP6Device);
-       if (!device) {
-               nm_warning ("%s: Out of memory creating IP6 addrconf object.", iface);
+       ifi = nlmsg_data (hdr);
+       if (ifi->ifi_family != AF_INET6) {
+               nm_log_dbg (LOGD_IP6, "ignoring netlink message family %d", ifi->ifi_family);
                return NULL;
        }
 
-       device->iface = g_strdup (iface);
-       if (!device->iface) {
-               nm_warning ("%s: Out of memory creating IP6 addrconf object "
-                           "property 'iface'.",
-                           iface);
-               goto error;
+       device = nm_ip6_manager_get_device (manager, ifi->ifi_index);
+       if (!device || device->addrconf_complete) {
+               nm_log_dbg (LOGD_IP6, "(%s): ignoring unknown or completed device",
+                           device ? device->iface : "(none)");
+               return NULL;
        }
-       device->index = nm_netlink_iface_to_index (iface);
 
-       device->accept_ra_path = g_strdup_printf ("/proc/sys/net/ipv6/conf/%s/accept_ra", iface);
-       if (!device->accept_ra_path) {
-               nm_warning ("%s: Out of memory creating IP6 addrconf object "
-                           "property 'accept_ra_path'.",
-                           iface);
-               goto error;
+       if (!tb[IFLA_PROTINFO]) {
+               nm_log_dbg (LOGD_IP6, "(%s): message had no PROTINFO attribute", device->iface);
+               return NULL;
        }
 
-       device->manager = manager;
+       err = nla_parse_nested (pi, IFLA_INET6_MAX, tb[IFLA_PROTINFO], link_prot_policy);
+       if (err < 0) {
+               nm_log_dbg (LOGD_IP6, "(%s): error parsing PROTINFO flags", device->iface);
+               return NULL;
+       }
+       if (!pi[IFLA_INET6_FLAGS]) {
+               nm_log_dbg (LOGD_IP6, "(%s): message had no PROTINFO flags", device->iface);
+               return NULL;
+       }
 
-       device->rdnss_servers = g_array_new (FALSE, FALSE, sizeof (NMIP6RDNSS));
+       device->ra_flags = nla_get_u32 (pi[IFLA_INET6_FLAGS]);
 
-       g_hash_table_replace (priv->devices_by_iface, device->iface, device);
-       g_hash_table_replace (priv->devices_by_index, GINT_TO_POINTER (device->index), device);
+       if (nm_logging_level_enabled (LOGL_DEBUG))
+               flags_str = ra_flags_to_string (device->ra_flags);
+       nm_log_dbg (LOGD_IP6, "(%s): got IPv6 flags 0x%X%s",
+                   device->iface, device->ra_flags, flags_str ? flags_str : "");
+       g_free (flags_str);
 
-       /* Grab the original value of "accept_ra" so we can restore it when the
-        * device is taken down.
-        */
-       if (!g_file_get_contents (device->accept_ra_path, &contents, NULL, &error)) {
-               nm_warning ("%s: error reading %s: (%d) %s",
-                           iface, device->accept_ra_path,
-                           error ? error->code : -1,
-                           error && error->message ? error->message : "(unknown)");
-               g_clear_error (&error);
-       } else {
-               long int tmp;
+       return device;
+}
 
-               errno = 0;
-               tmp = strtol (contents, NULL, 10);
-               if ((errno == 0) && (tmp == 0 || tmp == 1)) {
-                       device->accept_ra_save = (guint32) tmp;
-                       device->accept_ra_save_valid = TRUE;
-               }
-               g_free (contents);
-       }
+static void
+netlink_notification (NMNetlinkMonitor *monitor, struct nl_msg *msg, gpointer user_data)
+{
+       NMIP6Manager *manager = (NMIP6Manager *) user_data;
+       NMIP6Device *device;
+       struct nlmsghdr *hdr;
 
-       return device;
+       hdr = nlmsg_hdr (msg);
+       nm_log_dbg (LOGD_HW, "netlink event type %d", hdr->nlmsg_type);
+       switch (hdr->nlmsg_type) {
+       case RTM_NEWADDR:
+       case RTM_DELADDR:
+               device = process_address_change (manager, msg);
+               break;
+       case RTM_NEWROUTE:
+       case RTM_DELROUTE:
+               device = process_route_change (manager, msg);
+               break;
+       case RTM_NEWNDUSEROPT:
+               device = process_nduseropt (manager, msg);
+               break;
+       case RTM_NEWLINK:
+               device = process_newlink (manager, msg);
+               break;
+       default:
+               return;
+       }
 
-error:
-       nm_ip6_device_destroy (device);
-       return NULL;
+       if (device) {
+               nm_ip6_device_sync_from_netlink (device);
+       }
 }
 
-void
+gboolean
 nm_ip6_manager_prepare_interface (NMIP6Manager *manager,
-                                                                 const char *iface,
-                                                                 NMSettingIP6Config *s_ip6)
+                                  int ifindex,
+                                  NMSettingIP6Config *s_ip6,
+                                  const char *accept_ra_path)
 {
        NMIP6ManagerPrivate *priv;
        NMIP6Device *device;
        const char *method = NULL;
 
-       g_return_if_fail (NM_IS_IP6_MANAGER (manager));
-       g_return_if_fail (iface != NULL);
+       g_return_val_if_fail (NM_IS_IP6_MANAGER (manager), FALSE);
+       g_return_val_if_fail (ifindex > 0, FALSE);
 
        priv = NM_IP6_MANAGER_GET_PRIVATE (manager);
 
-       device = nm_ip6_device_new (manager, iface);
+       device = nm_ip6_device_new (manager, ifindex);
+       g_return_val_if_fail (device != NULL, FALSE);
+       g_return_val_if_fail (   strchr (device->iface, '/') == NULL
+                             && strcmp (device->iface, "all") != 0
+                             && strcmp (device->iface, "default") != 0,
+                             FALSE);
 
        if (s_ip6)
                method = nm_setting_ip6_config_get_method (s_ip6);
        if (!method)
                method = NM_SETTING_IP6_CONFIG_METHOD_AUTO;
 
-       if (   !strcmp (method, NM_SETTING_IP6_CONFIG_METHOD_MANUAL)
-               || !strcmp (method, NM_SETTING_IP6_CONFIG_METHOD_LINK_LOCAL))
+       /* Establish target state and turn router advertisement acceptance on or off */
+       if (!strcmp (method, NM_SETTING_IP6_CONFIG_METHOD_LINK_LOCAL)) {
                device->target_state = NM_IP6_DEVICE_GOT_LINK_LOCAL;
-       else
+               nm_utils_do_sysctl (accept_ra_path, "0\n");
+       } else {
                device->target_state = NM_IP6_DEVICE_GOT_ADDRESS;
+               nm_utils_do_sysctl (accept_ra_path, "2\n");
+       }
 
-       g_return_if_fail (strchr (iface, '/') == NULL &&
-                                         strcmp (iface, "all") != 0 &&
-                                         strcmp (iface, "default") != 0);
+       return TRUE;
+}
 
-       /* Turn router advertisement acceptance on or off... */
-       nm_utils_do_sysctl (device->accept_ra_path,
-                           device->target_state >= NM_IP6_DEVICE_GOT_ADDRESS ? "1\n" : "0\n");
+static gboolean
+poll_ip6_flags (gpointer user_data)
+{
+       nm_netlink_monitor_request_ip6_info (NM_NETLINK_MONITOR (user_data), NULL);
+       return TRUE;
 }
 
 void
-nm_ip6_manager_begin_addrconf (NMIP6Manager *manager,
-                                                          const char *iface)
+nm_ip6_manager_begin_addrconf (NMIP6Manager *manager, int ifindex)
 {
        NMIP6ManagerPrivate *priv;
        NMIP6Device *device;
+       CallbackInfo *info;
 
        g_return_if_fail (NM_IS_IP6_MANAGER (manager));
-       g_return_if_fail (iface != NULL);
+       g_return_if_fail (ifindex > 0);
 
        priv = NM_IP6_MANAGER_GET_PRIVATE (manager);
 
-       device = (NMIP6Device *) g_hash_table_lookup (priv->devices_by_iface, iface);
+       device = (NMIP6Device *) g_hash_table_lookup (priv->devices, GINT_TO_POINTER (ifindex));
        g_return_if_fail (device != NULL);
 
-       nm_info ("Activation (%s) Beginning IP6 addrconf.", iface);
+       nm_log_info (LOGD_IP6, "Activation (%s) Beginning IP6 addrconf.", device->iface);
 
        device->addrconf_complete = FALSE;
+       device->ra_flags = 0;
 
        /* Set up a timeout on the transaction to kill it after the timeout */
-       device->finish_addrconf_id = g_timeout_add_seconds (NM_IP6_TIMEOUT,
-                                                                                                               finish_addrconf,
-                                                                                                               device);
+       info = callback_info_new (device, 0, FALSE);
+       device->finish_addrconf_id = g_timeout_add_seconds_full (G_PRIORITY_DEFAULT,
+                                                                NM_IP6_TIMEOUT,
+                                                                finish_addrconf,
+                                                                info,
+                                                                (GDestroyNotify) g_free);
+
+       /* Bounce IPv6 on the interface to ensure the kernel will start looking for
+        * new RAs; there doesn't seem to be a better way to do this right now.
+        */
+       if (device->target_state >= NM_IP6_DEVICE_GOT_ADDRESS) {
+               nm_utils_do_sysctl (device->disable_ip6_path, "1\n");
+               g_usleep (200);
+               nm_utils_do_sysctl (device->disable_ip6_path, "0\n");
+       }
+
+       device->ip6flags_poll_id = g_timeout_add_seconds (1, poll_ip6_flags, priv->monitor);
+
+       /* Kick off the initial IPv6 flags request */
+       nm_netlink_monitor_request_ip6_info (priv->monitor, NULL);
 
        /* Sync flags, etc, from netlink; this will also notice if the
         * device is already fully configured and schedule the
         * ADDRCONF_COMPLETE signal in that case.
         */
-       nm_ip6_device_sync_from_netlink (device, FALSE);
+       nm_ip6_device_sync_from_netlink (device);
 }
 
 void
-nm_ip6_manager_cancel_addrconf (NMIP6Manager *manager,
-                                                               const char *iface)
+nm_ip6_manager_cancel_addrconf (NMIP6Manager *manager, int ifindex)
 {
-       NMIP6ManagerPrivate *priv;
-       NMIP6Device *device;
-
        g_return_if_fail (NM_IS_IP6_MANAGER (manager));
-       g_return_if_fail (iface != NULL);
-
-       priv = NM_IP6_MANAGER_GET_PRIVATE (manager);
+       g_return_if_fail (ifindex > 0);
 
-       device = g_hash_table_lookup (priv->devices_by_iface, iface);
-       if (device) {
-               g_hash_table_remove (priv->devices_by_index, GINT_TO_POINTER (device->index));
-               g_hash_table_remove (priv->devices_by_iface, iface);
-       }
+       g_hash_table_remove (NM_IP6_MANAGER_GET_PRIVATE (manager)->devices,
+                            GINT_TO_POINTER (ifindex));
 }
 
+#define FIRST_ROUTE(m) ((struct rtnl_route *) nl_cache_get_first (m))
+#define NEXT_ROUTE(m) ((struct rtnl_route *) nl_cache_get_next ((struct nl_object *) m))
+
+#define FIRST_ADDR(m) ((struct rtnl_addr *) nl_cache_get_first (m))
+#define NEXT_ADDR(m) ((struct rtnl_addr *) nl_cache_get_next ((struct nl_object *) m))
+
 NMIP6Config *
-nm_ip6_manager_get_ip6_config (NMIP6Manager *manager,
-                                                          const char *iface)
+nm_ip6_manager_get_ip6_config (NMIP6Manager *manager, int ifindex)
 {
        NMIP6ManagerPrivate *priv;
        NMIP6Device *device;
@@ -780,46 +1279,37 @@ nm_ip6_manager_get_ip6_config (NMIP6Manager *manager,
        int i;
 
        g_return_val_if_fail (NM_IS_IP6_MANAGER (manager), NULL);
-       g_return_val_if_fail (iface != NULL, NULL);
+       g_return_val_if_fail (ifindex > 0, NULL);
 
        priv = NM_IP6_MANAGER_GET_PRIVATE (manager);
 
-       device = (NMIP6Device *) g_hash_table_lookup (priv->devices_by_iface, iface);
+       device = (NMIP6Device *) g_hash_table_lookup (priv->devices,
+                                                     GINT_TO_POINTER (ifindex));
        if (!device) {
-               nm_warning ("Device '%s' addrconf not started.", iface);
+               nm_log_warn (LOGD_IP6, "(%d): addrconf not started.", ifindex);
                return NULL;
        }
 
        config = nm_ip6_config_new ();
        if (!config) {
-               nm_warning ("%s: Out of memory creating IP6 config object.",
-                           iface);
+               nm_log_err (LOGD_IP6, "(%s): out of memory creating IP6 config object.",
+                           device->iface);
                return NULL;
        }
 
-       /* Add addresses */
-       for (rtnladdr = (struct rtnl_addr *)nl_cache_get_first (priv->addr_cache);
-                rtnladdr;
-                rtnladdr = (struct rtnl_addr *)nl_cache_get_next ((struct nl_object *)rtnladdr)) {
-               if (rtnl_addr_get_ifindex (rtnladdr) != device->index)
-                       continue;
-
-               nladdr = rtnl_addr_get_local (rtnladdr);
-               if (!nladdr || nl_addr_get_family (nladdr) != AF_INET6)
-                       continue;
-
-               addr = nl_addr_get_binary_addr (nladdr);
-               ip6addr = nm_ip6_address_new ();
-               nm_ip6_address_set_prefix (ip6addr, rtnl_addr_get_prefixlen (rtnladdr));
-               nm_ip6_address_set_address (ip6addr, addr);
-               nm_ip6_config_take_address (config, ip6addr);
-       }
+       /* Make sure we refill the route and address caches, otherwise we won't get
+        * up-to-date information here since the netlink route/addr change messages
+        * may be lagging a bit.
+        */
+       nl_cache_refill (priv->nlh, priv->route_cache);
+       nl_cache_refill (priv->nlh, priv->addr_cache);
 
        /* Add routes */
-       for (rtnlroute = (struct rtnl_route *)nl_cache_get_first (priv->route_cache);
-                rtnlroute;
-                rtnlroute = (struct rtnl_route *)nl_cache_get_next ((struct nl_object *)rtnlroute)) {
-               if (rtnl_route_get_oif (rtnlroute) != device->index)
+       for (rtnlroute = FIRST_ROUTE (priv->route_cache); rtnlroute; rtnlroute = NEXT_ROUTE (rtnlroute)) {
+               /* Make sure it's an IPv6 route for this device */
+               if (rtnl_route_get_oif (rtnlroute) != device->ifindex)
+                       continue;
+               if (rtnl_route_get_family (rtnlroute) != AF_INET6)
                        continue;
 
                nldest = rtnl_route_get_dst (rtnlroute);
@@ -832,16 +1322,50 @@ nm_ip6_manager_get_ip6_config (NMIP6Manager *manager,
                        continue;
                gateway = nl_addr_get_binary_addr (nlgateway);
 
+               if (rtnl_route_get_dst_len (rtnlroute) == 0) {
+                       /* Default gateway route; don't add to normal routes but to each address */
+                       if (!nm_ip6_config_get_gateway (config)) {
+                               nm_ip6_config_set_gateway (config, gateway);
+                       }
+                       continue;
+               }
+
+               /* Also ignore link-local routes where the destination and gateway are
+                * the same, which apparently get added by the kernel but return -EINVAL
+                * when we try to add them via netlink.
+                */
+               if (gateway && IN6_ARE_ADDR_EQUAL (dest, gateway))
+                       continue;
+
                ip6route = nm_ip6_route_new ();
                nm_ip6_route_set_dest (ip6route, dest);
                nm_ip6_route_set_prefix (ip6route, rtnl_route_get_dst_len (rtnlroute));
                nm_ip6_route_set_next_hop (ip6route, gateway);
-               metric = rtnl_route_get_metric (rtnlroute, 1);
+               rtnl_route_get_metric(rtnlroute, 1, &metric);
                if (metric != UINT_MAX)
                        nm_ip6_route_set_metric (ip6route, metric);
                nm_ip6_config_take_route (config, ip6route);
        }
 
+       /* Add addresses */
+       for (rtnladdr = FIRST_ADDR (priv->addr_cache); rtnladdr; rtnladdr = NEXT_ADDR (rtnladdr)) {
+               if (rtnl_addr_get_ifindex (rtnladdr) != device->ifindex)
+                       continue;
+
+               nladdr = rtnl_addr_get_local (rtnladdr);
+               if (!nladdr || nl_addr_get_family (nladdr) != AF_INET6)
+                       continue;
+
+               addr = nl_addr_get_binary_addr (nladdr);
+               ip6addr = nm_ip6_address_new ();
+               nm_ip6_address_set_prefix (ip6addr, rtnl_addr_get_prefixlen (rtnladdr));
+               nm_ip6_address_set_address (ip6addr, addr);
+               nm_ip6_config_take_address (config, ip6addr);
+               gateway = nm_ip6_config_get_gateway (config);
+               if (gateway)
+                       nm_ip6_address_set_gateway (ip6addr, gateway);
+       }
+
        /* Add DNS servers */
        if (device->rdnss_servers) {
                NMIP6RDNSS *rdnss = (NMIP6RDNSS *)(device->rdnss_servers->data);
@@ -850,5 +1374,121 @@ nm_ip6_manager_get_ip6_config (NMIP6Manager *manager,
                        nm_ip6_config_add_nameserver (config, &rdnss[i].addr);
        }
 
+       /* Add DNS domains */
+       if (device->dnssl_domains) {
+               NMIP6DNSSL *dnssl = (NMIP6DNSSL *)(device->dnssl_domains->data);
+
+               for (i = 0; i < device->dnssl_domains->len; i++)
+                       nm_ip6_config_add_domain (config, dnssl[i].domain);
+       }
+
        return config;
 }
+
+/******************************************************************/
+
+static NMIP6Manager *
+nm_ip6_manager_new (void)
+{
+       NMIP6Manager *manager;
+       NMIP6ManagerPrivate *priv;
+
+       manager = g_object_new (NM_TYPE_IP6_MANAGER, NULL);
+       priv = NM_IP6_MANAGER_GET_PRIVATE (manager);
+
+       if (!priv->devices) {
+               nm_log_err (LOGD_IP6, "not enough memory to initialize IP6 manager tables");
+               g_object_unref (manager);
+               manager = NULL;
+       }
+
+       return manager;
+}
+
+static NMIP6Manager *singleton = NULL;
+
+NMIP6Manager *
+nm_ip6_manager_get (void)
+{
+       if (!singleton) {
+               singleton = nm_ip6_manager_new ();
+               g_assert (singleton);
+       } else
+               g_object_ref (singleton);
+
+       return singleton;
+}
+
+static void
+nm_ip6_manager_init (NMIP6Manager *manager)
+{
+       NMIP6ManagerPrivate *priv = NM_IP6_MANAGER_GET_PRIVATE (manager);
+
+       priv->devices = g_hash_table_new_full (g_direct_hash, g_direct_equal,
+                                              NULL,
+                                              (GDestroyNotify) nm_ip6_device_destroy);
+
+       priv->monitor = nm_netlink_monitor_get ();
+       nm_netlink_monitor_subscribe (priv->monitor, RTNLGRP_IPV6_IFADDR, NULL);
+       nm_netlink_monitor_subscribe (priv->monitor, RTNLGRP_IPV6_PREFIX, NULL);
+       nm_netlink_monitor_subscribe (priv->monitor, RTNLGRP_IPV6_ROUTE, NULL);
+       nm_netlink_monitor_subscribe (priv->monitor, RTNLGRP_ND_USEROPT, NULL);
+       nm_netlink_monitor_subscribe (priv->monitor, RTNLGRP_LINK, NULL);
+
+       priv->netlink_id = g_signal_connect (priv->monitor, "notification",
+                                            G_CALLBACK (netlink_notification), manager);
+
+       priv->nlh = nm_netlink_get_default_handle ();
+       rtnl_addr_alloc_cache (priv->nlh, &priv->addr_cache);
+       g_warn_if_fail (priv->addr_cache != NULL);
+       rtnl_route_alloc_cache (priv->nlh, NETLINK_ROUTE, NL_AUTO_PROVIDE, &priv->route_cache);
+       g_warn_if_fail (priv->route_cache != NULL);
+}
+
+static void
+finalize (GObject *object)
+{
+       NMIP6ManagerPrivate *priv = NM_IP6_MANAGER_GET_PRIVATE (object);
+
+       g_signal_handler_disconnect (priv->monitor, priv->netlink_id);
+
+       g_hash_table_destroy (priv->devices);
+       g_object_unref (priv->monitor);
+       nl_cache_free (priv->addr_cache);
+       nl_cache_free (priv->route_cache);
+
+       singleton = NULL;
+
+       G_OBJECT_CLASS (nm_ip6_manager_parent_class)->finalize (object);
+}
+
+static void
+nm_ip6_manager_class_init (NMIP6ManagerClass *manager_class)
+{
+       GObjectClass *object_class = G_OBJECT_CLASS (manager_class);
+
+       g_type_class_add_private (manager_class, sizeof (NMIP6ManagerPrivate));
+
+       /* virtual methods */
+       object_class->finalize = finalize;
+
+       /* signals */
+       signals[ADDRCONF_COMPLETE] =
+               g_signal_new ("addrconf-complete",
+                                         G_OBJECT_CLASS_TYPE (object_class),
+                                         G_SIGNAL_RUN_FIRST,
+                                         G_STRUCT_OFFSET (NMIP6ManagerClass, addrconf_complete),
+                                         NULL, NULL,
+                                         _nm_marshal_VOID__INT_UINT_BOOLEAN,
+                                         G_TYPE_NONE, 3, G_TYPE_INT, G_TYPE_UINT, G_TYPE_BOOLEAN);
+
+       signals[CONFIG_CHANGED] =
+               g_signal_new ("config-changed",
+                                         G_OBJECT_CLASS_TYPE (object_class),
+                                         G_SIGNAL_RUN_FIRST,
+                                         G_STRUCT_OFFSET (NMIP6ManagerClass, config_changed),
+                                         NULL, NULL,
+                                         _nm_marshal_VOID__INT_UINT_BOOLEAN,
+                                         G_TYPE_NONE, 3, G_TYPE_INT, G_TYPE_UINT, G_TYPE_BOOLEAN);
+}
+