summary refs log tree commit diff
path: root/src/core/nm-l3-ipv6ll.c
diff options
context:
space:
mode:
Diffstat (limited to 'src/core/nm-l3-ipv6ll.c')
-rw-r--r--src/core/nm-l3-ipv6ll.c717
1 files changed, 717 insertions, 0 deletions
diff --git a/src/core/nm-l3-ipv6ll.c b/src/core/nm-l3-ipv6ll.c
new file mode 100644
index 00000000..14387321
--- /dev/null
+++ b/src/core/nm-l3-ipv6ll.c
@@ -0,0 +1,717 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include "src/core/nm-default-daemon.h"
+
+#include "nm-l3-ipv6ll.h"
+
+#include <linux/if_addr.h>
+
+#include "nm-core-utils.h"
+
+/*****************************************************************************/
+
+/* FIXME(l3cfg): ensure that NML3IPv6LL generates the same stable privacy addresses
+ *  as previous implementation. */
+
+/*****************************************************************************/
+
+NM_UTILS_LOOKUP_STR_DEFINE(nm_l3_ipv6ll_state_to_string,
+                           NML3IPv6LLState,
+                           NM_UTILS_LOOKUP_DEFAULT_NM_ASSERT("???"),
+                           NM_UTILS_LOOKUP_STR_ITEM(NM_L3_IPV6LL_STATE_NONE, "none"),
+                           NM_UTILS_LOOKUP_STR_ITEM(NM_L3_IPV6LL_STATE_DEFUNCT, "defunct"),
+                           NM_UTILS_LOOKUP_STR_ITEM(NM_L3_IPV6LL_STATE_STARTING, "starting"),
+                           NM_UTILS_LOOKUP_STR_ITEM(NM_L3_IPV6LL_STATE_DAD_IN_PROGRESS,
+                                                    "dad-in-progress"),
+                           NM_UTILS_LOOKUP_STR_ITEM(NM_L3_IPV6LL_STATE_READY, "ready"),
+                           NM_UTILS_LOOKUP_STR_ITEM(NM_L3_IPV6LL_STATE_DAD_FAILED, "dad-failed"), );
+
+/*****************************************************************************/
+
+struct _NML3IPv6LL {
+    NML3Cfg *                l3cfg;
+    NML3CfgCommitTypeHandle *l3cfg_commit_handle;
+    NML3IPv6LLNotifyFcn      notify_fcn;
+    gpointer                 user_data;
+    GSource *                starting_on_idle_source;
+    GSource *                wait_for_addr_source;
+    GSource *                emit_changed_idle_source;
+    gulong                   l3cfg_signal_notify_id;
+    NML3IPv6LLState          state;
+
+    /* if we have cur_lladdr set, then this might cache the last
+     * matching NMPObject from the platform cache. This only serves
+     * for optimizing the lookup to the platform cache. */
+    const NMPlatformIP6Address *cur_lladdr_obj;
+
+    struct in6_addr cur_lladdr;
+
+    /* if we have cur_lladdr and _state_has_lladdr() indicates that
+     * the LL address is suitable, this is a NML3ConfigData instance
+     * with the configuration. */
+    const NML3ConfigData *l3cd;
+
+    /* "assume" means that we first look whether there is any suitable
+     * IPv6 address on the device, and in that case, try to use that
+     * instead of generating a new one. Otherwise, we always try to
+     * generate a new LL address. */
+    bool assume : 1;
+
+    struct {
+        NMUtilsStableType stable_type;
+        guint32           dad_counter;
+        struct {
+            const char *ifname;
+            const char *network_id;
+        } stable_privacy;
+        struct {
+            NMUtilsIPv6IfaceId iid;
+        } token;
+    } addrgen;
+};
+
+/*****************************************************************************/
+
+#define _NMLOG_DOMAIN      LOGD_IP6
+#define _NMLOG_PREFIX_NAME "ipv6ll"
+#define _NMLOG(level, ...)                                                             \
+    G_STMT_START                                                                       \
+    {                                                                                  \
+        nm_log((level),                                                                \
+               (_NMLOG_DOMAIN),                                                        \
+               NULL,                                                                   \
+               NULL,                                                                   \
+               _NMLOG_PREFIX_NAME "[" NM_HASH_OBFUSCATE_PTR_FMT                        \
+                                  ",ifindex=%d]: " _NM_UTILS_MACRO_FIRST(__VA_ARGS__), \
+               NM_HASH_OBFUSCATE_PTR(self),                                            \
+               nm_l3cfg_get_ifindex((self)->l3cfg) _NM_UTILS_MACRO_REST(__VA_ARGS__)); \
+    }                                                                                  \
+    G_STMT_END
+
+/*****************************************************************************/
+
+#define L3CD_TAG(self) (&(self)->notify_fcn)
+
+/*****************************************************************************/
+
+#define _ASSERT(self)                      \
+    G_STMT_START                           \
+    {                                      \
+        NML3IPv6LL *const _self = (self);  \
+                                           \
+        nm_assert(NM_IS_L3_IPV6LL(_self)); \
+    }                                      \
+    G_STMT_END
+
+/*****************************************************************************/
+
+static void _check(NML3IPv6LL *self);
+
+/*****************************************************************************/
+
+NML3Cfg *
+nm_l3_ipv6ll_get_l3cfg(NML3IPv6LL *self)
+{
+    nm_assert(NM_IS_L3_IPV6LL(self));
+
+    return self->l3cfg;
+}
+
+int
+nm_l3_ipv6ll_get_ifindex(NML3IPv6LL *self)
+{
+    nm_assert(NM_IS_L3_IPV6LL(self));
+
+    return nm_l3cfg_get_ifindex(self->l3cfg);
+}
+
+NMPlatform *
+nm_l3_ipv6ll_get_platform(NML3IPv6LL *self)
+{
+    nm_assert(NM_IS_L3_IPV6LL(self));
+
+    return nm_l3cfg_get_platform(self->l3cfg);
+}
+
+/*****************************************************************************/
+
+static gboolean
+_state_has_lladdr(NML3IPv6LLState state)
+{
+    return NM_IN_SET(state, NM_L3_IPV6LL_STATE_DAD_IN_PROGRESS, NM_L3_IPV6LL_STATE_READY);
+}
+
+NML3IPv6LLState
+nm_l3_ipv6ll_get_state(NML3IPv6LL *self, const struct in6_addr **out_lladdr)
+{
+    nm_assert(NM_IS_L3_IPV6LL(self));
+
+    NM_SET_OUT(out_lladdr, _state_has_lladdr(self->state) ? &self->cur_lladdr : NULL);
+    return self->state;
+}
+
+static const NML3ConfigData *
+_l3cd_config_create(int ifindex, const struct in6_addr *lladdr, NMDedupMultiIndex *multi_idx)
+{
+    NML3ConfigData *l3cd;
+
+    nm_assert(ifindex > 0);
+    nm_assert(lladdr);
+    nm_assert(IN6_IS_ADDR_LINKLOCAL(lladdr));
+
+    l3cd = nm_l3_config_data_new(multi_idx, ifindex, NM_IP_CONFIG_SOURCE_IP6LL);
+
+    nm_l3_config_data_add_address_6(
+        l3cd,
+        NM_PLATFORM_IP6_ADDRESS_INIT(.address     = *lladdr,
+                                     .plen        = 64,
+                                     .addr_source = NM_IP_CONFIG_SOURCE_IP6LL));
+
+    nm_l3_config_data_add_route_6(
+        l3cd,
+        NM_PLATFORM_IP6_ROUTE_INIT(.network.s6_addr16[0] = htons(0xfe80u),
+                                   .plen                 = 64,
+                                   .metric_any           = TRUE,
+                                   .table_any            = TRUE,
+                                   .rt_source            = NM_IP_CONFIG_SOURCE_IP6LL));
+
+    return nm_l3_config_data_seal(l3cd);
+}
+
+const NML3ConfigData *
+nm_l3_ipv6ll_get_l3cd(NML3IPv6LL *self)
+{
+    nm_assert(NM_IS_L3_IPV6LL(self));
+
+    if (!_state_has_lladdr(self->state)) {
+        nm_assert(!self->l3cd);
+        return NULL;
+    }
+
+    if (!self->l3cd) {
+        self->l3cd = _l3cd_config_create(nm_l3_ipv6ll_get_ifindex(self),
+                                         &self->cur_lladdr,
+                                         nm_l3cfg_get_multi_idx(self->l3cfg));
+    }
+
+    return self->l3cd;
+}
+
+/*****************************************************************************/
+
+static gboolean
+_emit_changed_on_idle_cb(gpointer user_data)
+{
+    NML3IPv6LL *           self = user_data;
+    const struct in6_addr *lladdr;
+    NML3IPv6LLState        state;
+    char                   sbuf[INET6_ADDRSTRLEN];
+
+    nm_clear_g_source_inst(&self->emit_changed_idle_source);
+
+    state = nm_l3_ipv6ll_get_state(self, &lladdr);
+
+    _LOGT("emit changed signal (state=%s%s%s)",
+          nm_l3_ipv6ll_state_to_string(state),
+          lladdr ? ", " : "",
+          lladdr ? _nm_utils_inet6_ntop(lladdr, sbuf) : "");
+
+    self->notify_fcn(self, state, lladdr, self->user_data);
+
+    return G_SOURCE_CONTINUE;
+}
+
+/*****************************************************************************/
+
+static gboolean
+_generate_new_address(NML3IPv6LL *self, struct in6_addr *out_lladdr)
+{
+    struct in6_addr lladdr;
+
+    memset(&lladdr, 0, sizeof(struct in6_addr));
+    lladdr.s6_addr16[0] = htons(0xfe80u);
+
+    if (self->addrgen.stable_type == NM_UTILS_STABLE_TYPE_NONE) {
+        if (self->addrgen.dad_counter > 0)
+            return FALSE;
+        self->addrgen.dad_counter++;
+        nm_utils_ipv6_addr_set_interface_identifier(&lladdr, &self->addrgen.token.iid);
+    } else {
+        /* RFC7217 says we MUST limit the number of retries, and it SHOULD try
+         * at least IDGEN_RETRIES times (that is, 3 times).
+         *
+         * 3 times seems really low. Instead, let's try 6 times. */
+        G_STATIC_ASSERT(NM_STABLE_PRIVACY_RFC7217_IDGEN_RETRIES == 3);
+        if (self->addrgen.dad_counter >= NM_STABLE_PRIVACY_RFC7217_IDGEN_RETRIES + 3)
+            return FALSE;
+
+        nm_utils_ipv6_addr_set_stable_privacy(self->addrgen.stable_type,
+                                              &lladdr,
+                                              self->addrgen.stable_privacy.ifname,
+                                              self->addrgen.stable_privacy.network_id,
+                                              self->addrgen.dad_counter++);
+    }
+
+    *out_lladdr = lladdr;
+    return TRUE;
+}
+
+/*****************************************************************************/
+
+static gboolean
+_pladdr_is_ll_failed(const NMPlatformIP6Address *addr)
+{
+    nm_assert(addr);
+    nm_assert(IN6_IS_ADDR_LINKLOCAL(&addr->address));
+
+    return NM_FLAGS_ANY(addr->n_ifa_flags, IFA_F_DADFAILED | IFA_F_DEPRECATED);
+}
+
+static gboolean
+_pladdr_is_ll_tentative(const NMPlatformIP6Address *addr)
+{
+    nm_assert(addr);
+    nm_assert(IN6_IS_ADDR_LINKLOCAL(&addr->address));
+    nm_assert(!_pladdr_is_ll_failed(addr));
+
+    return NM_FLAGS_HAS(addr->n_ifa_flags, IFA_F_TENTATIVE)
+           && !NM_FLAGS_HAS(addr->n_ifa_flags, IFA_F_OPTIMISTIC);
+}
+
+static const NMPlatformIP6Address *
+_pladdr_find_ll(NML3IPv6LL *self, gboolean *out_cur_addr_failed)
+{
+    NMDedupMultiIter            iter;
+    NMPLookup                   lookup;
+    const NMPlatformIP6Address *pladdr1 = NULL;
+    const NMPObject *           obj;
+    const NMPlatformIP6Address *pladdr_ready      = NULL;
+    const NMPlatformIP6Address *pladdr_tentative  = NULL;
+    gboolean                    cur_addr_check    = TRUE;
+    gboolean                    cur_addr_failed   = FALSE;
+    gboolean                    pladdr1_looked_up = FALSE;
+
+    nm_assert(!self->cur_lladdr_obj
+              || IN6_ARE_ADDR_EQUAL(&self->cur_lladdr, &self->cur_lladdr_obj->address));
+
+    *out_cur_addr_failed = FALSE;
+
+    if (self->state == NM_L3_IPV6LL_STATE_READY && self->cur_lladdr_obj) {
+        nm_assert(!_pladdr_is_ll_tentative(self->cur_lladdr_obj));
+        pladdr1 = NMP_OBJECT_CAST_IP6_ADDRESS(
+            nm_platform_lookup_obj(nm_l3_ipv6ll_get_platform(self),
+                                   NMP_CACHE_ID_TYPE_OBJECT_TYPE,
+                                   NMP_OBJECT_UP_CAST(self->cur_lladdr_obj)));
+        if (self->cur_lladdr_obj == pladdr1) {
+            /* Fast-path. We are ready and the cur_lladdr_obj is still in the cache. We
+             * got the result with a dictionary lookup without need to iterate over
+             * all addresses. */
+            return self->cur_lladdr_obj;
+        }
+        pladdr1_looked_up = TRUE;
+    }
+
+    if (!self->assume) {
+        /* We don't accept any suitable LL address, only he one we are waiting for.
+         * Let's do a dictionary lookup. */
+
+        if (IN6_IS_ADDR_LINKLOCAL(&self->cur_lladdr)) {
+            if (!pladdr1_looked_up) {
+                NMPObject needle;
+
+                nmp_object_stackinit_id_ip6_address(&needle,
+                                                    nm_l3_ipv6ll_get_ifindex(self),
+                                                    &self->cur_lladdr);
+                pladdr1 = NMP_OBJECT_CAST_IP6_ADDRESS(
+                    nm_platform_lookup_obj(nm_l3_ipv6ll_get_platform(self),
+                                           NMP_CACHE_ID_TYPE_OBJECT_TYPE,
+                                           &needle));
+            }
+            if (pladdr1) {
+                if (!_pladdr_is_ll_failed(pladdr1))
+                    return pladdr1;
+                *out_cur_addr_failed = TRUE;
+            }
+        } else
+            nm_assert(!pladdr1_looked_up);
+
+        return NULL;
+    }
+
+    if (!NM_IN_SET(self->state, NM_L3_IPV6LL_STATE_DAD_IN_PROGRESS, NM_L3_IPV6LL_STATE_READY))
+        cur_addr_check = FALSE;
+
+    nmp_lookup_init_object(&lookup, NMP_OBJECT_TYPE_IP6_ADDRESS, nm_l3_ipv6ll_get_ifindex(self));
+
+    nm_platform_iter_obj_for_each (&iter, nm_l3_ipv6ll_get_platform(self), &lookup, &obj) {
+        const NMPlatformIP6Address *pladdr = NMP_OBJECT_CAST_IP6_ADDRESS(obj);
+
+        if (!IN6_IS_ADDR_LINKLOCAL(&pladdr->address))
+            continue;
+
+        if (_pladdr_is_ll_failed(pladdr)) {
+            if (cur_addr_check && IN6_ARE_ADDR_EQUAL(&self->cur_lladdr, &pladdr->address)) {
+                /* "pladdr" is the address we are currently doing DAD for. But it failed.
+                 * We need to recognize and report to the caller, to stop waiting for this
+                 * address. */
+                cur_addr_failed = TRUE;
+                cur_addr_check  = FALSE;
+            }
+            continue;
+        }
+
+        if (_pladdr_is_ll_tentative(pladdr)) {
+            if (!pladdr_tentative)
+                pladdr_tentative = pladdr;
+            else if (pladdr == self->cur_lladdr_obj)
+                pladdr_tentative = pladdr;
+            else if (IN6_ARE_ADDR_EQUAL(&self->cur_lladdr, &pladdr->address))
+                pladdr_tentative = pladdr;
+            continue;
+        }
+
+        if (pladdr == self->cur_lladdr_obj) {
+            /* it doesn't get any better. We have our best address. */
+            return pladdr;
+        }
+        if (!pladdr_ready)
+            pladdr_ready = pladdr;
+        else if (IN6_ARE_ADDR_EQUAL(&self->cur_lladdr, &pladdr->address))
+            pladdr_ready = pladdr;
+    }
+
+    *out_cur_addr_failed = cur_addr_failed;
+    return pladdr_ready ?: pladdr_tentative;
+}
+
+/*****************************************************************************/
+
+static void
+_lladdr_handle_changed(NML3IPv6LL *self)
+{
+    const NML3ConfigData *l3cd;
+    gboolean              changed = FALSE;
+
+    /* We register the l3cd with l3cfg to start DAD. That is different from
+     * NML3IPv4LL, where we use NM_L3_CONFIG_MERGE_FLAGS_ONLY_FOR_ACD. The difference
+     * is that for IPv6 we let kernel do DAD, so we need to actually configure the
+     * address. For IPv4, we can run ACD without configuring anything in kernel,
+     * and let the user decide how to proceed.
+     *
+     * Also in this case, we use the most graceful commit-type (NM_L3_CFG_COMMIT_TYPE_ASSUME),
+     * but for that to work, we also need NM_L3CFG_CONFIG_FLAGS_ASSUME_CONFIG_ONCE flag. */
+
+    l3cd = nm_l3_ipv6ll_get_l3cd(self);
+
+    if (l3cd) {
+        if (nm_l3cfg_add_config(self->l3cfg,
+                                L3CD_TAG(self),
+                                TRUE,
+                                l3cd,
+                                NM_L3CFG_CONFIG_PRIORITY_IPV6LL,
+                                0,
+                                0,
+                                NM_PLATFORM_ROUTE_METRIC_DEFAULT_IP4,
+                                NM_PLATFORM_ROUTE_METRIC_DEFAULT_IP6,
+                                0,
+                                0,
+                                NM_DNS_PRIORITY_DEFAULT_NORMAL,
+                                NM_DNS_PRIORITY_DEFAULT_NORMAL,
+                                NM_L3_ACD_DEFEND_TYPE_ALWAYS,
+                                0,
+                                NM_L3CFG_CONFIG_FLAGS_ASSUME_CONFIG_ONCE,
+                                NM_L3_CONFIG_MERGE_FLAGS_NONE))
+            changed = TRUE;
+    } else {
+        if (nm_l3cfg_remove_config_all(self->l3cfg, L3CD_TAG(self)))
+            changed = TRUE;
+    }
+
+    self->l3cfg_commit_handle = nm_l3cfg_commit_type_register(self->l3cfg,
+                                                              l3cd ? NM_L3_CFG_COMMIT_TYPE_ASSUME
+                                                                   : NM_L3_CFG_COMMIT_TYPE_NONE,
+                                                              self->l3cfg_commit_handle,
+                                                              "ipv6ll");
+
+    if (changed)
+        nm_l3cfg_commit_on_idle_schedule(self->l3cfg, NM_L3_CFG_COMMIT_TYPE_AUTO);
+
+    if (!self->emit_changed_idle_source) {
+        _LOGT("schedule changed signal on idle");
+        self->emit_changed_idle_source = nm_g_idle_add_source(_emit_changed_on_idle_cb, self);
+    }
+}
+
+/*****************************************************************************/
+
+static gboolean
+_set_cur_lladdr(NML3IPv6LL *self, NML3IPv6LLState state, const struct in6_addr *lladdr)
+{
+    gboolean changed = FALSE;
+
+    if (lladdr) {
+        nm_assert(IN6_IS_ADDR_LINKLOCAL(lladdr));
+        if (!IN6_ARE_ADDR_EQUAL(&self->cur_lladdr, lladdr)) {
+            self->cur_lladdr = *lladdr;
+            nm_clear_l3cd(&self->l3cd);
+            changed = TRUE;
+        }
+    } else {
+        if (!nm_ip_addr_is_null(AF_INET6, &self->cur_lladdr)) {
+            nm_clear_l3cd(&self->l3cd);
+            self->cur_lladdr = nm_ip_addr_zero.addr6;
+            changed          = TRUE;
+        }
+        nm_assert(!self->l3cd);
+        nm_assert(!_state_has_lladdr(state));
+    }
+
+    if (self->state != state) {
+        if (!_state_has_lladdr(state))
+            nm_clear_l3cd(&self->l3cd);
+        self->state = state;
+        changed     = TRUE;
+    }
+
+    return changed;
+}
+
+static gboolean
+_set_cur_lladdr_obj(NML3IPv6LL *self, NML3IPv6LLState state, const NMPlatformIP6Address *lladdr_obj)
+{
+    nm_assert(lladdr_obj);
+    nm_assert(_state_has_lladdr(state));
+
+    nmp_object_ref_set_up_cast(&self->cur_lladdr_obj, lladdr_obj);
+    return _set_cur_lladdr(self, state, &lladdr_obj->address);
+}
+
+static gboolean
+_set_cur_lladdr_bin(NML3IPv6LL *self, NML3IPv6LLState state, const struct in6_addr *lladdr)
+{
+    nmp_object_ref_set_up_cast(&self->cur_lladdr_obj, NULL);
+    return _set_cur_lladdr(self, state, lladdr);
+}
+
+static gboolean
+_wait_for_addr_timeout_cb(gpointer user_data)
+{
+    NML3IPv6LL *self = user_data;
+
+    nm_clear_g_source_inst(&self->wait_for_addr_source);
+
+    nm_assert(
+        NM_IN_SET(self->state, NM_L3_IPV6LL_STATE_DAD_FAILED, NM_L3_IPV6LL_STATE_DAD_IN_PROGRESS));
+
+    _check(self);
+
+    return G_SOURCE_CONTINUE;
+}
+
+static void
+_check(NML3IPv6LL *self)
+{
+    const NMPlatformIP6Address *pladdr;
+    char                        sbuf[INET6_ADDRSTRLEN];
+    gboolean                    cur_addr_failed;
+    struct in6_addr             lladdr;
+
+    pladdr = _pladdr_find_ll(self, &cur_addr_failed);
+
+    if (pladdr) {
+        nm_clear_g_source_inst(&self->wait_for_addr_source);
+
+        if (_pladdr_is_ll_tentative(pladdr)) {
+            if (_set_cur_lladdr_obj(self, NM_L3_IPV6LL_STATE_DAD_IN_PROGRESS, pladdr)) {
+                _LOGT("changed: waiting for address %s to complete DAD",
+                      _nm_utils_inet6_ntop(&self->cur_lladdr, sbuf));
+                _lladdr_handle_changed(self);
+            }
+            return;
+        }
+
+        if (_set_cur_lladdr_obj(self, NM_L3_IPV6LL_STATE_READY, pladdr)) {
+            _LOGT("changed: address %s is ready", _nm_utils_inet6_ntop(&self->cur_lladdr, sbuf));
+            _lladdr_handle_changed(self);
+        }
+        return;
+    }
+
+    if (self->cur_lladdr_obj || cur_addr_failed) {
+        /* we were doing DAD, but the address is no longer a suitable candidate.
+         * Prematurely abort DAD to generate a new address below. */
+        nm_assert(
+            NM_IN_SET(self->state, NM_L3_IPV6LL_STATE_DAD_IN_PROGRESS, NM_L3_IPV6LL_STATE_READY));
+        if (self->state == NM_L3_IPV6LL_STATE_DAD_IN_PROGRESS)
+            _LOGT("changed: address %s did not complete DAD",
+                  _nm_utils_inet6_ntop(&self->cur_lladdr, sbuf));
+        else {
+            _LOGT("changed: address %s is gone", _nm_utils_inet6_ntop(&self->cur_lladdr, sbuf));
+        }
+
+        /* reset the state here, so that we are sure that the following
+         * _set_cur_lladdr_bin() calls (below) will notice the change
+         * and trigger a _lladdr_handle_changed(). */
+        _set_cur_lladdr_bin(self, NM_L3_IPV6LL_STATE_STARTING, NULL);
+        nm_clear_g_source_inst(&self->wait_for_addr_source);
+    } else if (self->wait_for_addr_source) {
+        /* we are waiting. Nothing to do for now. */
+        return;
+    }
+
+    if (!_generate_new_address(self, &lladdr)) {
+        /* our DAD counter expired. We reset it, and start a timer to retry
+         * and recover. */
+        self->addrgen.dad_counter = 0;
+        self->wait_for_addr_source =
+            nm_g_timeout_add_source(10000, _wait_for_addr_timeout_cb, self);
+        if (_set_cur_lladdr_bin(self, NM_L3_IPV6LL_STATE_DAD_FAILED, NULL)) {
+            _LOGW("changed: no IPv6 link local address to retry after Duplicate Address Detection "
+                  "failures (back off)");
+            _lladdr_handle_changed(self);
+        }
+        return;
+    }
+
+    /* we give NML3Cfg 2 seconds to configure the address on the interface. We
+     * thus very soon expect to see this address configured (and kernel started DAD).
+     * If that does not happen within timeout, we assume that this address failed DAD. */
+    self->wait_for_addr_source = nm_g_timeout_add_source(2000, _wait_for_addr_timeout_cb, self);
+    if (_set_cur_lladdr_bin(self, NM_L3_IPV6LL_STATE_DAD_IN_PROGRESS, &lladdr)) {
+        _LOGT("changed: starting DAD for address %s",
+              _nm_utils_inet6_ntop(&self->cur_lladdr, sbuf));
+        _lladdr_handle_changed(self);
+    }
+    return;
+}
+
+/*****************************************************************************/
+
+static void
+_l3cfg_notify_cb(NML3Cfg *l3cfg, const NML3ConfigNotifyData *notify_data, NML3IPv6LL *self)
+{
+    if (notify_data->notify_type == NM_L3_CONFIG_NOTIFY_TYPE_PLATFORM_CHANGE_ON_IDLE) {
+        if (NM_FLAGS_ANY(notify_data->platform_change_on_idle.obj_type_flags,
+                         nmp_object_type_to_flags(NMP_OBJECT_TYPE_IP6_ADDRESS)))
+            _check(self);
+        return;
+    }
+}
+
+/*****************************************************************************/
+
+static gboolean
+_starting_on_idle_cb(gpointer user_data)
+{
+    NML3IPv6LL *self = user_data;
+
+    nm_clear_g_source_inst(&self->starting_on_idle_source);
+
+    self->l3cfg_signal_notify_id =
+        g_signal_connect(self->l3cfg, NM_L3CFG_SIGNAL_NOTIFY, G_CALLBACK(_l3cfg_notify_cb), self);
+
+    _check(self);
+
+    return G_SOURCE_CONTINUE;
+}
+
+/*****************************************************************************/
+
+NML3IPv6LL *
+_nm_l3_ipv6ll_new(NML3Cfg *                 l3cfg,
+                  gboolean                  assume,
+                  NMUtilsStableType         stable_type,
+                  const char *              ifname,
+                  const char *              network_id,
+                  const NMUtilsIPv6IfaceId *token_iid,
+                  NML3IPv6LLNotifyFcn       notify_fcn,
+                  gpointer                  user_data)
+{
+    NML3IPv6LL *self;
+
+    g_return_val_if_fail(NM_IS_L3CFG(l3cfg), NULL);
+    g_return_val_if_fail(notify_fcn, NULL);
+    g_return_val_if_fail(
+        (stable_type == NM_UTILS_STABLE_TYPE_NONE && !ifname && !network_id && token_iid)
+            || (stable_type != NM_UTILS_STABLE_TYPE_NONE && ifname && network_id && !token_iid),
+        NULL);
+
+    self  = g_slice_new(NML3IPv6LL);
+    *self = (NML3IPv6LL){
+        .l3cfg                   = g_object_ref(l3cfg),
+        .notify_fcn              = notify_fcn,
+        .user_data               = user_data,
+        .state                   = NM_L3_IPV6LL_STATE_STARTING,
+        .starting_on_idle_source = nm_g_idle_add_source(_starting_on_idle_cb, self),
+        .l3cfg_signal_notify_id  = 0,
+        .cur_lladdr_obj          = NULL,
+        .cur_lladdr              = IN6ADDR_ANY_INIT,
+        .assume                  = assume,
+        .addrgen =
+            {
+                .stable_type = stable_type,
+                .dad_counter = 0,
+            },
+    };
+
+    if (self->addrgen.stable_type == NM_UTILS_STABLE_TYPE_NONE) {
+        char sbuf_token[sizeof(self->addrgen.token.iid) * 3];
+
+        self->addrgen.token.iid = *token_iid;
+        _LOGT("created: l3cfg=" NM_HASH_OBFUSCATE_PTR_FMT ", ifindex=%d, token=%s%s",
+              NM_HASH_OBFUSCATE_PTR(l3cfg),
+              nm_l3cfg_get_ifindex(l3cfg),
+              nm_utils_bin2hexstr_full(&self->addrgen.token.iid,
+                                       sizeof(self->addrgen.token.iid),
+                                       ':',
+                                       FALSE,
+                                       sbuf_token),
+              self->assume ? ", assume" : "");
+    } else {
+        self->addrgen.stable_privacy.ifname     = g_strdup(ifname);
+        self->addrgen.stable_privacy.network_id = g_strdup(network_id);
+        _LOGT("created: l3cfg=" NM_HASH_OBFUSCATE_PTR_FMT
+              ", ifindex=%d, stable-type=%u, ifname=%s, network_id=%s%s",
+              NM_HASH_OBFUSCATE_PTR(l3cfg),
+              nm_l3cfg_get_ifindex(l3cfg),
+              (unsigned) self->addrgen.stable_type,
+              self->addrgen.stable_privacy.ifname,
+              self->addrgen.stable_privacy.network_id,
+              self->assume ? ", assume" : "");
+    }
+
+    return self;
+}
+
+void
+nm_l3_ipv6ll_destroy(NML3IPv6LL *self)
+{
+    if (!self)
+        return;
+
+    _ASSERT(self);
+
+    _LOGT("finalize");
+
+    nm_l3cfg_commit_type_unregister(self->l3cfg, g_steal_pointer(&self->l3cfg_commit_handle));
+
+    nm_l3cfg_remove_config_all(self->l3cfg, L3CD_TAG(self));
+
+    nm_clear_g_source_inst(&self->starting_on_idle_source);
+    nm_clear_g_source_inst(&self->wait_for_addr_source);
+    nm_clear_g_source_inst(&self->emit_changed_idle_source);
+    nm_clear_g_signal_handler(self->l3cfg, &self->l3cfg_signal_notify_id);
+
+    g_clear_object(&self->l3cfg);
+
+    nm_clear_l3cd(&self->l3cd);
+
+    nm_clear_nmp_object_up_cast(&self->cur_lladdr_obj);
+
+    if (self->addrgen.stable_type != NM_UTILS_STABLE_TYPE_NONE) {
+        g_free((char *) self->addrgen.stable_privacy.ifname);
+        g_free((char *) self->addrgen.stable_privacy.network_id);
+    }
+
+    nm_g_slice_free(self);
+}