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..05149e66a6 --- /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 + +#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. */ + 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_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 9baef4e359..1b9eadea32 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..e4c6beb4b9 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 + #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_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); }