/* SPDX-License-Identifier: LGPL-2.1-or-later */

#include "src/core/nm-default-daemon.h"

#include "nm-l3-ipv6ll.h"

#include "nm-compat-headers/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;

    guint32 route_table;

    /* "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_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_by_ifindex(&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, gboolean force_commit)
{
    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. */

    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,
                                self->route_table,
                                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_NONE,
                                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_UPDATE
                                                                   : NM_L3_CFG_COMMIT_TYPE_NONE,
                                                              self->l3cfg_commit_handle,
                                                              "ipv6ll");

    if (changed || force_commit)
        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;
    gboolean                    restarted = FALSE;
    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_inet6_ntop(&self->cur_lladdr, sbuf));
                _lladdr_handle_changed(self, FALSE);
            }
            return;
        }

        if (_set_cur_lladdr_obj(self, NM_L3_IPV6LL_STATE_READY, pladdr)) {
            _LOGT("changed: address %s is ready", nm_inet6_ntop(&self->cur_lladdr, sbuf));
            _lladdr_handle_changed(self, FALSE);
        }
        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 (cur_addr_failed) {
            /* On DAD failure, we always try to regenerate a new address. */
            _LOGT("changed: address %s failed", nm_inet6_ntop(&self->cur_lladdr, sbuf));
        } else {
            _LOGT("changed: address %s is gone", nm_inet6_ntop(&self->cur_lladdr, sbuf));
            /* When the address is removed, we always try to re-add it. */
            nm_clear_g_source_inst(&self->wait_for_addr_source);
            lladdr    = self->cur_lladdr;
            restarted = TRUE;
            goto commit;
        }

        /* 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, FALSE);
        }
        return;
    }

commit:
    /* 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) || restarted) {
        _LOGT("changed: starting DAD for address %s", nm_inet6_ntop(&self->cur_lladdr, sbuf));
        _lladdr_handle_changed(self, restarted);
    }
    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,
                  guint32                   route_table,
                  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,
        .route_table             = route_table,
        .addrgen =
            {
                .stable_type = stable_type,
                .dad_counter = 0,
            },
    };

    if (self->addrgen.stable_type == NM_UTILS_STABLE_TYPE_NONE) {
        char sbuf_token[INET6_ADDRSTRLEN];

        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_inet6_interface_identifier_to_token(&self->addrgen.token.iid, 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);
}
