summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorThomas Haller <thaller@redhat.com>2021-08-25 11:30:35 +0200
committerThomas Haller <thaller@redhat.com>2021-09-09 19:23:53 +0200
commit1c7e411d2b6d53adb527df358443b02a392e9ec5 (patch)
tree9d04682db1cd4c8c53c9e90aa07bccc4b700fb13
parent4942597db109d97f3c8337083ce470975906d54d (diff)
downloadNetworkManager-th/l3cfg-ipv6ll.tar.gz
core: add NML3IPv6LL helperth/l3cfg-ipv6ll
This helper class is supposed to encapsulate most logic about configuring IPv6 link local addresses and exposes a simpler API in order to simplify NMDevice. Currently this logic is spread out in NMDevice. Also, NML3IPv6LL directly uses NML3Cfg, thereby freeing NMDevice to care about that too much. For several reasons, NML3IPv6LL works different than NML3IPv4LL. For one, with IPv6 we need to configure the address in kernel, which does DAD for us. So, NML3IPv6LL will tell NML3Cfg to configure those addresses that it wants to probe. For IPv4, it only tells NML3Cfg to do ACD, without configuring anything yet. That is left to the caller.
-rw-r--r--Makefile.am2
-rw-r--r--src/core/meson.build1
-rw-r--r--src/core/nm-core-utils.h2
-rw-r--r--src/core/nm-l3-ipv6ll.c713
-rw-r--r--src/core/nm-l3-ipv6ll.h106
-rw-r--r--src/core/nm-l3cfg.c2
-rw-r--r--src/core/nm-l3cfg.h1
-rw-r--r--src/core/tests/test-l3cfg.c277
8 files changed, 1103 insertions, 1 deletions
diff --git a/Makefile.am b/Makefile.am
index 32e88ee480..22634626d0 100644
--- a/Makefile.am
+++ b/Makefile.am
@@ -2382,6 +2382,8 @@ src_core_libNetworkManagerBase_la_SOURCES = \
src/core/nm-l3-config-data.h \
src/core/nm-l3-ipv4ll.c \
src/core/nm-l3-ipv4ll.h \
+ src/core/nm-l3-ipv6ll.c \
+ src/core/nm-l3-ipv6ll.h \
src/core/nm-l3cfg.c \
src/core/nm-l3cfg.h \
src/core/nm-ip-config.c \
diff --git a/src/core/meson.build b/src/core/meson.build
index 1a3f334fd8..46b636817d 100644
--- a/src/core/meson.build
+++ b/src/core/meson.build
@@ -51,6 +51,7 @@ libNetworkManagerBase = static_library(
'nm-netns.c',
'nm-l3-config-data.c',
'nm-l3-ipv4ll.c',
+ 'nm-l3-ipv6ll.c',
'nm-l3cfg.c',
'nm-ip-config.c',
'nm-ip4-config.c',
diff --git a/src/core/nm-core-utils.h b/src/core/nm-core-utils.h
index 614df02e75..38f56adc08 100644
--- a/src/core/nm-core-utils.h
+++ b/src/core/nm-core-utils.h
@@ -298,6 +298,8 @@ typedef enum {
NM_UTILS_STABLE_TYPE_RANDOM = 3,
} NMUtilsStableType;
+#define NM_UTILS_STABLE_TYPE_NONE ((NMUtilsStableType) -1)
+
NMUtilsStableType nm_utils_stable_id_parse(const char *stable_id,
const char *deviceid,
const char *hwaddr,
diff --git a/src/core/nm-l3-ipv6ll.c b/src/core/nm-l3-ipv6ll.c
new file mode 100644
index 0000000000..24079fcb81
--- /dev/null
+++ b/src/core/nm-l3-ipv6ll.c
@@ -0,0 +1,713 @@
+/* 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(next): 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_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. */
+ 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_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);
+
+ if (changed)
+ nm_l3cfg_commit_on_idle_schedule(self->l3cfg);
+
+ 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);
+}
diff --git a/src/core/nm-l3-ipv6ll.h b/src/core/nm-l3-ipv6ll.h
new file mode 100644
index 0000000000..8125b5d06a
--- /dev/null
+++ b/src/core/nm-l3-ipv6ll.h
@@ -0,0 +1,106 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#ifndef __NM_L3_IPV6LL_H__
+#define __NM_L3_IPV6LL_H__
+
+#include "nm-l3cfg.h"
+#include "nm-core-utils.h"
+
+/*****************************************************************************/
+
+typedef struct _NML3IPv6LL NML3IPv6LL;
+
+typedef enum _nm_packed {
+
+ /* NONE is not actually used by NML3IPv6LL. This is a bogus placeholder
+ * state for external users. */
+ NM_L3_IPV6LL_STATE_NONE,
+
+ NM_L3_IPV6LL_STATE_STARTING,
+ NM_L3_IPV6LL_STATE_DAD_IN_PROGRESS,
+ NM_L3_IPV6LL_STATE_READY,
+ NM_L3_IPV6LL_STATE_DAD_FAILED,
+} NML3IPv6LLState;
+
+const char *nm_l3_ipv6ll_state_to_string(NML3IPv6LLState state);
+
+typedef void (*NML3IPv6LLNotifyFcn)(NML3IPv6LL * ipv6ll,
+ NML3IPv6LLState state,
+ const struct in6_addr *lladdr,
+ gpointer user_data);
+
+static inline gboolean
+NM_IS_L3_IPV6LL(const NML3IPv6LL *self)
+{
+ nm_assert(!self || (NM_IS_L3CFG(*((NML3Cfg **) self))));
+ return !!self;
+}
+
+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);
+
+static inline NML3IPv6LL *
+nm_l3_ipv6ll_new_stable_privacy(NML3Cfg * l3cfg,
+ gboolean assume,
+ NMUtilsStableType stable_type,
+ const char * ifname,
+ const char * network_id,
+ NML3IPv6LLNotifyFcn notify_fcn,
+ gpointer user_data)
+{
+ nm_assert(stable_type != NM_UTILS_STABLE_TYPE_NONE);
+ return _nm_l3_ipv6ll_new(l3cfg,
+ assume,
+ stable_type,
+ ifname,
+ network_id,
+ NULL,
+ notify_fcn,
+ user_data);
+}
+
+static inline NML3IPv6LL *
+nm_l3_ipv6ll_new_token(NML3Cfg * l3cfg,
+ gboolean assume,
+ const NMUtilsIPv6IfaceId *token_iid,
+ NML3IPv6LLNotifyFcn notify_fcn,
+ gpointer user_data)
+{
+ return _nm_l3_ipv6ll_new(l3cfg,
+ assume,
+ NM_UTILS_STABLE_TYPE_NONE,
+ NULL,
+ NULL,
+ token_iid,
+ notify_fcn,
+ user_data);
+}
+
+void nm_l3_ipv6ll_destroy(NML3IPv6LL *self);
+
+NM_AUTO_DEFINE_FCN0(NML3IPv6LL *, _nm_auto_destroy_l3ipv6ll, nm_l3_ipv6ll_destroy);
+#define nm_auto_destroy_l3ipv6ll nm_auto(_nm_auto_destroy_l3ipv6ll)
+
+/*****************************************************************************/
+
+NML3Cfg *nm_l3_ipv6ll_get_l3cfg(NML3IPv6LL *self);
+
+int nm_l3_ipv6ll_get_ifindex(NML3IPv6LL *self);
+
+NMPlatform *nm_l3_ipv6ll_get_platform(NML3IPv6LL *self);
+
+/*****************************************************************************/
+
+NML3IPv6LLState nm_l3_ipv6ll_get_state(NML3IPv6LL *self, const struct in6_addr **out_lladdr);
+
+const NML3ConfigData *nm_l3_ipv6ll_get_l3cd(NML3IPv6LL *self);
+
+/*****************************************************************************/
+
+#endif /* __NM_L3_IPV6LL_H__ */
diff --git a/src/core/nm-l3cfg.c b/src/core/nm-l3cfg.c
index 92c382b0cc..607519179b 100644
--- a/src/core/nm-l3cfg.c
+++ b/src/core/nm-l3cfg.c
@@ -3806,7 +3806,7 @@ nm_l3cfg_commit_type_get(NML3Cfg *self)
* a certain @commit_type. The "higher" commit type is the used one when calling
* nm_l3cfg_commit() with %NM_L3_CFG_COMMIT_TYPE_AUTO.
*
- * Returns: a handle tracking the registration, or %NULL of @commit_type
+ * Returns: a handle tracking the registration, or %NULL if @commit_type
* is %NM_L3_CFG_COMMIT_TYPE_NONE.
*/
NML3CfgCommitTypeHandle *
diff --git a/src/core/nm-l3cfg.h b/src/core/nm-l3cfg.h
index 9190e1f398..193ff72a7c 100644
--- a/src/core/nm-l3cfg.h
+++ b/src/core/nm-l3cfg.h
@@ -7,6 +7,7 @@
#include "nm-l3-config-data.h"
#define NM_L3CFG_CONFIG_PRIORITY_IPV4LL 0
+#define NM_L3CFG_CONFIG_PRIORITY_IPV6LL 0
#define NM_ACD_TIMEOUT_RFC5227_MSEC 9000u
#define NM_TYPE_L3CFG (nm_l3cfg_get_type())
diff --git a/src/core/tests/test-l3cfg.c b/src/core/tests/test-l3cfg.c
index 09efb92465..00bca40ea5 100644
--- a/src/core/tests/test-l3cfg.c
+++ b/src/core/tests/test-l3cfg.c
@@ -2,8 +2,11 @@
#include "src/core/nm-default-daemon.h"
+#include <linux/if_addr.h>
+
#include "nm-l3cfg.h"
#include "nm-l3-ipv4ll.h"
+#include "nm-l3-ipv6ll.h"
#include "nm-netns.h"
#include "libnm-platform/nm-platform.h"
@@ -780,6 +783,276 @@ test_l3_ipv4ll(gconstpointer test_data)
/*****************************************************************************/
+#define _LLADDR_TEST1 "fe80::dd5a:8a44:48bc:3ad"
+#define _LLADDR_TEST2 "fe80::878b:938e:46f9:4807"
+
+typedef struct {
+ const TestFixture1 *f;
+ NML3Cfg * l3cfg0;
+ NML3IPv6LL * l3ipv6ll;
+ int step;
+ int ipv6ll_callback_step;
+ bool steps_done : 1;
+ const NMPObject * lladdr0;
+} TestL3IPv6LLData;
+
+static const NMPlatformIP6Address *
+_test_l3_ipv6ll_find_lladdr(TestL3IPv6LLData *tdata, int ifindex)
+{
+ const NMPlatformIP6Address *found = NULL;
+ NMDedupMultiIter iter;
+ const NMPObject * obj;
+ NMPLookup lookup;
+
+ g_assert(tdata);
+
+ nmp_lookup_init_object(&lookup, NMP_OBJECT_TYPE_IP6_ADDRESS, ifindex);
+ nm_platform_iter_obj_for_each (&iter, tdata->f->platform, &lookup, &obj) {
+ const NMPlatformIP6Address *a = NMP_OBJECT_CAST_IP6_ADDRESS(obj);
+
+ if (!IN6_IS_ADDR_LINKLOCAL(&a->address))
+ continue;
+
+ if (!found)
+ found = a;
+ else
+ g_assert_not_reached();
+ }
+
+ return found;
+}
+
+static const NMPObject *
+_test_l3_ipv6ll_find_lladdr_wait(TestL3IPv6LLData *tdata, int ifindex)
+{
+ const NMPObject *obj = NULL;
+
+ nmtst_main_context_iterate_until_assert(NULL, 3000, ({
+ const NMPlatformIP6Address *a;
+
+ a = _test_l3_ipv6ll_find_lladdr(tdata, ifindex);
+ if (a
+ && !NM_FLAGS_HAS(a->n_ifa_flags,
+ IFA_F_TENTATIVE))
+ obj = nmp_object_ref(NMP_OBJECT_UP_CAST(a));
+ obj;
+ }));
+
+ return obj;
+}
+
+static const NMPlatformIP6Address *
+_test_l3_ipv6ll_find_inet6(TestL3IPv6LLData *tdata, const struct in6_addr *addr)
+{
+ const NMPlatformIP6Address *a;
+
+ a = nmtstp_platform_ip6_address_find(nm_l3cfg_get_platform(tdata->l3cfg0),
+ nmtst_get_rand_bool() ? 0 : tdata->f->ifindex0,
+ addr);
+ if (a) {
+ g_assert_cmpint(a->ifindex, ==, tdata->f->ifindex0);
+ g_assert_cmpmem(addr, sizeof(*addr), &a->address, sizeof(a->address));
+ }
+
+ g_assert(a
+ == nm_platform_ip6_address_get(nm_l3cfg_get_platform(tdata->l3cfg0),
+ tdata->f->ifindex0,
+ addr));
+
+ return a;
+}
+
+static void
+_test_l3_ipv6ll_signal_notify(NML3Cfg * l3cfg,
+ const NML3ConfigNotifyData *notify_data,
+ TestL3IPv6LLData * tdata)
+{
+ g_assert_cmpint(tdata->step, >=, 1);
+ g_assert_cmpint(tdata->step, <=, 2);
+}
+
+static void
+_test_l3_ipv6ll_callback_changed(NML3IPv6LL * ipv6ll,
+ NML3IPv6LLState state,
+ const struct in6_addr *lladdr,
+ gpointer user_data)
+{
+ TestL3IPv6LLData * tdata = user_data;
+ int step = tdata->ipv6ll_callback_step++;
+ const NMPlatformIP6Address *a1;
+
+ g_assert_cmpint(tdata->step, ==, 1);
+ g_assert(!tdata->steps_done);
+
+ switch (step) {
+ case 0:
+ if (NM_IN_SET(tdata->f->test_idx, 1, 2, 4)) {
+ g_assert_cmpint(state, ==, NM_L3_IPV6LL_STATE_DAD_IN_PROGRESS);
+ g_assert_cmpstr(nmtst_inet6_to_string(lladdr), ==, _LLADDR_TEST1);
+ } else if (NM_IN_SET(tdata->f->test_idx, 3)) {
+ g_assert_cmpint(state, ==, NM_L3_IPV6LL_STATE_READY);
+ g_assert(
+ IN6_ARE_ADDR_EQUAL(lladdr, &NMP_OBJECT_CAST_IP6_ADDRESS(tdata->lladdr0)->address));
+ tdata->steps_done = TRUE;
+ } else
+ g_assert_not_reached();
+ break;
+ case 1:
+ if (NM_IN_SET(tdata->f->test_idx, 1, 2)) {
+ g_assert_cmpint(state, ==, NM_L3_IPV6LL_STATE_READY);
+ g_assert_cmpstr(nmtst_inet6_to_string(lladdr), ==, _LLADDR_TEST1);
+ a1 = _test_l3_ipv6ll_find_inet6(tdata, lladdr);
+ g_assert(a1);
+ g_assert(!NM_FLAGS_HAS(a1->n_ifa_flags, IFA_F_TENTATIVE));
+ tdata->steps_done = TRUE;
+ } else if (NM_IN_SET(tdata->f->test_idx, 4)) {
+ g_assert_cmpint(state, ==, NM_L3_IPV6LL_STATE_DAD_IN_PROGRESS);
+ g_assert_cmpstr(nmtst_inet6_to_string(lladdr), ==, _LLADDR_TEST2);
+ } else
+ g_assert_not_reached();
+ break;
+ case 2:
+ if (NM_IN_SET(tdata->f->test_idx, 4)) {
+ g_assert_cmpint(state, ==, NM_L3_IPV6LL_STATE_READY);
+ g_assert_cmpstr(nmtst_inet6_to_string(lladdr), ==, _LLADDR_TEST2);
+ a1 = _test_l3_ipv6ll_find_inet6(tdata, lladdr);
+ g_assert(a1);
+ g_assert(!NM_FLAGS_HAS(a1->n_ifa_flags, IFA_F_TENTATIVE));
+ tdata->steps_done = TRUE;
+ } else
+ g_assert_not_reached();
+ break;
+ default:
+ g_assert_not_reached();
+ }
+}
+
+static void
+test_l3_ipv6ll(gconstpointer test_data)
+{
+ NMTST_UTILS_HOST_ID_CONTEXT("l3-ipv6ll");
+ const int TEST_IDX = GPOINTER_TO_INT(test_data);
+ nm_auto(_test_fixture_1_teardown) TestFixture1 test_fixture = {};
+ gs_unref_object NML3Cfg *l3cfg0 = NULL;
+ TestL3IPv6LLData tdata_stack = {
+ .step = 0,
+ .steps_done = FALSE,
+ };
+ TestL3IPv6LLData *const tdata = &tdata_stack;
+ char sbuf1[sizeof(_nm_utils_to_string_buffer)];
+ int r;
+
+ _LOGD("test start (/l3-ipv6ll/%d)", TEST_IDX);
+
+ if (nmtst_test_quick()) {
+ gs_free char *msg =
+ g_strdup_printf("Skipping test: don't run long running test %s (NMTST_DEBUG=slow)\n",
+ g_get_prgname() ?: "test-l3-ipv6ll");
+
+ g_test_skip(msg);
+ return;
+ }
+
+ tdata->f = _test_fixture_1_setup(&test_fixture, TEST_IDX);
+
+ if (NM_IN_SET(tdata->f->test_idx, 4)) {
+ _LOGD("add conflicting IPv6LL on other interface...");
+ r = nm_platform_link_change_flags(tdata->f->platform, tdata->f->ifindex1, IFF_UP, FALSE);
+ g_assert_cmpint(r, >=, 0);
+
+ r = nm_platform_link_set_inet6_addr_gen_mode(tdata->f->platform,
+ tdata->f->ifindex1,
+ NM_IN6_ADDR_GEN_MODE_NONE);
+ g_assert_cmpint(r, >=, 0);
+
+ r = nm_platform_link_change_flags(tdata->f->platform, tdata->f->ifindex1, IFF_UP, TRUE);
+ g_assert_cmpint(r, >=, 0);
+
+ nmtstp_ip6_address_add(tdata->f->platform,
+ -1,
+ tdata->f->ifindex1,
+ *nmtst_inet6_from_string(_LLADDR_TEST1),
+ 64,
+ in6addr_any,
+ NM_PLATFORM_LIFETIME_PERMANENT,
+ NM_PLATFORM_LIFETIME_PERMANENT,
+ 0);
+
+ _LOGD("wait for IPv6 LL address...");
+ tdata->lladdr0 =
+ nmp_object_ref(_test_l3_ipv6ll_find_lladdr_wait(tdata, tdata->f->ifindex1));
+ } else if (NM_IN_SET(tdata->f->test_idx, 2, 3)) {
+ _LOGD("wait for IPv6 LL address...");
+ tdata->lladdr0 =
+ nmp_object_ref(_test_l3_ipv6ll_find_lladdr_wait(tdata, tdata->f->ifindex0));
+ }
+
+ if (tdata->lladdr0) {
+ _LOGD("got IPv6 LL address %s",
+ nmp_object_to_string(tdata->lladdr0,
+ NMP_OBJECT_TO_STRING_PUBLIC,
+ sbuf1,
+ sizeof(sbuf1)));
+ }
+
+ l3cfg0 = _netns_access_l3cfg(tdata->f->netns, tdata->f->ifindex0);
+ tdata->l3cfg0 = l3cfg0;
+
+ g_signal_connect(tdata->l3cfg0,
+ NM_L3CFG_SIGNAL_NOTIFY,
+ G_CALLBACK(_test_l3_ipv6ll_signal_notify),
+ tdata);
+
+ tdata->l3ipv6ll = nm_l3_ipv6ll_new_stable_privacy(tdata->l3cfg0,
+ NM_IN_SET(tdata->f->test_idx, 3),
+ NM_UTILS_STABLE_TYPE_UUID,
+ tdata->f->ifname0,
+ "b6a5b934-c649-43dc-a524-3dfdb74f9419",
+ _test_l3_ipv6ll_callback_changed,
+ tdata);
+
+ g_assert(nm_l3_ipv6ll_get_l3cfg(tdata->l3ipv6ll) == tdata->l3cfg0);
+ g_assert_cmpint(nm_l3_ipv6ll_get_ifindex(tdata->l3ipv6ll), ==, tdata->f->ifindex0);
+
+ tdata->step = 1;
+ nmtst_main_context_iterate_until_assert(NULL, 7000, tdata->steps_done);
+
+ g_assert_cmpint(tdata->step, ==, 1);
+ if (NM_IN_SET(tdata->f->test_idx, 3))
+ g_assert_cmpint(tdata->ipv6ll_callback_step, ==, 1);
+ else if (NM_IN_SET(tdata->f->test_idx, 4))
+ g_assert_cmpint(tdata->ipv6ll_callback_step, ==, 3);
+ else
+ g_assert_cmpint(tdata->ipv6ll_callback_step, ==, 2);
+ g_assert(tdata->steps_done);
+
+ tdata->step = 2;
+ nmtst_main_context_iterate_until(NULL, nmtst_get_rand_uint32() % 1000, FALSE);
+
+ g_assert_cmpint(tdata->step, ==, 2);
+ if (NM_IN_SET(tdata->f->test_idx, 3))
+ g_assert_cmpint(tdata->ipv6ll_callback_step, ==, 1);
+ else if (NM_IN_SET(tdata->f->test_idx, 4))
+ g_assert_cmpint(tdata->ipv6ll_callback_step, ==, 3);
+ else
+ g_assert_cmpint(tdata->ipv6ll_callback_step, ==, 2);
+ g_assert(tdata->steps_done);
+ g_assert(tdata->steps_done);
+
+ tdata->step = 0;
+ tdata->steps_done = FALSE;
+
+ g_signal_handlers_disconnect_by_func(tdata->l3cfg0,
+ G_CALLBACK(_test_l3_ipv6ll_signal_notify),
+ tdata);
+
+ nm_l3_ipv6ll_destroy(tdata->l3ipv6ll);
+
+ nm_clear_nmp_object(&tdata->lladdr0);
+}
+
+/*****************************************************************************/
+
NMTstpSetupFunc const _nmtstp_setup_platform_func = nm_linux_platform_setup;
void
@@ -797,4 +1070,8 @@ _nmtstp_setup_tests(void)
g_test_add_data_func("/l3cfg/4", GINT_TO_POINTER(4), test_l3cfg);
g_test_add_data_func("/l3-ipv4ll/1", GINT_TO_POINTER(1), test_l3_ipv4ll);
g_test_add_data_func("/l3-ipv4ll/2", GINT_TO_POINTER(2), test_l3_ipv4ll);
+ g_test_add_data_func("/l3-ipv6ll/1", GINT_TO_POINTER(1), test_l3_ipv6ll);
+ g_test_add_data_func("/l3-ipv6ll/2", GINT_TO_POINTER(2), test_l3_ipv6ll);
+ g_test_add_data_func("/l3-ipv6ll/3", GINT_TO_POINTER(3), test_l3_ipv6ll);
+ g_test_add_data_func("/l3-ipv6ll/4", GINT_TO_POINTER(4), test_l3_ipv6ll);
}