From 66c7272eef1358f8b86a413c3b73d7d586fa97cf Mon Sep 17 00:00:00 2001 From: Abtin Keshavarzian Date: Mon, 9 Feb 2026 14:31:34 -0800 Subject: [PATCH] [border-agent] implement Border Admitter (#12201) This commit introduces the Border Agent Admitter feature , which enhances Thread MeshCoP. A Border Admitter is an enhanced Border Agent that functions as a traditional BA while enabling new behaviors. It acts as a proxy and dispatcher, allowing multiple external Enrollers to connect to it. It then petitions to become the single Active Commissioner on the Thread mesh, forwards new joiner requests to connected Enrollers, and manages the session between joiners and multiple Enrollers The implementation includes three main components: - `Admitter`: The main class that orchestrates the feature, managing enroller sessions and aggregating steering data. - `Arbitrator`: A distributed election mechanism that runs among Border Admitters on the mesh to select a single "Prime Admitter" by publishing a new Border Admitter service in the Thread Network Data. - `CommissionerPetitioner`: A sub-component responsible for petitioning the Leader to be granted the commissioner role. It handles conflicts if another commissioner is already active. New public APIs are added in `openthread/border_agent_admitter.h`. This commit also introduces a suite of comprehensive tests for the Border Admitter functionality under the `nexus` test framework. It covers various scenarios including: - Prime Admitter election and role management - Enroller registration, keep-alive, and timeout interactions - Handling of commissioner conflicts and petitioner retry mechanisms - Support for multiple enroller sessions and combined steering data - Forwarding of Joiner `RelayRx` and `UdpProxy` messages - Joiner acceptance, release, and expiration tracking --- etc/cmake/options.cmake | 1 + .../config/ot-core-config-check-size-br.h | 1 + include/openthread/BUILD.gn | 1 + include/openthread/border_agent_admitter.h | 237 ++ include/openthread/border_agent_txt_data.h | 17 +- include/openthread/dataset.h | 3 + include/openthread/instance.h | 2 +- src/core/BUILD.gn | 4 + src/core/CMakeLists.txt | 2 + src/core/api/border_agent_admitter_api.cpp | 99 + src/core/common/notifier.cpp | 3 + src/core/config/border_agent_admitter.h | 84 + src/core/instance/instance.cpp | 3 + src/core/instance/instance.hpp | 9 + src/core/meshcop/border_agent.cpp | 121 +- src/core/meshcop/border_agent.hpp | 39 + src/core/meshcop/border_agent_admitter.cpp | 1464 ++++++++ src/core/meshcop/border_agent_admitter.hpp | 459 +++ src/core/meshcop/border_agent_txt_data.cpp | 14 +- src/core/meshcop/border_agent_txt_data.hpp | 34 +- src/core/meshcop/meshcop_tlvs.hpp | 28 + src/core/meshcop/steering_data.cpp | 5 + src/core/meshcop/steering_data.hpp | 13 +- src/core/net/netif.hpp | 7 + src/core/openthread-core-config.h | 1 + src/core/thread/network_data_publisher.cpp | 129 + src/core/thread/network_data_publisher.hpp | 50 + src/core/thread/network_data_service.cpp | 34 + src/core/thread/network_data_service.hpp | 36 + src/core/thread/tmf.cpp | 15 +- src/core/thread/tmf.hpp | 29 +- src/core/thread/uri_paths.cpp | 15 + src/core/thread/uri_paths.hpp | 10 + tests/nexus/CMakeLists.txt | 1 + tests/nexus/openthread-core-nexus-config.h | 3 + tests/nexus/test_border_admitter.cpp | 3343 +++++++++++++++++ tests/nexus/test_border_agent.cpp | 93 + tests/toranj/openthread-core-toranj-config.h | 2 + 38 files changed, 6361 insertions(+), 50 deletions(-) create mode 100644 include/openthread/border_agent_admitter.h create mode 100644 src/core/api/border_agent_admitter_api.cpp create mode 100644 src/core/config/border_agent_admitter.h create mode 100644 src/core/meshcop/border_agent_admitter.cpp create mode 100644 src/core/meshcop/border_agent_admitter.hpp create mode 100644 tests/nexus/test_border_admitter.cpp diff --git a/etc/cmake/options.cmake b/etc/cmake/options.cmake index 49b63b498..e1905da0a 100644 --- a/etc/cmake/options.cmake +++ b/etc/cmake/options.cmake @@ -173,6 +173,7 @@ ot_option(OT_BACKBONE_ROUTER OPENTHREAD_CONFIG_BACKBONE_ROUTER_ENABLE "backbone ot_option(OT_BACKBONE_ROUTER_DUA_NDPROXYING OPENTHREAD_CONFIG_BACKBONE_ROUTER_DUA_NDPROXYING_ENABLE "BBR DUA ND Proxy") ot_option(OT_BACKBONE_ROUTER_MULTICAST_ROUTING OPENTHREAD_CONFIG_BACKBONE_ROUTER_MULTICAST_ROUTING_ENABLE "BBR MR") ot_option(OT_BLE_TCAT OPENTHREAD_CONFIG_BLE_TCAT_ENABLE "Ble based thread commissioning") +ot_option(OT_BORDER_ADMITTER OPENTHREAD_CONFIG_BORDER_AGENT_ADMITTER_ENABLE "border agent admitter") ot_option(OT_BORDER_AGENT OPENTHREAD_CONFIG_BORDER_AGENT_ENABLE "border agent") ot_option(OT_BORDER_AGENT_EPSKC OPENTHREAD_CONFIG_BORDER_AGENT_EPHEMERAL_KEY_ENABLE "border agent ephemeral PSKc") ot_option(OT_BORDER_AGENT_ID OPENTHREAD_CONFIG_BORDER_AGENT_ID_ENABLE "create and save border agent ID") diff --git a/examples/config/ot-core-config-check-size-br.h b/examples/config/ot-core-config-check-size-br.h index 0d821186d..81536face 100644 --- a/examples/config/ot-core-config-check-size-br.h +++ b/examples/config/ot-core-config-check-size-br.h @@ -38,6 +38,7 @@ #define OPENTHREAD_CONFIG_ASSERT_ENABLE 1 #define OPENTHREAD_CONFIG_BACKBONE_ROUTER_ENABLE 1 +#define OPENTHREAD_CONFIG_BORDER_AGENT_ADMITTER_ENABLE 1 #define OPENTHREAD_CONFIG_BORDER_AGENT_ENABLE 1 #define OPENTHREAD_CONFIG_BORDER_AGENT_EPHEMERAL_KEY_ENABLE 1 #define OPENTHREAD_CONFIG_BORDER_AGENT_ID_ENABLE 1 diff --git a/include/openthread/BUILD.gn b/include/openthread/BUILD.gn index bba5c3573..3c6d3dc20 100644 --- a/include/openthread/BUILD.gn +++ b/include/openthread/BUILD.gn @@ -43,6 +43,7 @@ source_set("openthread") { "backbone_router_ftd.h", "ble_secure.h", "border_agent.h", + "border_agent_admitter.h", "border_agent_ephemeral_key.h", "border_agent_tracker.h", "border_agent_txt_data.h", diff --git a/include/openthread/border_agent_admitter.h b/include/openthread/border_agent_admitter.h new file mode 100644 index 000000000..2927f0389 --- /dev/null +++ b/include/openthread/border_agent_admitter.h @@ -0,0 +1,237 @@ +/* + * Copyright (c) 2025, The OpenThread Authors. + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * 3. Neither the name of the copyright holder nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ + +/** + * @file + * @brief + * This file includes functions for the Thread Border Agent Admitter. + */ + +#ifndef OPENTHREAD_BORDER_AGENT_ADMITTER_H_ +#define OPENTHREAD_BORDER_AGENT_ADMITTER_H_ + +#include +#include + +#include +#include +#include +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * @addtogroup api-border-agent + * + * @brief + * This module includes functions for the Thread Border Agent Admitter role. + * + * All APIs in this module require both `OPENTHREAD_CONFIG_BORDER_AGENT_ENABLE` and + * `OPENTHREAD_CONFIG_BORDER_AGENT_ADMITTER_ENABLE` features to be enabled. + * + * @{ + */ + +/** + * Represents an iterator for Border Admitter enroller. + * + * The caller MUST NOT access or update the fields in this struct. It is intended for OpenThread internal use only. + */ +typedef struct otBorderAdmitterIterator +{ + void *mPtr1; + void *mPtr2; + uint64_t mData1; + uint32_t mData2; +} otBorderAdmitterIterator; + +/** + * Represents information about an enroller. + * + * To ensure consistent `mRegisterDuration` calculations, the iterator's initialization time is stored within the + * iterator, and each enroller `mRegisterDuration` is calculated relative to this time. + */ +typedef struct otBorderAdmitterEnrollerInfo +{ + otBorderAgentSessionInfo mSessionInfo; ///< The session information. + const char *mId; ///< The enroller ID string. + otSteeringData mSteeringData; ///< The steering data. + uint8_t mMode; ///< The enroller's mode. + uint64_t mRegisterDuration; ///< Milliseconds since the enroller registered. +} otBorderAdmitterEnrollerInfo; + +/** + * Represents information about a joiner accepted by an enroller. + * + * To ensure consistent duration calculations, the iterator's initialization time is stored within the iterator, and + * the `mMsecSinceAccept` is calculated relative to this time. + */ +typedef struct otBorderAdmitterJoinerInfo +{ + otIp6InterfaceIdentifier mIid; ///< Joiner IID. + uint64_t mMsecSinceAccept; ///< Milliseconds since the joiner was accepted by the enroller. + uint32_t mMsecTillExpiration; ///< Milliseconds till the joiner will be expired and removed. +} otBorderAdmitterJoinerInfo; + +/** + * Enables or disables the Border Agent Admitter. + * + * The default enable/disable state of Border Admitter (after OpenThread stack initialization) is determined by the + * OpenThread config `OPENTHREAD_CONFIG_BORDER_AGENT_ADMITTER_ENABLED_BY_DEFAULT`. + * + * @param[in] aInstance The OpenThread instance. + * @param[in] aEnabled A boolean to indicate whether to enable (TRUE) or disable (FALSE) the Border Agent Admitter. + */ +void otBorderAdmitterSetEnabled(otInstance *aInstance, bool aEnable); + +/** + * Indicates whether the Border Agent Admitter is enabled. + * + * @param[in] aInstance The OpenThread instance. + * + * @retval TRUE The Border Agent Admitter is enabled. + * @retval FALSE The Border Agent Admitter is disabled. + */ +bool otBorderAdmitterIsEnabled(otInstance *aInstance); + +/** + * Indicates whether the device is currently the Prime Admitter. + * + * The Prime Admitter is the device that wins the election among all Admitters within the Thread mesh network. The + * election algorithm ensures convergence on a single Prime Admitter within the mesh. + * + * @param[in] aInstance The OpenThread instance. + * + * @retval TRUE This device is the Prime Admitter. + * @retval FALSE This device is not the Prime Admitter. + */ +bool otBorderAdmitterIsPrimeAdmitter(otInstance *aInstance); + +/** + * Indicates whether the Prime Admitter is currently the active commissioner. + * + * After becoming the Prime Admitter and having at least one enroller register, the Admitter petitions the Leader to + * be granted the commissioner role. + * + * @param[in] aInstance The OpenThread instance. + * + * @retval TRUE This device is the active commissioner. + * @retval FALSE This device is not the active commissioner. + */ +bool otBorderAdmitterIsActiveCommissioner(otInstance *aInstance); + +/** + * Indicates whether the Prime Admitter's petition to become the native mesh commissioner was rejected. + * + * A rejection typically occurs if there is already another active commissioner in the Thread network. + * + * The Admitter will automatically retry petitioning. It monitors the Thread Network Data to see when the other + * commissioner is removed and retry its own petition. + * + * @param[in] aInstance The OpenThread instance. + * + * @retval TRUE The petition was rejected. + * @retval FALSE The petition was not rejected. + */ +bool otBorderAdmitterIsPetitionRejected(otInstance *aInstance); + +/** + * Gets the Joiner UDP port. + * + * A zero value indicates the Joiner UDP port is not specified/fixed by the Admitter (Joiner Routers can pick). + * + * @param[in] aInstance The OpenThread instance. + * + * @returns The joiner UDP port number. + */ +uint16_t otBorderAdmitterGetJoinerUdpPort(otInstance *aInstance); + +/** + * Sets the joiner UDP port. + * + * A zero value indicates the Joiner UDP port is not specified/fixed by the Admitter (Joiner Routers can pick). + * + * @param[in] aInstance The OpenThread instance. + * @param[in] aUdpPort The joiner UDP port number. + */ +void otBorderAdmitterSetJoinerUdpPort(otInstance *aInstance, uint16_t aUdpPort); + +/** + * Initializes an `otBorderAdmitterIterator`. + * + * An iterator MUST be initialized before it is used. + * + * An iterator can be initialized again to restart from the beginning of the list. + * + * When iterating over enrollers, the initialization time is recorded and used to calculate a consistent + * `mRegisterDuration` for each enroller. + * + * @param[in] aInstance A pointer to an OpenThread instance. + * @param[in] aIterator A pointer to the iterator to initialize. + * + */ +void otBorderAdmitterInitIterator(otInstance *aInstance, otBorderAdmitterIterator *aIterator); + +/** + * Retrieves the information about the next Enroller registered with the Admitter. + * + * @param[in] aIterator The iterator to use. + * @param[out] aEnrollerInfo A pointer to an `otBorderAdmitterEnrollerInfo` to populate. + * + * @retval OT_ERROR_NONE Successfully retrieved the next enroller info. + * @retval OT_ERROR_NOT_FOUND No more enrollers are available. The end of the list has been reached. + */ +otError otBorderAdmitterGetNextEnrollerInfo(otBorderAdmitterIterator *aIterator, + otBorderAdmitterEnrollerInfo *aEnrollerInfo); + +/** + * Retrieves the information about the next accepted joiner by the latest retrieved enroller during iteration. + * + * Iterates over all joiners which are accepted by the latest enroller, i.e., the last enroller which was retrieved + * using the @p aIterator along with `otBorderAdmitterGetNextEnrollerInfo()`. + * + * @param[in] aIterator The iterator to use. + * @param[out] aJoinerInfo A pointer to an `otBorderAdmitterJoinerInfo` to populate. + * + * @retval OT_ERROR_NONE Successfully retrieved the next joiner info. + * @retval OT_ERROR_NOT_FOUND No more joiners are available. The end of the list has been reached. + */ +otError otBorderAdmitterGetNextJoinerInfo(otBorderAdmitterIterator *aIterator, otBorderAdmitterJoinerInfo *aJoinerInfo); + +/** + * @} + */ + +#ifdef __cplusplus +} // end of extern "C" +#endif + +#endif // OPENTHREAD_BORDER_AGENT_ADMITTER_H_ diff --git a/include/openthread/border_agent_txt_data.h b/include/openthread/border_agent_txt_data.h index 8738132e6..a68ef465c 100644 --- a/include/openthread/border_agent_txt_data.h +++ b/include/openthread/border_agent_txt_data.h @@ -119,14 +119,15 @@ typedef enum otBorderAgentMultiAilState */ typedef struct otBorderAgentStateBitmap { - otBorderAgentConnMode mConnMode; ///< Connection Mode. - otBorderAgentThreadIfState mThreadIfState; ///< Thread Interface Status. - otBorderAgentAvailability mAvailability; ///< Availability - otBorderAgentThreadRole mThreadRole; ///< Thread Role. - bool mBbrIsActive; ///< Backbone Router function is active. - bool mBbrIsPrimary; ///< Device is the Primary Backbone Router. - bool mEpskcSupported; ///< ePSKc Mode is supported. - otBorderAgentMultiAilState mMultiAilState; ///< Multi-AIL detection state. + otBorderAgentConnMode mConnMode; ///< Connection Mode. + otBorderAgentThreadIfState mThreadIfState; ///< Thread Interface Status. + otBorderAgentAvailability mAvailability; ///< Availability + otBorderAgentThreadRole mThreadRole; ///< Thread Role. + bool mBbrIsActive; ///< Backbone Router function is active. + bool mBbrIsPrimary; ///< Device is the Primary Backbone Router. + bool mEpskcSupported; ///< ePSKc Mode is supported. + otBorderAgentMultiAilState mMultiAilState; ///< Multi-AIL detection state. + bool mAdmitterSupported; ///< Border Admitter function is supported. } otBorderAgentStateBitmap; /** diff --git a/include/openthread/dataset.h b/include/openthread/dataset.h index 8bb7e574b..20c462ed8 100644 --- a/include/openthread/dataset.h +++ b/include/openthread/dataset.h @@ -304,6 +304,9 @@ typedef enum otMeshcopTlvType OT_MESHCOP_TLV_ENERGY_LIST = 57, ///< meshcop Energy List TLV OT_MESHCOP_TLV_THREAD_DOMAIN_NAME = 59, ///< meshcop Thread Domain Name TLV OT_MESHCOP_TLV_WAKEUP_CHANNEL = 74, ///< meshcop Wake-up Channel TLV + OT_MESHCOP_TLV_ADMITTER_STATE = 90, ///< meshcop Admitter State TLV + OT_MESHCOP_TLV_ENROLLER_ID = 91, ///< meshcop Enroller ID TLV + OT_MESHCOP_TLV_ENROLLER_MODE = 92, ///< meshcop Enroller Mode TLV OT_MESHCOP_TLV_DISCOVERYREQUEST = 128, ///< meshcop Discovery Request TLV OT_MESHCOP_TLV_DISCOVERYRESPONSE = 129, ///< meshcop Discovery Response TLV OT_MESHCOP_TLV_JOINERADVERTISEMENT = 241, ///< meshcop Joiner Advertisement TLV (experimental) diff --git a/include/openthread/instance.h b/include/openthread/instance.h index 287813296..83adf768b 100644 --- a/include/openthread/instance.h +++ b/include/openthread/instance.h @@ -52,7 +52,7 @@ extern "C" { * * @note This number versions both OpenThread platform and user APIs. */ -#define OPENTHREAD_API_VERSION (577) +#define OPENTHREAD_API_VERSION (578) /** * @addtogroup api-instance diff --git a/src/core/BUILD.gn b/src/core/BUILD.gn index 0c3cef1b1..625277715 100644 --- a/src/core/BUILD.gn +++ b/src/core/BUILD.gn @@ -324,6 +324,7 @@ openthread_core_files = [ "api/backbone_router_api.cpp", "api/backbone_router_ftd_api.cpp", "api/ble_secure_api.cpp", + "api/border_agent_admitter_api.cpp", "api/border_agent_api.cpp", "api/border_agent_ephemeral_key_api.cpp", "api/border_agent_tracker_api.cpp", @@ -551,6 +552,8 @@ openthread_core_files = [ "meshcop/announce_begin_client.hpp", "meshcop/border_agent.cpp", "meshcop/border_agent.hpp", + "meshcop/border_agent_admitter.cpp", + "meshcop/border_agent_admitter.hpp", "meshcop/border_agent_ephemeral_key.cpp", "meshcop/border_agent_ephemeral_key.hpp", "meshcop/border_agent_tracker.cpp", @@ -874,6 +877,7 @@ source_set("libopenthread_core_config") { "config/announce_sender.h", "config/backbone_router.h", "config/border_agent.h", + "config/border_agent_admitter.h", "config/border_router.h", "config/border_routing.h", "config/channel_manager.h", diff --git a/src/core/CMakeLists.txt b/src/core/CMakeLists.txt index 6f49a728d..f9c6b1a3e 100644 --- a/src/core/CMakeLists.txt +++ b/src/core/CMakeLists.txt @@ -34,6 +34,7 @@ set(COMMON_SOURCES api/backbone_router_api.cpp api/backbone_router_ftd_api.cpp api/ble_secure_api.cpp + api/border_agent_admitter_api.cpp api/border_agent_api.cpp api/border_agent_ephemeral_key_api.cpp api/border_agent_tracker_api.cpp @@ -164,6 +165,7 @@ set(COMMON_SOURCES mac/wakeup_tx_scheduler.cpp meshcop/announce_begin_client.cpp meshcop/border_agent.cpp + meshcop/border_agent_admitter.cpp meshcop/border_agent_ephemeral_key.cpp meshcop/border_agent_tracker.cpp meshcop/border_agent_txt_data.cpp diff --git a/src/core/api/border_agent_admitter_api.cpp b/src/core/api/border_agent_admitter_api.cpp new file mode 100644 index 000000000..df15691f3 --- /dev/null +++ b/src/core/api/border_agent_admitter_api.cpp @@ -0,0 +1,99 @@ +/* + * Copyright (c) 2025, The OpenThread Authors. + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * 3. Neither the name of the copyright holder nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ + +/** + * @file + * This file implements the OpenThread Border Agent Admitter API. + */ + +#include "openthread-core-config.h" + +#if OPENTHREAD_CONFIG_BORDER_AGENT_ENABLE && OPENTHREAD_CONFIG_BORDER_AGENT_ADMITTER_ENABLE + +#include + +#include "instance/instance.hpp" + +using namespace ot; + +void otBorderAdmitterSetEnabled(otInstance *aInstance, bool aEnable) +{ + AsCoreType(aInstance).Get().SetEnabled(aEnable); +} + +bool otBorderAdmitterIsEnabled(otInstance *aInstance) +{ + return AsCoreType(aInstance).Get().IsEnabled(); +} + +bool otBorderAdmitterIsPrimeAdmitter(otInstance *aInstance) +{ + return AsCoreType(aInstance).Get().IsPrimeAdmitter(); +} + +bool otBorderAdmitterIsActiveCommissioner(otInstance *aInstance) +{ + return AsCoreType(aInstance).Get().IsActiveCommissioner(); +} + +bool otBorderAdmitterIsPetitionRejected(otInstance *aInstance) +{ + return AsCoreType(aInstance).Get().IsPetitionRejected(); +} + +uint16_t otBorderAdmitterGetJoinerUdpPort(otInstance *aInstance) +{ + return AsCoreType(aInstance).Get().GetJoinerUdpPort(); +} + +void otBorderAdmitterSetJoinerUdpPort(otInstance *aInstance, uint16_t aUdpPort) +{ + AsCoreType(aInstance).Get().SetJoinerUdpPort(aUdpPort); +} + +void otBorderAdmitterInitIterator(otInstance *aInstance, otBorderAdmitterIterator *aIterator) +{ + AsCoreType(aIterator).Init(AsCoreType(aInstance)); +} + +otError otBorderAdmitterGetNextEnrollerInfo(otBorderAdmitterIterator *aIterator, + otBorderAdmitterEnrollerInfo *aEnrollerInfo) +{ + AssertPointerIsNotNull(aEnrollerInfo); + + return AsCoreType(aIterator).GetNextEnrollerInfo(*aEnrollerInfo); +} + +otError otBorderAdmitterGetNextJoinerInfo(otBorderAdmitterIterator *aIterator, otBorderAdmitterJoinerInfo *aJoinerInfo) +{ + AssertPointerIsNotNull(aJoinerInfo); + + return AsCoreType(aIterator).GetNextJoinerInfo(*aJoinerInfo); +} + +#endif // OPENTHREAD_CONFIG_BORDER_AGENT_ENABLE && OPENTHREAD_CONFIG_BORDER_AGENT_ADMITTER_ENABLE diff --git a/src/core/common/notifier.cpp b/src/core/common/notifier.cpp index e1ea00f6f..682199ee6 100644 --- a/src/core/common/notifier.cpp +++ b/src/core/common/notifier.cpp @@ -134,6 +134,9 @@ void Notifier::EmitEvents(void) Get().HandleNotifierEvents(events); Get().HandleNotifierEvents(events); #endif +#if OPENTHREAD_CONFIG_BORDER_AGENT_ENABLE && OPENTHREAD_CONFIG_BORDER_AGENT_ADMITTER_ENABLE + Get().HandleNotifierEvents(events); +#endif #if OPENTHREAD_CONFIG_MLR_ENABLE || (OPENTHREAD_FTD && OPENTHREAD_CONFIG_TMF_PROXY_MLR_ENABLE) Get().HandleNotifierEvents(events); #endif diff --git a/src/core/config/border_agent_admitter.h b/src/core/config/border_agent_admitter.h new file mode 100644 index 000000000..361b32e52 --- /dev/null +++ b/src/core/config/border_agent_admitter.h @@ -0,0 +1,84 @@ +/* + * Copyright (c) 2025, The OpenThread Authors. + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * 3. Neither the name of the copyright holder nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ + +/** + * @file + * This file includes compile-time configurations for the Border Agent Admitter. + */ + +#ifndef OT_CORE_CONFIG_BORDER_AGENT_ADMITTER_H_ +#define OT_CORE_CONFIG_BORDER_AGENT_ADMITTER_H_ + +/** + * @addtogroup config-border-agent + * + * @brief + * This module includes configuration variables for the Border Agent Admitter. + * + * @{ + */ + +/** + * @def OPENTHREAD_CONFIG_BORDER_AGENT_ADMITTER_ENABLE + * + * Define to 1 to enable Border Agent's Admitter feature. + */ +#ifndef OPENTHREAD_CONFIG_BORDER_AGENT_ADMITTER_ENABLE +#define OPENTHREAD_CONFIG_BORDER_AGENT_ADMITTER_ENABLE 0 +#endif + +/** + * @def OPENTHREAD_CONFIG_BORDER_AGENT_ADMITTER_ENABLED_BY_DEFAULT + * + * Define to 1 to enable the Border Admitter by default upon OpenThread stack initialization. + * + * Applicable when `OPENTHREAD_CONFIG_BORDER_AGENT_ADMITTER_ENABLE` is enabled. + */ +#ifndef OPENTHREAD_CONFIG_BORDER_AGENT_ADMITTER_ENABLED_BY_DEFAULT +#define OPENTHREAD_CONFIG_BORDER_AGENT_ADMITTER_ENABLED_BY_DEFAULT 0 +#endif + +/** + * @def OPENTHREAD_CONFIG_BORDER_AGENT_ADMITTER_DEFAULT_JOINER_UDP_PORT + * + * Specifies the default Joiner UDP port used by the Border Admitter. + * + * A value of zero indicates that the Joiner UDP port is not specified/fixed by the Admitter, allowing Joiner Routers + * to pick. + * + * Applicable when `OPENTHREAD_CONFIG_BORDER_AGENT_ADMITTER_ENABLE` is enabled. + */ +#ifndef OPENTHREAD_CONFIG_BORDER_AGENT_ADMITTER_DEFAULT_JOINER_UDP_PORT +#define OPENTHREAD_CONFIG_BORDER_AGENT_ADMITTER_DEFAULT_JOINER_UDP_PORT 0 +#endif + +/** + * @} + */ + +#endif // OT_CORE_CONFIG_BORDER_AGENT_ADMITTER_H_ diff --git a/src/core/instance/instance.cpp b/src/core/instance/instance.cpp index 3f4ce2668..7fdcb64e7 100644 --- a/src/core/instance/instance.cpp +++ b/src/core/instance/instance.cpp @@ -175,6 +175,9 @@ Instance::Instance(void) , mBorderAgentTxtData(*this) , mBorderAgentManager(*this) #endif +#if OPENTHREAD_CONFIG_BORDER_AGENT_ENABLE && OPENTHREAD_CONFIG_BORDER_AGENT_ADMITTER_ENABLE + , mBorderAgentAdmitter(*this) +#endif #if OPENTHREAD_CONFIG_BORDER_AGENT_ENABLE && OPENTHREAD_CONFIG_BORDER_AGENT_EPHEMERAL_KEY_ENABLE , mBorderAgentEphemeralKeyManager(*this) #endif diff --git a/src/core/instance/instance.hpp b/src/core/instance/instance.hpp index 285bda172..143d330af 100644 --- a/src/core/instance/instance.hpp +++ b/src/core/instance/instance.hpp @@ -89,6 +89,7 @@ #include "mac/mac.hpp" #include "mac/wakeup_tx_scheduler.hpp" #include "meshcop/border_agent.hpp" +#include "meshcop/border_agent_admitter.hpp" #include "meshcop/border_agent_ephemeral_key.hpp" #include "meshcop/border_agent_tracker.hpp" #include "meshcop/border_agent_txt_data.hpp" @@ -602,6 +603,10 @@ private: MeshCoP::BorderAgent::Manager mBorderAgentManager; #endif +#if OPENTHREAD_CONFIG_BORDER_AGENT_ENABLE && OPENTHREAD_CONFIG_BORDER_AGENT_ADMITTER_ENABLE + MeshCoP::BorderAgent::Admitter mBorderAgentAdmitter; +#endif + #if OPENTHREAD_CONFIG_BORDER_AGENT_ENABLE && OPENTHREAD_CONFIG_BORDER_AGENT_EPHEMERAL_KEY_ENABLE MeshCoP::BorderAgent::EphemeralKeyManager mBorderAgentEphemeralKeyManager; #endif @@ -1073,6 +1078,10 @@ template <> inline MeshCoP::BorderAgent::Manager &Instance::Get(void) { return m template <> inline MeshCoP::BorderAgent::TxtData &Instance::Get(void) { return mBorderAgentTxtData; } #endif +#if OPENTHREAD_CONFIG_BORDER_AGENT_ENABLE && OPENTHREAD_CONFIG_BORDER_AGENT_ADMITTER_ENABLE +template <> inline MeshCoP::BorderAgent::Admitter &Instance::Get(void) { return mBorderAgentAdmitter; } +#endif + #if OPENTHREAD_CONFIG_BORDER_AGENT_ENABLE && OPENTHREAD_CONFIG_BORDER_AGENT_EPHEMERAL_KEY_ENABLE template <> inline MeshCoP::BorderAgent::EphemeralKeyManager &Instance::Get(void) { diff --git a/src/core/meshcop/border_agent.cpp b/src/core/meshcop/border_agent.cpp index 47b3e448c..943fb5f03 100644 --- a/src/core/meshcop/border_agent.cpp +++ b/src/core/meshcop/border_agent.cpp @@ -48,8 +48,15 @@ RegisterLogModule("BorderAgent"); // `Manager` #if OPENTHREAD_CONFIG_BORDER_AGENT_MESHCOP_SERVICE_ENABLE + const char Manager::kServiceType[] = "_meshcop._udp"; const char Manager::kDefaultBaseServiceName[] = OPENTHREAD_CONFIG_BORDER_AGENT_MESHCOP_SERVICE_BASE_NAME; + +#if OPENTHREAD_CONFIG_BORDER_AGENT_ADMITTER_ENABLE +const char Manager::kAdmitterSubType[] = "_admitter"; +const char *const Manager::kServiceSubTypes[] = {kAdmitterSubType}; +#endif + #endif Manager::Manager(Instance &aInstance) @@ -171,6 +178,10 @@ void Manager::Start(void) Get().Refresh(); +#if OPENTHREAD_CONFIG_BORDER_AGENT_ADMITTER_ENABLE + Get().EvaluateOperation(); +#endif + exit: if (!mIsRunning) { @@ -196,6 +207,10 @@ void Manager::Stop(void) Get().Refresh(); +#if OPENTHREAD_CONFIG_BORDER_AGENT_ADMITTER_ENABLE + Get().EvaluateOperation(); +#endif + exit: return; } @@ -364,6 +379,10 @@ template <> void Manager::HandleTmf(Coap::Msg &aMsg) LogInfo("Received %s from %s", UriToString(), aMsg.mMessageInfo.GetPeerAddr().ToString().AsCString()); +#if OPENTHREAD_CONFIG_BORDER_AGENT_ADMITTER_ENABLE + Get().ForwardJoinerRelayToEnrollers(aMsg); +#endif + VerifyOrExit(mCommissionerSession != nullptr); mCommissionerSession->ForwardUdpRelayToCommissioner(aMsg.mMessage); @@ -446,6 +465,14 @@ void Manager::RegisterService(void) service.mTxtData = txtDataBuffer; service.mTxtDataLength = txtDataLength; +#if OPENTHREAD_CONFIG_BORDER_AGENT_ADMITTER_ENABLE + if (Get().IsPrimeAdmitter()) + { + service.mSubTypeLabels = kServiceSubTypes; + service.mSubTypeLabelsLength = GetArrayLength(kServiceSubTypes); + } +#endif + Get().RegisterService(service, /* aRequestId */ 0, /* aCallback */ nullptr); Heap::Free(txtDataBuffer); @@ -520,12 +547,7 @@ Error Manager::SessionIterator::GetNextSessionInfo(SessionInfo &aSessionInfo) VerifyOrExit(session != nullptr, error = kErrorNotFound); SetSession(static_cast(session->GetNext())); - - aSessionInfo.mPeerSockAddr.mAddress = session->GetMessageInfo().GetPeerAddr(); - aSessionInfo.mPeerSockAddr.mPort = session->GetMessageInfo().GetPeerPort(); - aSessionInfo.mIsConnected = session->IsConnected(); - aSessionInfo.mIsCommissioner = session->IsActiveCommissioner(); - aSessionInfo.mLifetime = GetInitTime() - session->GetAllocationTime(); + session->CopyInfoTo(aSessionInfo, GetInitTime()); exit: return error; @@ -573,6 +595,10 @@ void Manager::CoapDtlsSession::Cleanup(void) Get().RevokeRoleIfActiveCommissioner(*this); +#if OPENTHREAD_CONFIG_BORDER_AGENT_ADMITTER_ENABLE + ResignEnroller(); +#endif + Coap::SecureSession::Cleanup(); } @@ -607,6 +633,15 @@ bool Manager::CoapDtlsSession::HandleResource(const char *aUriPath, Coap::Msg &a case kUriProxyTx: HandleTmfProxyTx(aMsg); break; +#if OPENTHREAD_CONFIG_BORDER_AGENT_ADMITTER_ENABLE + case kUriEnrollerRegister: + case kUriEnrollerKeepAlive: + case kUriEnrollerJoinerAccept: + case kUriEnrollerJoinerRelease: + HandleEnrollerTmf(uri, aMsg); + break; +#endif + default: didHandle = false; break; @@ -631,6 +666,9 @@ void Manager::CoapDtlsSession::HandleConnected(ConnectEvent aEvent) else { LogInfo("Session %u disconnected", mIndex); +#if OPENTHREAD_CONFIG_BORDER_AGENT_ADMITTER_ENABLE + ResignEnroller(); +#endif Get().HandleSessionDisconnected(*this, aEvent); } } @@ -817,6 +855,17 @@ exit: void Manager::CoapDtlsSession::ForwardUdpProxyToCommissioner(const Message &aMessage, const Ip6::MessageInfo &aMessageInfo) +{ + Error error; + + SuccessOrExit(error = ForwardUdpProxy(aMessage, aMessageInfo)); + Log(kForward); + +exit: + LogWarnOnError(error, "forward UDP proxy"); +} + +Error Manager::CoapDtlsSession::ForwardUdpProxy(const Message &aMessage, const Ip6::MessageInfo &aMessageInfo) { Error error = kErrorNone; OwnedPtr message; @@ -843,15 +892,24 @@ void Manager::CoapDtlsSession::ForwardUdpProxyToCommissioner(const Message SuccessOrExit(error = Tlv::Append(*message, aMessageInfo.GetPeerAddr())); - SuccessOrExit(error = SendMessage(message.PassOwnership())); - - Log(kForward); + error = SendMessage(message.PassOwnership()); exit: - LogWarnOnError(error, "forward UDP proxy"); + return error; } void Manager::CoapDtlsSession::ForwardUdpRelayToCommissioner(const Message &aMessage) +{ + Error error; + + SuccessOrExit(error = ForwardUdpRelay(aMessage)); + Log(kForward); + +exit: + LogWarnOnError(error, "forward UDP relay"); +} + +Error Manager::CoapDtlsSession::ForwardUdpRelay(const Message &aMessage) { OwnedPtr forwardMessage; Error error = kErrorNone; @@ -859,12 +917,10 @@ void Manager::CoapDtlsSession::ForwardUdpRelayToCommissioner(const Message &aMes forwardMessage.Reset(NewPriorityNonConfirmablePostMessage(kUriRelayRx)); VerifyOrExit(forwardMessage != nullptr, error = kErrorNoBufs); - SuccessOrExit(error = ForwardToCommissioner(forwardMessage.PassOwnership(), aMessage)); - - Log(kForward); + error = ForwardToCommissioner(forwardMessage.PassOwnership(), aMessage); exit: - LogWarnOnError(error, "forward UDP relay"); + return error; } Error Manager::CoapDtlsSession::ForwardToCommissioner(OwnedPtr aForwardMessage, const Message &aMessage) @@ -911,7 +967,18 @@ void Manager::CoapDtlsSession::HandleTmfProxyTx(Coap::Msg &aMsg) Log(kReceive); - VerifyOrExit(IsActiveCommissioner(), error = kErrorInvalidState); +#if OPENTHREAD_CONFIG_BORDER_AGENT_ADMITTER_ENABLE + if (IsEnroller()) + { + VerifyOrExit(Get().IsActiveCommissioner(), error = kErrorInvalidState); + messageInfo.SetSockAddr(Get().mCommissionerPetitioner.GetAloc()); + } + else +#endif + { + VerifyOrExit(IsActiveCommissioner(), error = kErrorInvalidState); + messageInfo.SetSockAddr(Get().GetCommissionerAloc()); + } SuccessOrExit(error = Tlv::FindTlvValueOffsetRange(aMsg.mMessage, Tlv::kUdpEncapsulation, offsetRange)); @@ -926,7 +993,6 @@ void Manager::CoapDtlsSession::HandleTmfProxyTx(Coap::Msg &aMsg) SuccessOrExit(error = message->AppendBytesFromMessage(aMsg.mMessage, offsetRange)); messageInfo.SetSockPort(udpEncapHeader.GetSourcePort()); - messageInfo.SetSockAddr(Get().GetCommissionerAloc()); messageInfo.SetPeerPort(udpEncapHeader.GetDestinationPort()); SuccessOrExit(error = Tlv::Find(aMsg.mMessage, messageInfo.GetPeerAddr())); @@ -951,7 +1017,16 @@ void Manager::CoapDtlsSession::HandleTmfRelayTx(Coap::Msg &aMsg) VerifyOrExit(aMsg.IsNonConfirmablePostRequest()); - VerifyOrExit(IsActiveCommissioner(), error = kErrorInvalidState); +#if OPENTHREAD_CONFIG_BORDER_AGENT_ADMITTER_ENABLE + if (IsEnroller()) + { + VerifyOrExit(Get().IsActiveCommissioner(), error = kErrorInvalidState); + } + else +#endif + { + VerifyOrExit(IsActiveCommissioner(), error = kErrorInvalidState); + } Log(kReceive); @@ -1052,11 +1127,23 @@ void Manager::CoapDtlsSession::HandleTimer(void) { if (IsConnected()) { +#if OPENTHREAD_CONFIG_BORDER_AGENT_ADMITTER_ENABLE + ResignEnroller(); +#endif LogInfo("Session %u timed out - disconnecting", mIndex); DisconnectTimeout(); } } +void Manager::CoapDtlsSession::CopyInfoTo(SessionInfo &aInfo, UptimeMsec aUptimeNow) const +{ + aInfo.mPeerSockAddr.mAddress = GetMessageInfo().GetPeerAddr(); + aInfo.mPeerSockAddr.mPort = GetMessageInfo().GetPeerPort(); + aInfo.mIsConnected = IsConnected(); + aInfo.mIsCommissioner = IsActiveCommissioner(); + aInfo.mLifetime = aUptimeNow - GetAllocationTime(); +} + #if OT_SHOULD_LOG_AT(OT_LOG_LEVEL_INFO) void Manager::CoapDtlsSession::LogUri(Action aAction, const char *aUriString, const char *aTxt) diff --git a/src/core/meshcop/border_agent.hpp b/src/core/meshcop/border_agent.hpp index ed23967e1..6f4c651eb 100644 --- a/src/core/meshcop/border_agent.hpp +++ b/src/core/meshcop/border_agent.hpp @@ -53,6 +53,7 @@ #include "common/owned_ptr.hpp" #include "common/tasklet.hpp" #include "common/uptime.hpp" +#include "meshcop/border_agent_admitter.hpp" #include "meshcop/border_agent_txt_data.hpp" #include "meshcop/dataset.hpp" #include "meshcop/secure_transport.hpp" @@ -109,6 +110,9 @@ class Manager : public InstanceLocator, private NonCopyable #endif #if OPENTHREAD_CONFIG_BORDER_AGENT_EPHEMERAL_KEY_ENABLE friend class EphemeralKeyManager; +#endif +#if OPENTHREAD_CONFIG_BORDER_AGENT_ADMITTER_ENABLE + friend class Admitter; #endif friend class ot::Notifier; friend class Tmf::Agent; @@ -270,6 +274,9 @@ private: class CoapDtlsSession : public Coap::SecureSession, public Heap::Allocatable { friend Heap::Allocatable; +#if OPENTHREAD_CONFIG_BORDER_AGENT_ADMITTER_ENABLE + friend class Admitter; +#endif public: Error SendMessage(OwnedPtr aMessage); @@ -279,6 +286,7 @@ private: bool IsActiveCommissioner(void) const; uint64_t GetAllocationTime(void) const { return mAllocationTime; } uint16_t GetIndex(void) const { return mIndex; } + void CopyInfoTo(SessionInfo &aInfo, UptimeMsec aUptimeNow) const; private: enum Action : uint8_t @@ -303,6 +311,8 @@ private: CoapDtlsSession(Instance &aInstance, Dtls::Transport &aDtlsTransport); Error ForwardToCommissioner(OwnedPtr aForwardMessage, const Message &aMessage); + Error ForwardUdpRelay(const Message &aMessage); + Error ForwardUdpProxy(const Message &aMessage, const Ip6::MessageInfo &aMessageInfo); void HandleTmfCommissionerKeepAlive(Coap::Msg &aMsg); void HandleTmfRelayTx(Coap::Msg &aMsg); void HandleTmfProxyTx(Coap::Msg &aMsg); @@ -321,6 +331,23 @@ private: static void HandleTimer(Timer &aTimer); void HandleTimer(void); +#if OPENTHREAD_CONFIG_BORDER_AGENT_ADMITTER_ENABLE + bool IsEnroller(void) const { return mEnroller != nullptr; } + void ResignEnroller(void); + void HandleEnrollerTmf(Uri aUri, const Coap::Msg &aMsg); + Error ProcessEnrollerRegister(const Coap::Message &aMessage); + Error ProcessEnrollerKeepAlive(const Coap::Message &aMessage); + Error ProcessEnrollerJoinerAccept(const Coap::Message &aMessage); + Error ProcessEnrollerJoinerRelease(const Coap::Message &aMessage); + void SendEnrollerResponse(Uri aUri, StateTlv::State aResponseState, const Coap::Message &aRequest); + void SendEnrollerReportState(uint8_t aAdmitterState); + Error AppendAdmitterTlvs(Coap::Message &aMessage, uint8_t aAdmitterState); + void ForwardUdpRelayToEnroller(const Coap::Message &aMessage); + void ForwardUdpProxyToEnroller(const Message &aMessage, const Ip6::MessageInfo &aMessageInfo); + + static Error ReadSteeringDataTlv(const Message &aMessage, SteeringData &aSteeringData); +#endif + #if OT_SHOULD_LOG_AT(OT_LOG_LEVEL_INFO) void LogUri(Action aAction, const char *aUriString, const char *aTxt); @@ -331,6 +358,9 @@ private: template void Log(Action, const char *) {} #endif +#if OPENTHREAD_CONFIG_BORDER_AGENT_ADMITTER_ENABLE + OwnedPtr mEnroller; +#endif LinkedList mForwardContexts; TimerMilliContext mTimer; UptimeMsec mAllocationTime; @@ -372,6 +402,11 @@ private: // Callback from `Dnssd` void HandleDnssdPlatformStateChange(void) { RegisterService(); } +#if OPENTHREAD_CONFIG_BORDER_AGENT_ADMITTER_ENABLE + // Callback from `Admitter` + void HandlePrimeAdmitterStateChanged(void) { RegisterService(); } +#endif + const char *GetServiceName(void); bool IsServiceNameEmpty(void) const { return mServiceName[0] == kNullChar; } void ConstrcutServiceName(const char *aBaseName, Dns::Name::LabelBuffer &aNameBuffer); @@ -382,6 +417,10 @@ private: #if OPENTHREAD_CONFIG_BORDER_AGENT_MESHCOP_SERVICE_ENABLE static const char kServiceType[]; static const char kDefaultBaseServiceName[]; +#if OPENTHREAD_CONFIG_BORDER_AGENT_ADMITTER_ENABLE + static const char kAdmitterSubType[]; + static const char *const kServiceSubTypes[]; +#endif #endif bool mEnabled; diff --git a/src/core/meshcop/border_agent_admitter.cpp b/src/core/meshcop/border_agent_admitter.cpp new file mode 100644 index 000000000..f23eab5ba --- /dev/null +++ b/src/core/meshcop/border_agent_admitter.cpp @@ -0,0 +1,1464 @@ +/* + * Copyright (c) 2025, The OpenThread Authors. + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * 3. Neither the name of the copyright holder nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ + +/** + * @file + * This file implements the Border Agent Admitter. + */ + +#include "border_agent_admitter.hpp" + +#if OPENTHREAD_CONFIG_BORDER_AGENT_ENABLE && OPENTHREAD_CONFIG_BORDER_AGENT_ADMITTER_ENABLE + +#include "instance/instance.hpp" + +namespace ot { +namespace MeshCoP { +namespace BorderAgent { + +RegisterLogModule("BorderAdmitter"); + +//--------------------------------------------------------------------------------------------------------------------- +// Admitter + +const uint8_t Admitter::kEnrollerValidSteeringDataLengths[] = {1, 8, 16}; + +Admitter::Admitter(Instance &aInstance) + : InstanceLocator(aInstance) + , mEnabled(kEnabledByDefault) + , mHasAnyEnroller(false) + , mArbitrator(aInstance) + , mCommissionerPetitioner(aInstance) + , mJoinerTimer(aInstance) + , mReportStateTask(aInstance) + , mLastSyncedState(kStateUnavailable) +{ +} + +void Admitter::SetEnabled(bool aEnable) +{ + VerifyOrExit(mEnabled != aEnable); + mEnabled = aEnable; + EvaluateOperation(); + + // Signal a Border Agent TXT data refresh so that the `StateBitmap` + // flag indicating `Admitter` function support is updated. + + Get().Refresh(); + +exit: + return; +} + +void Admitter::EvaluateOperation(void) +{ + // This method is called whenever there is a change or an event that + // impacts the operation of the `Admitter` or its sub-components. + // It evaluates the current operational conditions and orchestrates + // the starting or stopping of the sub-components accordingly. + + if (mEnabled && Get().IsRunning()) + { + mArbitrator.Start(); + + if (mArbitrator.IsPrimeAdmitter() && mHasAnyEnroller) + { + mCommissionerPetitioner.Start(); + } + else + { + mCommissionerPetitioner.Stop(); + } + } + else + { + mCommissionerPetitioner.Stop(); + mArbitrator.Stop(); + } + + PostReportStateTask(); +} + +Admitter::State Admitter::DetermineState(void) const +{ + // The `Admitter::State` is determined from the operational state of + // its sub-components (`Arbitrator` and `CommissionerPetitioner`). + // It is not directly tracked by the `Admitter` itself. + + State state = kStateUnavailable; + + VerifyOrExit(mArbitrator.IsPrimeAdmitter()); + + switch (mCommissionerPetitioner.GetState()) + { + case CommissionerPetitioner::kStopped: + case CommissionerPetitioner::kToPetition: + case CommissionerPetitioner::kPetitioning: + state = kStateReady; + break; + case CommissionerPetitioner::kAcceptedToSyncData: + case CommissionerPetitioner::kAcceptedSyncingData: + case CommissionerPetitioner::kAcceptedDataSynced: + state = kStateActive; + break; + case CommissionerPetitioner::kRejected: + state = kStateConflictError; + break; + } + +exit: + return state; +} + +void Admitter::PostReportStateTask(void) +{ + // Posts a task to signal the `Admitter` state change to all registered + // Enrollers. We track and check against the last synced state to avoid + // reporting if the state has not changed. + + State state = DetermineState(); + + VerifyOrExit(state != mLastSyncedState); + mReportStateTask.Post(); + +exit: + return; +} + +void Admitter::HandleReportStateTask(void) +{ + State state = DetermineState(); + + for (EnrollerIterator iter(GetInstance()); !iter.IsDone(); iter.Advance()) + { + Manager::CoapDtlsSession &coapSession = *iter.GetSessionAs(); + + coapSession.SendEnrollerReportState(state); + + if (state == kStateUnavailable) + { + coapSession.ResignEnroller(); + } + } + + mLastSyncedState = state; +} + +void Admitter::HandleEnrollerChange(void) +{ + // Handles any changes to Enroller status or properties, + // such as registration of a new Enroller, removal of an + // existing one, or modification of registered properties + // (e.g., steering data). + + EnrollerIterator iter(GetInstance()); + bool hasAnyEnroller = !iter.IsDone(); + + if (hasAnyEnroller != mHasAnyEnroller) + { + mHasAnyEnroller = hasAnyEnroller; + EvaluateOperation(); + } + + mCommissionerPetitioner.HandleEnrollerChange(); +} + +void Admitter::DetermineSteeringData(SteeringData &aSteeringData) const +{ + uint8_t maxLength = 1; + + for (EnrollerIterator iter(GetInstance()); !iter.IsDone(); iter.Advance()) + { + if (iter.GetEnroller()->mSteeringData.PermitsAllJoiners()) + { + aSteeringData.SetToPermitAllJoiners(); + ExitNow(); + } + + maxLength = Max(maxLength, iter.GetEnroller()->mSteeringData.GetLength()); + } + + IgnoreError(aSteeringData.Init(maxLength)); + + for (EnrollerIterator iter(GetInstance()); !iter.IsDone(); iter.Advance()) + { + SuccessOrAssert(aSteeringData.MergeBloomFilterWith(iter.GetEnroller()->mSteeringData)); + } + +exit: + return; +} + +void Admitter::ForwardJoinerRelayToEnrollers(const Coap::Msg &aMsg) +{ + Ip6::InterfaceIdentifier joinerIid; + + VerifyOrExit(mCommissionerPetitioner.IsActiveCommissioner()); + + VerifyOrExit(aMsg.IsNonConfirmablePostRequest()); + SuccessOrExit(Tlv::Find(aMsg.mMessage, joinerIid)); + + LogInfo("Processing %s from joiner %s", UriToString(), joinerIid.ToString().AsCString()); + + // Check for a specific `Enroller` that accepted this Joiner IID. + // If found, forward to that specific `Enroller`, otherwise, send + // to all. + + for (EnrollerIterator iter(GetInstance()); !iter.IsDone(); iter.Advance()) + { + Joiner *joiner = iter.GetEnroller()->mJoiners.FindMatching(joinerIid); + + if (joiner != nullptr) + { + joiner->UpdateExpirationTime(); + iter.GetSessionAs()->ForwardUdpRelayToEnroller(aMsg.mMessage); + ExitNow(); + } + } + + for (EnrollerIterator iter(GetInstance()); !iter.IsDone(); iter.Advance()) + { + iter.GetSessionAs()->ForwardUdpRelayToEnroller(aMsg.mMessage); + } + +exit: + return; +} + +void Admitter::ForwardUdpProxyToEnrollers(const Message &aMessage, const Ip6::MessageInfo &aMessageInfo) +{ + VerifyOrExit(mCommissionerPetitioner.IsActiveCommissioner()); + + for (EnrollerIterator iter(GetInstance()); !iter.IsDone(); iter.Advance()) + { + iter.GetSessionAs()->ForwardUdpProxyToEnroller(aMessage, aMessageInfo); + } + +exit: + return; +} + +void Admitter::HandleJoinerTimer(void) +{ + NextFireTime nextTime; + OwningList removedJoiners; + + for (EnrollerIterator iter(GetInstance()); !iter.IsDone(); iter.Advance()) + { + iter.GetEnroller()->mJoiners.RemoveAllMatching(removedJoiners, ExpirationChecker(nextTime.GetNow())); + +#if OT_SHOULD_LOG_AT(OT_LOG_LEVEL_INFO) + for (const Joiner &joiner : removedJoiners) + { + LogInfo("Removing timed-out joiner %s - previously accepted by enroller - session %u", + joiner.mIid.ToString().AsCString(), iter.GetSessionIndex()); + } +#endif + + removedJoiners.Free(); + + for (const Joiner &joiner : iter.GetEnroller()->mJoiners) + { + nextTime.UpdateIfEarlier(joiner.mExpirationTime); + } + } + + mJoinerTimer.FireAt(nextTime); +} + +void Admitter::HandleNotifierEvents(Events aEvents) +{ + if (aEvents.Contains(kEventThreadNetdataChanged)) + { + mCommissionerPetitioner.HandleNetDataChange(); + } +} + +const char *Admitter::EnrollerUriToString(Uri aUri) +{ + const char *uriString = "Unknown"; + + switch (aUri) + { + case kUriEnrollerRegister: + uriString = UriToString(); + break; + case kUriEnrollerKeepAlive: + uriString = UriToString(); + break; + case kUriEnrollerJoinerAccept: + uriString = UriToString(); + break; + case kUriEnrollerJoinerRelease: + uriString = UriToString(); + break; + default: + break; + } + + return uriString; +} + +//--------------------------------------------------------------------------------------------------------------------- +// Admitter::Arbitrator + +Admitter::Arbitrator::Arbitrator(Instance &aInstance) + : InstanceLocator(aInstance) + , mState(kStopped) + , mTimer(aInstance) +{ +} + +void Admitter::Arbitrator::SetState(State aState) +{ +#if OPENTHREAD_CONFIG_BORDER_AGENT_MESHCOP_SERVICE_ENABLE + bool shouldUpdateService = (aState == kPrime); +#endif + + VerifyOrExit(mState != aState); + LogInfo("Arbitrator state: %s -> %s", StateToString(mState), StateToString(aState)); + mState = aState; + +#if OPENTHREAD_CONFIG_BORDER_AGENT_MESHCOP_SERVICE_ENABLE + // Signal to update the registered MeshCoP service when the prime + // admitter role changes, i.e. when we transition to or from the + // `kPrime` state. + + shouldUpdateService |= (mState == kPrime); + + if (shouldUpdateService) + { + Get().HandlePrimeAdmitterStateChanged(); + } +#endif + +exit: + return; +} + +void Admitter::Arbitrator::Start(void) +{ + VerifyOrExit(mState == kStopped); + + SetState(kClaiming); + + Get().PublishBorderAdmitterService(); + + if (Get().IsBorderAdmitterServicePublished()) + { + HandlePublisherEvent(NetworkData::Publisher::kEventEntryAdded); + } + +exit: + return; +} + +void Admitter::Arbitrator::Stop(void) +{ + VerifyOrExit(mState != kStopped); + + SetState(kStopped); + mTimer.Stop(); + Get().UnpublishBorderAdmitterService(); + +exit: + return; +} + +void Admitter::Arbitrator::HandlePublisherEvent(NetworkData::Publisher::Event aEvent) +{ + switch (aEvent) + { + case NetworkData::Publisher::kEventEntryAdded: + VerifyOrExit(mState == kClaiming); + SetState(kCandidate); + mTimer.Start(kDelayToBecomePrime); + break; + + case NetworkData::Publisher::kEventEntryRemoved: + switch (mState) + { + case kStopped: + case kClaiming: + break; + + case kCandidate: + mTimer.Stop(); + SetState(kClaiming); + break; + + case kPrime: + SetState(kClaiming); + Get().EvaluateOperation(); + break; + } + + break; + } + +exit: + return; +} + +void Admitter::Arbitrator::HandleTimer(void) +{ + VerifyOrExit(mState == kCandidate); + + SetState(kPrime); + Get().EvaluateOperation(); + +exit: + return; +} + +const char *Admitter::Arbitrator::StateToString(State aState) +{ +#define ArbitratorStateMapList(_) \ + _(kStopped, "Stopped") \ + _(kClaiming, "Claiming") \ + _(kCandidate, "Candidate") \ + _(kPrime, "Prime") + + DefineEnumStringArray(ArbitratorStateMapList); + + return kStrings[aState]; +} + +//--------------------------------------------------------------------------------------------------------------------- +// Admitter::CommissionerPetitioner + +Admitter::CommissionerPetitioner::CommissionerPetitioner(Instance &aInstance) + : InstanceLocator(aInstance) + , mState(kStopped) + , mJoinerUdpPort(kDefaultJoinerUdpPort) + , mSessionId(0) + , mRetryTimer(aInstance) + , mKeepAliveTimer(aInstance) + , mUdpReceiver(HandleUdpReceive, this) +{ + mSteeringData.Clear(); + mAloc.InitAsThreadOriginMeshLocal(); +} + +void Admitter::CommissionerPetitioner::SetJoinerUdpPort(uint16_t aUdpPort) +{ + VerifyOrExit(mJoinerUdpPort != aUdpPort); + + LogInfo("Petitioner JoinerUdpPort: %u -> %u", mJoinerUdpPort, aUdpPort); + mJoinerUdpPort = aUdpPort; + + VerifyOrExit(IsActiveCommissioner()); + + SetState(kAcceptedToSyncData); + ScheduleImmediateDataSync(); + +exit: + return; +} + +void Admitter::CommissionerPetitioner::Start(void) +{ + VerifyOrExit(mState == kStopped); + + SetState(kToPetition); + SendPetitionIfNoOtherCommissioner(); + +exit: + return; +} + +void Admitter::CommissionerPetitioner::Stop(void) +{ + mRetryTimer.Stop(); + mKeepAliveTimer.Stop(); + + switch (mState) + { + case kStopped: + case kToPetition: + case kPetitioning: + case kRejected: + break; + + case kAcceptedSyncingData: + case kAcceptedToSyncData: + case kAcceptedDataSynced: + RemoveAlocAndUdpReceiver(); + IgnoreError(SendKeepAlive(StateTlv::kReject)); + break; + } + + SetState(kStopped); + + IgnoreError(Get().AbortTransaction(HandleDataSetResponse, this)); +} + +bool Admitter::CommissionerPetitioner::IsActiveCommissioner(void) const +{ + bool isActive = false; + + switch (mState) + { + case kStopped: + case kToPetition: + case kPetitioning: + case kRejected: + break; + case kAcceptedToSyncData: + case kAcceptedSyncingData: + case kAcceptedDataSynced: + isActive = true; + break; + } + + return isActive; +} + +void Admitter::CommissionerPetitioner::SetState(State aState) +{ + VerifyOrExit(mState != aState); + LogInfo("Petitioner state: %s -> %s", StateToString(mState), StateToString(aState)); + mState = aState; + + Get().PostReportStateTask(); + +exit: + return; +} + +void Admitter::CommissionerPetitioner::HandleNetDataChange(void) +{ + VerifyOrExit(mState == kRejected); + SendPetitionIfNoOtherCommissioner(); + +exit: + return; +} + +void Admitter::CommissionerPetitioner::SendPetitionIfNoOtherCommissioner(void) +{ + Error error = kErrorNone; + OwnedPtr message; + CommissionerIdTlv::StringType commissionerId; + StringWriter writer(commissionerId, sizeof(commissionerId)); + + OT_ASSERT(mState == kToPetition || mState == kRejected); + + if (Get().FindInCommissioningData() != nullptr) + { + SetState(kRejected); + ExitNow(); + } + + message.Reset(Get().NewPriorityConfirmablePostMessage(kUriLeaderPetition)); + VerifyOrExit(message != nullptr, error = kErrorNoBufs); + + writer.Append("otAdmitter"); + writer.AppendHexBytes(Get().GetExtAddress().m8, sizeof(Mac::ExtAddress)); + SuccessOrExit(error = Tlv::Append(*message, commissionerId)); + + IgnoreError(Get().AbortTransaction(HandlePetitionResponse, this)); + + SuccessOrExit(error = SendToLeader(message.PassOwnership(), HandlePetitionResponse)); + + LogInfo("Send %s", UriToString()); + SetState(kPetitioning); + +exit: + if (error != kErrorNone) + { + SchedulePetitionRetry(); + } +} + +void Admitter::CommissionerPetitioner::HandlePetitionResponse(Coap::Msg *aMsg, Error aResult) +{ + Error error = ProcessPetitionResponse(aMsg, aResult); + + if (error != kErrorNone) + { + VerifyOrExit(mState == kPetitioning); + + SchedulePetitionRetry(); + SetState((error == kErrorRejected) ? kRejected : kToPetition); + ExitNow(); + } + + // The state can change while awaiting a petition response from + // the leader. We intentionally do not abort the TMF transaction, + // ensuring we can process the response. If the petition is + // accepted, we always update the session ID and ALOC. However, + // if the state is `kStopped`, we send a keep-alive with + // `kReject` to resign the active commissioner role. + + switch (mState) + { + case kToPetition: + case kPetitioning: + case kRejected: + case kAcceptedToSyncData: + case kAcceptedSyncingData: + case kAcceptedDataSynced: + ScheduleNextKeepAlive(); + AddAlocAndUdpReceiver(); + SetState(kAcceptedToSyncData); + Get().DetermineSteeringData(mSteeringData); + SendDataSet(); + break; + + case kStopped: + IgnoreError(SendKeepAlive(StateTlv::kReject)); + break; + } + +exit: + return; +} + +Error Admitter::CommissionerPetitioner::ProcessPetitionResponse(const Coap::Msg *aMsg, Error aResult) +{ + // Processes a petition response from the leader, returning + // `kErrorNone` if the petition is valid and accepted, + // `kErrorRejected` if it is rejected, and other error codes if + // the response is invalid or cannot be parsed. + + Error error = aResult; + uint8_t state; + + LogInfo("Receive %s response, error: %s", UriToString(), ErrorToString(error)); + + SuccessOrExit(error); + VerifyOrExit(aMsg != nullptr, error = kErrorInvalidArgs); + + VerifyOrExit(aMsg->GetCode() == Coap::kCodeChanged, error = kErrorParse); + + SuccessOrExit(error = Tlv::Find(aMsg->mMessage, state)); + + VerifyOrExit(state == StateTlv::kAccept, error = kErrorRejected); + + error = Tlv::Find(aMsg->mMessage, mSessionId); + +exit: + return error; +} + +void Admitter::CommissionerPetitioner::SchedulePetitionRetry(void) +{ + mRetryTimer.Start(Random::NonCrypto::AddJitter(kPetitionRetryDelay, kPetitionRetryJitter)); +} + +void Admitter::CommissionerPetitioner::AddAlocAndUdpReceiver(void) +{ + Ip6::Address alocAddr; + + Get().GetCommissionerAloc(mSessionId, alocAddr); + + if (Get().HasUnicastAddress(mAloc)) + { + VerifyOrExit(mAloc.GetAddress() != alocAddr); + RemoveAlocAndUdpReceiver(); + } + + mAloc.SetAddress(alocAddr); + + LogInfo("Adding ALOC %s", alocAddr.ToString().AsCString()); + + Get().AddUnicastAddress(mAloc); + IgnoreError(Get().AddReceiver(mUdpReceiver)); + +exit: + return; +} + +void Admitter::CommissionerPetitioner::RemoveAlocAndUdpReceiver(void) +{ + LogInfo("Removing ALOC %s", mAloc.GetAddress().ToString().AsCString()); + IgnoreError(Get().RemoveReceiver(mUdpReceiver)); + Get().RemoveUnicastAddress(mAloc); +} + +Error Admitter::CommissionerPetitioner::SendKeepAlive(StateTlv::State aState) +{ + Error error = kErrorNone; + OwnedPtr message; + + message.Reset(Get().NewPriorityConfirmablePostMessage(kUriLeaderKeepAlive)); + VerifyOrExit(message != nullptr, error = kErrorNoBufs); + + SuccessOrExit(error = Tlv::Append(*message, aState)); + SuccessOrExit(error = Tlv::Append(*message, mSessionId)); + + switch (aState) + { + case StateTlv::kAccept: + SuccessOrExit(error = SendToLeader(message.PassOwnership(), HandleKeepAliveResponse)); + LogInfo("Send %s", UriToString()); + break; + case StateTlv::kReject: + default: + SuccessOrExit(error = SendToLeader(message.PassOwnership(), nullptr)); + LogInfo("Send %s with reject status - resigning the commissioner role", UriToString()); + break; + } + +exit: + return error; +} + +void Admitter::CommissionerPetitioner::HandleKeepAliveResponse(Coap::Msg *aMsg, Error aResult) +{ + Error error = aResult; + uint8_t state; + + LogInfo("Receive %s response, error: %s", UriToString(), ErrorToString(error)); + + VerifyOrExit(IsActiveCommissioner()); + + if (error == kErrorNone) + { + error = (aMsg != nullptr) ? Tlv::Find(aMsg->mMessage, state) : kErrorInvalidArgs; + } + + if (error != kErrorNone) + { + ScheduleKeepAliveRetry(); + ExitNow(); + } + + if (state != StateTlv::kAccept) + { + LogInfo("%s response contains reject status", UriToString()); + + mKeepAliveTimer.Stop(); + RemoveAlocAndUdpReceiver(); + + SetState(kToPetition); + SchedulePetitionRetry(); + ExitNow(); + } + + ScheduleNextKeepAlive(); + +exit: + return; +} + +void Admitter::CommissionerPetitioner::ScheduleNextKeepAlive(void) +{ + mKeepAliveTimer.Start(Random::NonCrypto::AddJitter(kKeepAliveTxInterval, kKeepAliveTxJitter)); +} + +void Admitter::CommissionerPetitioner::ScheduleKeepAliveRetry(void) +{ + mKeepAliveTimer.Start(Random::NonCrypto::AddJitter(kKeepAliveRetryDelay, kKeepAliveRetryJitter)); +} + +void Admitter::CommissionerPetitioner::HandleKeepAliveTimer(void) +{ + VerifyOrExit(IsActiveCommissioner()); + + switch (SendKeepAlive(StateTlv::kAccept)) + { + case kErrorNone: + ScheduleNextKeepAlive(); + break; + default: + ScheduleKeepAliveRetry(); + break; + } + +exit: + return; +} + +void Admitter::CommissionerPetitioner::SendDataSet(void) +{ + Error error = kErrorNone; + OwnedPtr message; + + OT_ASSERT(mState == kAcceptedToSyncData); + + mRetryTimer.Stop(); + + IgnoreError(Get().AbortTransaction(HandleDataSetResponse, this)); + + message.Reset(Get().NewPriorityConfirmablePostMessage(kUriCommissionerSet)); + VerifyOrExit(message != nullptr, error = kErrorNoBufs); + + SuccessOrExit(error = Tlv::Append(*message, mSessionId)); + SuccessOrExit(error = Tlv::Append(*message, mSteeringData.GetData(), mSteeringData.GetLength())); + + if (mJoinerUdpPort != 0) + { + SuccessOrExit(error = Tlv::Append(*message, mJoinerUdpPort)); + } + + SuccessOrExit(error = SendToLeader(message.PassOwnership(), HandleDataSetResponse)); + + LogInfo("Send %s", UriToString()); + SetState(kAcceptedSyncingData); + +exit: + if (error != kErrorNone) + { + ScheduleDataSyncRetry(); + } +} + +void Admitter::CommissionerPetitioner::HandleDataSetResponse(Coap::Msg *aMsg, Error aResult) +{ + VerifyOrExit(mState == kAcceptedSyncingData); + + if (ProcessDataSetResponse(aMsg, aResult) == kErrorNone) + { + SetState(kAcceptedDataSynced); + } + else + { + SetState(kAcceptedToSyncData); + ScheduleDataSyncRetry(); + } + +exit: + return; +} + +Error Admitter::CommissionerPetitioner::ProcessDataSetResponse(const Coap::Msg *aMsg, Error aResult) +{ + Error error = aResult; + uint8_t state; + + SuccessOrExit(error); + VerifyOrExit(aMsg != nullptr, error = kErrorInvalidArgs); + + VerifyOrExit(aMsg->GetCode() == Coap::kCodeChanged, error = kErrorParse); + + SuccessOrExit(error = Tlv::Find(aMsg->mMessage, state)); + + VerifyOrExit(state == StateTlv::kAccept, error = kErrorRejected); + +exit: + LogInfo("Receive %s response, error: %s", UriToString(), ErrorToString(error)); + return error; +} + +void Admitter::CommissionerPetitioner::ScheduleDataSyncRetry(void) +{ + mRetryTimer.Start(Random::NonCrypto::AddJitter(kDataSyncRetryDelay, kDataSyncRetryJitter)); +} + +void Admitter::CommissionerPetitioner::ScheduleImmediateDataSync(void) { mRetryTimer.Start(0); } + +void Admitter::CommissionerPetitioner::HandleEnrollerChange(void) +{ + SteeringData newSteeringData; + + VerifyOrExit(IsActiveCommissioner()); + + Get().DetermineSteeringData(newSteeringData); + + VerifyOrExit(newSteeringData != mSteeringData); + mSteeringData = newSteeringData; + + SetState(kAcceptedToSyncData); + ScheduleImmediateDataSync(); + +exit: + return; +} + +Error Admitter::CommissionerPetitioner::SendToLeader(OwnedPtr aMessage, Coap::ResponseHandler aHandler) +{ + Error error; + Tmf::MessageInfo messageInfo(GetInstance()); + + messageInfo.SetSockAddrToRlocPeerAddrToLeaderAloc(); + + // On success the message ownership is transferred. + + SuccessOrExit(error = Get().SendMessage(*aMessage, messageInfo, aHandler, + (aHandler != nullptr) ? this : nullptr)); + aMessage.Release(); + +exit: + return error; +} + +void Admitter::CommissionerPetitioner::HandleRetryTimer(void) +{ + switch (mState) + { + case kToPetition: + case kRejected: + SendPetitionIfNoOtherCommissioner(); + break; + case kAcceptedToSyncData: + SendDataSet(); + break; + + case kPetitioning: + case kStopped: + case kAcceptedSyncingData: + case kAcceptedDataSynced: + break; + } +} + +bool Admitter::CommissionerPetitioner::HandleUdpReceive(void *aContext, + const otMessage *aMessage, + const otMessageInfo *aMessageInfo) +{ + return static_cast(aContext)->HandleUdpReceive(AsCoreType(aMessage), + AsCoreType(aMessageInfo)); +} + +bool Admitter::CommissionerPetitioner::HandleUdpReceive(const Message &aMessage, const Ip6::MessageInfo &aMessageInfo) +{ + bool didHandle = false; + + VerifyOrExit(IsActiveCommissioner()); + VerifyOrExit(aMessageInfo.GetSockAddr() == mAloc.GetAddress()); + + Get().ForwardUdpProxyToEnrollers(aMessage, aMessageInfo); + didHandle = true; + +exit: + return didHandle; +} + +const char *Admitter::CommissionerPetitioner::StateToString(State aState) +{ +#define CommissionerPetitionerStateMapList(_) \ + _(kStopped, "Stopped") \ + _(kToPetition, "ToPetition") \ + _(kPetitioning, "Petitioning") \ + _(kAcceptedToSyncData, "AcceptedToSyncData") \ + _(kAcceptedSyncingData, "AcceptedSyncingData") \ + _(kAcceptedDataSynced, "AcceptedDataSynced") \ + _(kRejected, "Rejected") + + DefineEnumStringArray(CommissionerPetitionerStateMapList); + + return kStrings[aState]; +} + +//--------------------------------------------------------------------------------------------------------------------- +// Admitter::Joiner + +Admitter::Joiner::Joiner(Instance &aInstance, const Ip6::InterfaceIdentifier &aIid) + : InstanceLocator(aInstance) + , mNext(nullptr) + , mIid(aIid) +{ + mAcceptUptime = Get().GetUptime(); + UpdateExpirationTime(); +} + +void Admitter::Joiner::UpdateExpirationTime(void) +{ + mExpirationTime = TimerMilli::GetNow() + kTimeout; + + Get().mJoinerTimer.FireAtIfEarlier(mExpirationTime); +} + +bool Admitter::Joiner::Matches(const Ip6::InterfaceIdentifier &aIid) const +{ + // An unspecified `aIid` (all zero) acts as a wildcard. + + return aIid.IsUnspecified() || (aIid == mIid); +} + +bool Admitter::Joiner::Matches(const ExpirationChecker &aChecker) const { return aChecker.IsExpired(mExpirationTime); } + +//--------------------------------------------------------------------------------------------------------------------- +// Admitter::Iterator + +void Admitter::Iterator::Init(Instance &aInstance) +{ + SetSession(aInstance.Get().mDtlsTransport.GetSessions().GetHead()); + SetJoiner(nullptr); + SetInitUptime(aInstance.Get().GetUptime()); + SetInitTime(TimerMilli::GetNow()); +} + +void Admitter::Iterator::SkipToNextEnrollerSession(SecureSession *&aSession) +{ + // Skip over sessions in the list that are not enrollers, starting + // from the given `aSession` itself. Upon return, the `aSession` + // pointer is updated to the next session which is an enroller, + // or it is `nullptr` if none is found. + + for (; aSession != nullptr; aSession = aSession->GetNext()) + { + if (static_cast(aSession)->IsEnroller()) + { + break; + } + } +} + +Error Admitter::Iterator::GetNextEnrollerInfo(EnrollerInfo &aEnrollerInfo) +{ + Error error = kErrorNone; + SecureSession *session; + Manager::CoapDtlsSession *coapSession; + Enroller *enroller; + + session = GetSession(); + + SkipToNextEnrollerSession(session); + + VerifyOrExit(session != nullptr, error = kErrorNotFound); + + coapSession = static_cast(session); + enroller = coapSession->mEnroller.Get(); + + coapSession->CopyInfoTo(aEnrollerInfo.mSessionInfo, GetInitUptime()); + + aEnrollerInfo.mId = enroller->mId; + aEnrollerInfo.mSteeringData = enroller->mSteeringData; + aEnrollerInfo.mMode = enroller->mMode; + aEnrollerInfo.mRegisterDuration = GetInitUptime() - enroller->mRegisterUptime; + + SetJoiner(enroller->mJoiners.GetHead()); + SetSession(session->GetNext()); + +exit: + return error; +} + +Error Admitter::Iterator::GetNextJoinerInfo(JoinerInfo &aJoinerInfo) +{ + Error error = kErrorNone; + Joiner *joiner = GetJoiner(); + + VerifyOrExit(joiner != nullptr, error = kErrorNotFound); + + aJoinerInfo.mIid = joiner->mIid; + aJoinerInfo.mMsecSinceAccept = GetInitUptime() - joiner->mAcceptUptime; + aJoinerInfo.mMsecTillExpiration = Max(joiner->mExpirationTime, GetInitTime()) - GetInitTime(); + + SetJoiner(joiner->GetNext()); + +exit: + return error; +} + +//--------------------------------------------------------------------------------------------------------------------- +// Admitter::EnrollerIterator + +Admitter::EnrollerIterator::EnrollerIterator(Instance &aInstance) + : mSession(aInstance.Get().mDtlsTransport.GetSessions().GetHead()) +{ + FindNextEnroller(); +} + +void Admitter::EnrollerIterator::Advance(void) +{ + VerifyOrExit(mSession != nullptr); + + mSession = mSession->GetNext(); + FindNextEnroller(); + +exit: + return; +} + +Admitter::Enroller *Admitter::EnrollerIterator::GetEnroller(void) const +{ + Enroller *enroller = nullptr; + + VerifyOrExit(mSession != nullptr); + enroller = GetSessionAs()->mEnroller.Get(); + +exit: + return enroller; +} + +uint16_t Admitter::EnrollerIterator::GetSessionIndex(void) const +{ + return (mSession == nullptr) ? 0 : GetSessionAs()->mIndex; +} + +//--------------------------------------------------------------------------------------------------------------------- +// Manager::CoapDtlSession (enroller/admitter specific methods) + +void Manager::CoapDtlsSession::ResignEnroller(void) +{ + VerifyOrExit(mEnroller != nullptr); + + LogInfo("Resigning enroller - session %u", mIndex); + mEnroller.Reset(nullptr); + Get().HandleEnrollerChange(); + +exit: + return; +} + +void Manager::CoapDtlsSession::HandleEnrollerTmf(Uri aUri, const Coap::Msg &aMsg) +{ + Error error = kErrorNone; + StateTlv::State responseState; + + VerifyOrExit(aMsg.IsConfirmablePostRequest()); + + LogInfo("Receive %s", Admitter::EnrollerUriToString(aUri)); + + switch (aUri) + { + case kUriEnrollerRegister: + error = ProcessEnrollerRegister(aMsg.mMessage); + break; + case kUriEnrollerKeepAlive: + error = ProcessEnrollerKeepAlive(aMsg.mMessage); + break; + case kUriEnrollerJoinerAccept: + error = ProcessEnrollerJoinerAccept(aMsg.mMessage); + break; + case kUriEnrollerJoinerRelease: + error = ProcessEnrollerJoinerRelease(aMsg.mMessage); + break; + default: + ExitNow(); + } + + responseState = (error == kErrorNone) ? StateTlv::kAccept : StateTlv::kReject; + + SendEnrollerResponse(aUri, responseState, aMsg.mMessage); + +exit: + return; +} + +Error Manager::CoapDtlsSession::ProcessEnrollerRegister(const Coap::Message &aRequest) +{ + Error error = kErrorNone; + + VerifyOrExit(Get().IsPrimeAdmitter(), error = kErrorInvalidState); + +#if OPENTHREAD_CONFIG_BORDER_AGENT_EPHEMERAL_KEY_ENABLE + VerifyOrExit(!Get().OwnsSession(*this), error = kErrorNotCapable); +#endif + + mEnroller.Reset(Admitter::Enroller::Allocate()); + VerifyOrExit(mEnroller != nullptr, error = kErrorNoBufs); + + SuccessOrExit(error = Tlv::Find(aRequest, mEnroller->mId)); + SuccessOrExit(error = Tlv::Find(aRequest, mEnroller->mMode)); + SuccessOrExit(error = ReadSteeringDataTlv(aRequest, mEnroller->mSteeringData)); + mEnroller->mRegisterUptime = Get().GetUptime(); + + LogInfo("Registered enroller - session %u", mIndex); + LogInfo(" id: %s", mEnroller->mId); + LogInfo(" mode: 0x%02x", mEnroller->mMode); + LogInfo(" steeringData: %s", mEnroller->mSteeringData.ToString().AsCString()); + + mTimer.Start(Admitter::kEnrollerKeepAliveTimeout); + Get().HandleEnrollerChange(); + +exit: + if (error != kErrorNone) + { + LogWarn("Failed processing %s - session %u, error:%s", UriToString(), mIndex, + ErrorToString(error)); + ResignEnroller(); + } + + return error; +} + +Error Manager::CoapDtlsSession::ReadSteeringDataTlv(const Message &aMessage, SteeringData &aSteeringData) +{ + Error error; + OffsetRange offsetRange; + + SuccessOrExit(error = Tlv::FindTlvValueOffsetRange(aMessage, Tlv::kSteeringData, offsetRange)); + + // Ensure the read steering data has a valid length. A length of + // one byte is only allowed to indicate `PermitsAllJoiners()`. + + error = kErrorInvalidArgs; + + for (uint8_t validLength : Admitter::kEnrollerValidSteeringDataLengths) + { + if (offsetRange.GetLength() == validLength) + { + error = kErrorNone; + break; + } + } + + SuccessOrExit(error); + + IgnoreError(aSteeringData.Init(static_cast(offsetRange.GetLength()))); + aMessage.ReadBytes(offsetRange, aSteeringData.GetData()); + + if (aSteeringData.GetLength() == 1) + { + VerifyOrExit(aSteeringData.PermitsAllJoiners() || aSteeringData.IsEmpty(), error = kErrorInvalidArgs); + } + +exit: + return error; +} + +Error Manager::CoapDtlsSession::ProcessEnrollerKeepAlive(const Coap::Message &aRequest) +{ + Error error = kErrorNone; + uint8_t state; + uint8_t mode; + SteeringData steeringData; + + VerifyOrExit(IsEnroller(), error = kErrorInvalidState); + + SuccessOrExit(error = Tlv::Find(aRequest, state)); + + if (state != StateTlv::kAccept) + { + ResignEnroller(); + error = kErrorRejected; + ExitNow(); + } + + // EnrollerKeepAlive can optionally include Enroller Mode TLV + // or Steering Data TLV. + + error = Tlv::Find(aRequest, mode); + + switch (error) + { + case kErrorNone: + VerifyOrExit(mEnroller->mMode != mode); + LogInfo("Enroller mode changed: 0x%02x -> 0x%02x, session %u", mEnroller->mMode, mode, mIndex); + mEnroller->mMode = mode; + break; + case kErrorNotFound: + error = kErrorNone; + break; + default: + ExitNow(); + } + + error = ReadSteeringDataTlv(aRequest, steeringData); + + switch (error) + { + case kErrorNone: + VerifyOrExit(mEnroller->mSteeringData != steeringData); + mEnroller->mSteeringData = steeringData; + LogInfo("Enroller steering data changed - session %u", mIndex); + LogInfo(" steeringData: %s", mEnroller->mSteeringData.ToString().AsCString()); + Get().HandleEnrollerChange(); + break; + case kErrorNotFound: + error = kErrorNone; + break; + default: + ExitNow(); + } + + LogInfo("Extending enroller timeout - session %u", mIndex); + mTimer.Start(Admitter::kEnrollerKeepAliveTimeout); + +exit: + if (error != kErrorNone) + { + ResignEnroller(); + } + + return error; +} + +Error Manager::CoapDtlsSession::ProcessEnrollerJoinerAccept(const Coap::Message &aRequest) +{ + Error error = kErrorNone; + Ip6::InterfaceIdentifier joinerIid; + Admitter::Joiner *joiner; + + VerifyOrExit(IsEnroller(), error = kErrorInvalidState); + + SuccessOrExit(error = Tlv::Find(aRequest, joinerIid)); + + VerifyOrExit(!joinerIid.IsUnspecified(), error = kErrorInvalidArgs); + + joiner = mEnroller->mJoiners.FindMatching(joinerIid); + + if (joiner != nullptr) + { + joiner->UpdateExpirationTime(); + LogInfo("Enroller re-accepted joiner %s - session %u", joinerIid.ToString().AsCString(), mIndex); + ExitNow(); + } + + for (Admitter::EnrollerIterator iter(GetInstance()); !iter.IsDone(); iter.Advance()) + { + if (iter.GetEnroller()->mJoiners.ContainsMatching(joinerIid)) + { + LogInfo("Joiner %s is already accepted by another Enroller %u, rejecting request - session %u", + joinerIid.ToString().AsCString(), iter.GetSessionIndex(), mIndex); + ExitNow(error = kErrorRejected); + } + } + + joiner = Admitter::Joiner::Allocate(GetInstance(), joinerIid); + VerifyOrExit(joiner != nullptr, error = kErrorNoBufs); + + mEnroller->mJoiners.Push(*joiner); + LogInfo("Enroller accepted joiner %s - session %u", joinerIid.ToString().AsCString(), mIndex); + +exit: + return error; +} + +Error Manager::CoapDtlsSession::ProcessEnrollerJoinerRelease(const Coap::Message &aRequest) +{ + Error error = kErrorNone; + Ip6::InterfaceIdentifier joinerIid; + + VerifyOrExit(IsEnroller(), error = kErrorInvalidState); + + SuccessOrExit(error = Tlv::Find(aRequest, joinerIid)); + + VerifyOrExit(mEnroller->mJoiners.RemoveAndFreeAllMatching(joinerIid)); + + if (joinerIid.IsUnspecified()) + { + LogInfo("Enroller released all its previously accepted joiners - session %u", mIndex); + } + else + { + LogInfo("Enroller released joiner %s - session %u", joinerIid.ToString().AsCString(), mIndex); + } + +exit: + return error; +} + +void Manager::CoapDtlsSession::SendEnrollerResponse(Uri aUri, + StateTlv::State aResponseState, + const Coap::Message &aRequest) +{ + OwnedPtr response; + + response.Reset(NewPriorityResponseMessage(aRequest)); + VerifyOrExit(response != nullptr); + + SuccessOrExit(Tlv::Append(*response, static_cast(aResponseState))); + + switch (aUri) + { + case kUriEnrollerRegister: + case kUriEnrollerKeepAlive: + SuccessOrExit(AppendAdmitterTlvs(*response, Get().DetermineState())); + break; + default: + break; + } + + SuccessOrExit(SendMessage(response.PassOwnership())); + + LogInfo("Send %s response (%s) - session %u", Admitter::EnrollerUriToString(aUri), + StateTlv::StateToString(aResponseState), mIndex); + +exit: + return; +} + +void Manager::CoapDtlsSession::SendEnrollerReportState(uint8_t aAdmitterState) +{ + OwnedPtr message; + + message.Reset(NewNonConfirmablePostMessage(kUriEnrollerReportState)); + VerifyOrExit(message != nullptr); + + SuccessOrExit(AppendAdmitterTlvs(*message, aAdmitterState)); + + SuccessOrExit(SendMessage(message.PassOwnership())); + + LogInfo("Send %s - session %u", UriToString(), mIndex); + +exit: + return; +} + +Error Manager::CoapDtlsSession::AppendAdmitterTlvs(Coap::Message &aMessage, uint8_t aAdmitterState) +{ + Error error; + uint16_t joinerUdpPort; + + SuccessOrExit(error = Tlv::Append(aMessage, aAdmitterState)); + + VerifyOrExit(aAdmitterState == Admitter::kStateActive); + + SuccessOrExit(error = Tlv::Append(aMessage, Get().GetCommissionerSessionId())); + + joinerUdpPort = Get().GetJoinerUdpPort(); + + if (joinerUdpPort != 0) + { + SuccessOrExit(error = Tlv::Append(aMessage, joinerUdpPort)); + } + +exit: + return error; +} + +void Manager::CoapDtlsSession::ForwardUdpRelayToEnroller(const Coap::Message &aMessage) +{ + Error error = kErrorNone; + + VerifyOrExit(IsEnroller()); + VerifyOrExit(mEnroller->ShouldForwardJoinerRelay()); + + SuccessOrExit(error = ForwardUdpRelay(aMessage)); + LogInfo("Forward %s to enroller - session %u", UriToString(), mIndex); + +exit: + LogWarnOnError(error, "forward UDP relay to enroller"); +} + +void Manager::CoapDtlsSession::ForwardUdpProxyToEnroller(const Message &aMessage, const Ip6::MessageInfo &aMessageInfo) +{ + Error error = kErrorNone; + + VerifyOrExit(IsEnroller()); + VerifyOrExit(mEnroller->ShouldForwardUdpProxy()); + + SuccessOrExit(error = ForwardUdpProxy(aMessage, aMessageInfo)); + + LogInfo("Forward %s to enroller - session %u", UriToString(), mIndex); + +exit: + LogWarnOnError(error, "forward UDP proxy to enroller"); +} + +} // namespace BorderAgent +} // namespace MeshCoP +} // namespace ot + +#endif // OPENTHREAD_CONFIG_BORDER_AGENT_ENABLE && OPENTHREAD_CONFIG_BORDER_AGENT_ADMITTER_ENABLE diff --git a/src/core/meshcop/border_agent_admitter.hpp b/src/core/meshcop/border_agent_admitter.hpp new file mode 100644 index 000000000..f28a1bae7 --- /dev/null +++ b/src/core/meshcop/border_agent_admitter.hpp @@ -0,0 +1,459 @@ +/* + * Copyright (c) 2025, The OpenThread Authors. + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * 3. Neither the name of the copyright holder nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ + +/** + * @file + * This file includes definitions for the Border Agent Admitter. + */ + +#ifndef OT_CORE_MESHCOP_BORDER_AGENT_ADMITTER_HPP_ +#define OT_CORE_MESHCOP_BORDER_AGENT_ADMITTER_HPP_ + +#include "openthread-core-config.h" + +#if OPENTHREAD_CONFIG_BORDER_AGENT_ENABLE && OPENTHREAD_CONFIG_BORDER_AGENT_ADMITTER_ENABLE + +#if !OPENTHREAD_CONFIG_TMF_NETDATA_SERVICE_ENABLE +#error "Border Admitter requires OPENTHREAD_CONFIG_TMF_NETDATA_SERVICE_ENABLE" +#endif + +#include + +#include "common/error.hpp" +#include "common/heap_allocatable.hpp" +#include "common/linked_list.hpp" +#include "common/locator.hpp" +#include "common/non_copyable.hpp" +#include "common/notifier.hpp" +#include "common/owning_list.hpp" +#include "common/tasklet.hpp" +#include "common/time.hpp" +#include "common/timer.hpp" +#include "common/uptime.hpp" +#include "meshcop/meshcop.hpp" +#include "meshcop/meshcop_tlvs.hpp" +#include "net/ip6_address.hpp" +#include "net/netif.hpp" +#include "net/udp6.hpp" +#include "thread/network_data_publisher.hpp" +#include "thread/tmf.hpp" + +namespace ot { +namespace MeshCoP { +namespace BorderAgent { + +class Manager; + +/** + * Implements a Border Agent Admitter. + */ +class Admitter : public InstanceLocator, private NonCopyable +{ + friend class Manager; + friend class ot::Notifier; + friend class NetworkData::Publisher; + + struct Joiner; + +public: + typedef otBorderAdmitterEnrollerInfo EnrollerInfo; ///< Information about an enroller. + typedef otBorderAdmitterJoinerInfo JoinerInfo; ///< Information about a joiner accepted by an enroller. + + /** + * Represents an iterator for enrollers and joiners accepted by an enroller. + */ + class Iterator : public otBorderAdmitterIterator + { + friend class Admitter; + + public: + /** + * Initializes the `Iterator`. + * + * @param[in] aInstance The OpenThread instance. + */ + void Init(Instance &aInstance); + + /** + * Retrieves the next enroller information. + * + * @param[out] aEnrollerInfo A `EnrollerInfo` to populate. + * + * @retval kErrorNone Successfully retrieved the next session. @p aEnrollerInfo is updated. + * @retval kErrorNotFound No more entries are available. The end of the list has been reached. + */ + Error GetNextEnrollerInfo(EnrollerInfo &aEnrollerInfo); + + /** + * Retrieves the information about next accepted joiner by the latest retrieved enroller during iteration. + * + * @param[out] aJoinerInfo A `JoinerInfo` to populate. + * + * @retval kErrorNone Successfully retrieved the next session. @p aJoinerInfo is updated. + * @retval kErrorNotFound No more entries are available. The end of the list has been reached. + */ + Error GetNextJoinerInfo(JoinerInfo &aJoinerInfo); + + private: + void SetSession(SecureSession *aSession) { mPtr1 = aSession; } + SecureSession *GetSession(void) const { return static_cast(mPtr1); } + void SetJoiner(Joiner *aJoiner) { mPtr2 = aJoiner; } + Joiner *GetJoiner(void) const { return static_cast(mPtr2); } + UptimeMsec GetInitUptime(void) const { return mData1; } + void SetInitUptime(UptimeMsec aUptime) { mData1 = aUptime; } + void SetInitTime(TimeMilli aNow) { mData2 = aNow.GetValue(); } + TimeMilli GetInitTime(void) const { return TimeMilli(mData2); } + + static void SkipToNextEnrollerSession(SecureSession *&aSession); + }; + + /** + * Initializes the `Admitter`. + * + * @param[in] aInstance The OpenThread instance. + */ + explicit Admitter(Instance &aInstance); + + /** + * Enables/Disables the Admitter functionality. + * + * @param[in] aEnable TRUE to enable, FALSE to disable. + */ + void SetEnabled(bool aEnable); + + /** + * Indicates whether or not the Admitter functionality is enabled. + * + * @retval TRUE The Admitter functionality is enabled. + * @retval FALSE The Admitter functionality is disabled. + */ + bool IsEnabled(void) const { return mEnabled; } + + /** + * Indicates whether or not the Admitter is selected and as the prime Admitter within the Thread mesh. + * + * @retval TRUE The Admitter is the prime Admitter. + * @retval FALSE The Admitter is not the prime Admitter. + */ + bool IsPrimeAdmitter(void) const { return mArbitrator.IsPrimeAdmitter(); } + + /** + * Indicates whether or not the Admitter is currently acting as the native Commissioner within the Thread mesh. + * + * @retval TRUE The Admitter is currently native Commissioner. + * @retval FALSE The Admitter is not a native Commissioner. + */ + bool IsActiveCommissioner(void) const { return mCommissionerPetitioner.IsActiveCommissioner(); } + + /** + * Indicates whether the Admitter's petition to become the native commissioner within mesh was rejected. + * + * A rejection typically occurs if there is already another active commissioner in the Thread network. + * + * @retval TRUE The petition was rejected. + * @retval FALSE The petition was not rejected. + */ + bool IsPetitionRejected(void) const { return mCommissionerPetitioner.IsPetitionRejected(); } + + /** + * Gets the Joiner UDP port. + * + * Zero value indicates the Joiner UDP port is not specified/fixed by the Admitter (Joiner Routers can pick). + * + * @returns The joiner UDP port number. + */ + uint16_t GetJoinerUdpPort(void) const { return mCommissionerPetitioner.GetJoinerUdpPort(); } + + /** + * Sets the joiner UDP port. + * + * Zero value indicates the Joiner UDP port is not specified/fixed by the Admitter (Joiner Routers can pick). + * + * @param[in] aInstance The OpenThread instance. + * @param[in] aUdpPort The UDP port number. + */ + void SetJoinerUdpPort(uint16_t aUdpPort) { mCommissionerPetitioner.SetJoinerUdpPort(aUdpPort); } + + /** + * Gets the commissioner session ID. + * + * @returns The commissioner session ID when `IsActiveCommissioner()`, zero otherwise. + */ + uint16_t GetCommissionerSessionId(void) const { return mCommissionerPetitioner.GetSessionId(); } + +private: + //----------------------------------------------------------------------------------------------------------------- + // Constants and enumerations + + static constexpr bool kEnabledByDefault = OPENTHREAD_CONFIG_BORDER_AGENT_ADMITTER_ENABLED_BY_DEFAULT; + static constexpr uint16_t kDefaultJoinerUdpPort = OPENTHREAD_CONFIG_BORDER_AGENT_ADMITTER_DEFAULT_JOINER_UDP_PORT; + + static constexpr uint32_t kEnrollerKeepAliveTimeout = 50 * Time::kOneSecondInMsec; + + enum State : uint8_t + { + kStateUnavailable = 0, // Cannot act as Admitter (e.g. disabled/stopped, or there is another Admitter). + kStateReady = 1, // Admitter is ready to accept Enroller registrations but not yet active. + kStateActive = 2, // Admitter is fully active (it is the native mesh commissioner). + kStateConflictError = 3, // Admitter could not become active (e.g., another commissioner is active). + }; + + //----------------------------------------------------------------------------------------------------------------- + // Nested Types + + struct Joiner : public InstanceLocator, LinkedListEntry, Heap::Allocatable + { + // Track information about a Joiner which is accepted by + // an Enroller. + + static constexpr uint32_t kTimeout = 7 * Time::kOneMinuteInMsec; + + Joiner(Instance &aInstance, const Ip6::InterfaceIdentifier &aIid); + + void UpdateExpirationTime(void); + bool Matches(const Ip6::InterfaceIdentifier &aIid) const; + bool Matches(const ExpirationChecker &aChecker) const; + + Joiner *mNext; + Ip6::InterfaceIdentifier mIid; + UptimeMsec mAcceptUptime; + TimeMilli mExpirationTime; + }; + + //- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + struct Enroller : Heap::Allocatable + { + // Tracks information for a registered enroller. This is + // included in `CoapDtlsSession` as the member variable + // `mEnroller` of type `OwnedPtr`. If the + // session is not for an enroller, the `OwnedPtr` will + // be null. + + bool ShouldForwardJoinerRelay(void) const { return (mMode & EnrollerModeTlv::kForwardJoinerRelayRx); } + bool ShouldForwardUdpProxy(void) const { return (mMode & EnrollerModeTlv::kForwardUdpProxyRx); } + + EnrollerIdTlv::StringType mId; + SteeringData mSteeringData; + OwningList mJoiners; + UptimeMsec mRegisterUptime; + uint8_t mMode; + }; + + //- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + class EnrollerIterator : private NonCopyable + { + // Iterates over Border Agent sessions and returns only the + // ones acting as Enroller. + + public: + explicit EnrollerIterator(Instance &aInstance); + + bool IsDone(void) const { return (mSession == nullptr); } + void Advance(void); + Enroller *GetEnroller(void) const; + uint16_t GetSessionIndex(void) const; + + template Type *GetSessionAs(void) const { return static_cast(mSession); } + + private: + void FindNextEnroller(void) { Iterator::SkipToNextEnrollerSession(mSession); } + + SecureSession *mSession; + }; + + //- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + void HandleNetDataPublisherEvent(NetworkData::Publisher::Event aEvent) { mArbitrator.HandlePublisherEvent(aEvent); } + void HandleArbitratorTimer(void) { mArbitrator.HandleTimer(); } + + class Arbitrator : public InstanceLocator, private NonCopyable + { + // Coordinates between Border Agents within mesh to decide which + // one can take the prime Admitter role. + + public: + explicit Arbitrator(Instance &aInstance); + + void Start(void); + void Stop(void); + bool IsPrimeAdmitter(void) const { return mState == kPrime; } + + void HandlePublisherEvent(NetworkData::Publisher::Event aEvent); + void HandleTimer(void); + + private: + static constexpr uint32_t kDelayToBecomePrime = 18 * Time::kOneSecondInMsec; + + enum State : uint8_t + { + kStopped, // Stopped. + kClaiming, // Actively trying to claim the prime role (publishing netdata service). + kCandidate, // Is a candidate (netdata service is added - delaying before becoming prime). + kPrime, // Is the prime admitter. + }; + + void SetState(State aState); + + static const char *StateToString(State aState); + + using DelayTimer = TimerMilliIn; + + State mState; + DelayTimer mTimer; + }; + + //- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + void HandleCommissionerPetitionerRetryTimer(void) { mCommissionerPetitioner.HandleRetryTimer(); } + void HandleCommissionerPetitionerKeepAliveTimer(void) { mCommissionerPetitioner.HandleKeepAliveTimer(); } + + class CommissionerPetitioner : public InstanceLocator, private NonCopyable + { + // Manages becoming the native commissioner within mesh. + + public: + enum State : uint8_t + { + kStopped, // Stopped. + kToPetition, // To send petition to leader for becoming commissioner. + kPetitioning, // Petition request sent, waiting for response from leader. + kAcceptedToSyncData, // Petition accepted - need to sync commissioner data with leader. + kAcceptedSyncingData, // Petition accepted - commissioner data sync in progress, waiting for response. + kAcceptedDataSynced, // Petition accepted - commissioner data sync done. + kRejected, // Petition rejected (another commissioner is active). + }; + + explicit CommissionerPetitioner(Instance &aInstance); + + void Start(void); + void Stop(void); + State GetState(void) const { return mState; } + bool IsActiveCommissioner(void) const; + bool IsPetitionRejected(void) const { return (mState == kRejected); } + uint16_t GetJoinerUdpPort(void) const { return mJoinerUdpPort; } + void SetJoinerUdpPort(uint16_t aUdpPort); + uint16_t GetSessionId(void) const { return mSessionId; } + const Ip6::Address &GetAloc(void) const { return mAloc.GetAddress(); } + + void HandleEnrollerChange(void); + void HandleNetDataChange(void); + void HandleRetryTimer(void); + void HandleKeepAliveTimer(void); + + private: + // All intervals are in msec + static constexpr uint32_t kPetitionRetryDelay = Time::kOneSecondInMsec; + static constexpr uint16_t kPetitionRetryJitter = 100; + static constexpr uint32_t kKeepAliveTxInterval = 25 * Time::kOneSecondInMsec; // Half of timeout used by leader + static constexpr uint16_t kKeepAliveTxJitter = 500; + static constexpr uint32_t kKeepAliveRetryDelay = Time::kOneSecondInMsec; + static constexpr uint16_t kKeepAliveRetryJitter = 100; + static constexpr uint32_t kDataSyncRetryDelay = Time::kOneSecondInMsec; + static constexpr uint16_t kDataSyncRetryJitter = 100; + + void SetState(State aState); + void SendPetitionIfNoOtherCommissioner(void); + void SchedulePetitionRetry(void); + Error ProcessPetitionResponse(const Coap::Msg *aMsg, Error aResult); + void AddAlocAndUdpReceiver(void); + void RemoveAlocAndUdpReceiver(void); + void ScheduleNextKeepAlive(void); + void ScheduleKeepAliveRetry(void); + Error SendKeepAlive(StateTlv::State aState); + void SendDataSet(void); + Error ProcessDataSetResponse(const Coap::Msg *aMsg, Error aResult); + void ScheduleDataSyncRetry(void); + void ScheduleImmediateDataSync(void); + Error SendToLeader(OwnedPtr aMessage, Coap::ResponseHandler aHandler); + bool HandleUdpReceive(const Message &aMessage, const Ip6::MessageInfo &aMessageInfo); + + DeclareTmfResponseHandlerIn(CommissionerPetitioner, HandlePetitionResponse); + DeclareTmfResponseHandlerIn(CommissionerPetitioner, HandleKeepAliveResponse); + DeclareTmfResponseHandlerIn(CommissionerPetitioner, HandleDataSetResponse); + + static bool HandleUdpReceive(void *aContext, const otMessage *aMessage, const otMessageInfo *aMessageInfo); + static const char *StateToString(State aState); + + using RetryTimer = TimerMilliIn; + using KeepAliveTimer = TimerMilliIn; + + State mState; + SteeringData mSteeringData; + uint16_t mJoinerUdpPort; + uint16_t mSessionId; + RetryTimer mRetryTimer; + KeepAliveTimer mKeepAliveTimer; + Ip6::Udp::Receiver mUdpReceiver; + Ip6::Netif::UnicastAddress mAloc; + }; + + //----------------------------------------------------------------------------------------------------------------- + // Methods + + State DetermineState(void) const; + void EvaluateOperation(void); + void PostReportStateTask(void); + void HandleReportStateTask(void); + void DetermineSteeringData(SteeringData &aSteeringData) const; + void ForwardJoinerRelayToEnrollers(const Coap::Msg &aMsg); + void ForwardUdpProxyToEnrollers(const Message &aMessage, const Ip6::MessageInfo &aMessageInfo); + void HandleJoinerTimer(void); + void HandleEnrollerChange(void); + void HandleNotifierEvents(Events aEvents); + + static const char *EnrollerUriToString(Uri aUri); + + //----------------------------------------------------------------------------------------------------------------- + // Variables and Constants + + using JoinerTimer = TimerMilliIn; + using ReportStateTask = TaskletIn; + + static const uint8_t kEnrollerValidSteeringDataLengths[]; + + bool mEnabled; + bool mHasAnyEnroller; + Arbitrator mArbitrator; + CommissionerPetitioner mCommissionerPetitioner; + JoinerTimer mJoinerTimer; + ReportStateTask mReportStateTask; + State mLastSyncedState; +}; + +} // namespace BorderAgent +} // namespace MeshCoP + +DefineCoreType(otBorderAdmitterIterator, MeshCoP::BorderAgent::Admitter::Iterator); + +} // namespace ot + +#endif // OPENTHREAD_CONFIG_BORDER_AGENT_ENABLE && OPENTHREAD_CONFIG_BORDER_AGENT_ADMITTER_ENABLE + +#endif // OT_CORE_MESHCOP_BORDER_AGENT_ADMITTER_HPP_ diff --git a/src/core/meshcop/border_agent_txt_data.cpp b/src/core/meshcop/border_agent_txt_data.cpp index 002979fee..a2dc9c428 100644 --- a/src/core/meshcop/border_agent_txt_data.cpp +++ b/src/core/meshcop/border_agent_txt_data.cpp @@ -262,6 +262,13 @@ uint32_t TxtData::StateBitmap::Determine(Instance &aInstance) } #endif +#if OPENTHREAD_CONFIG_BORDER_AGENT_ADMITTER_ENABLE + if (aInstance.Get().IsEnabled()) + { + bitmap |= kFlagAdmitterSupported; + } +#endif + return bitmap; } @@ -431,9 +438,10 @@ void TxtData::StateBitmap::Parse(uint32_t aBitmap, Info &aInfo) aInfo.mThreadRole = static_cast((aBitmap & kMaskRole) >> kOffsetRole); aInfo.mMultiAilState = static_cast((aBitmap & kMaskMultiAilState) >> kOffsetMultiAilState); - aInfo.mBbrIsActive = aBitmap & kFlagBbrIsActive; - aInfo.mBbrIsPrimary = aBitmap & kFlagBbrIsPrimary; - aInfo.mEpskcSupported = aBitmap & kFlagEpskcSupported; + aInfo.mBbrIsActive = aBitmap & kFlagBbrIsActive; + aInfo.mBbrIsPrimary = aBitmap & kFlagBbrIsPrimary; + aInfo.mEpskcSupported = aBitmap & kFlagEpskcSupported; + aInfo.mAdmitterSupported = aBitmap & kFlagAdmitterSupported; } #endif // OPENTHREAD_CONFIG_BORDER_AGENT_TXT_DATA_PARSER_ENABLE diff --git a/src/core/meshcop/border_agent_txt_data.hpp b/src/core/meshcop/border_agent_txt_data.hpp index 13b6ac604..6592bf21d 100644 --- a/src/core/meshcop/border_agent_txt_data.hpp +++ b/src/core/meshcop/border_agent_txt_data.hpp @@ -265,23 +265,25 @@ private: struct StateBitmap { - static constexpr uint8_t kOffsetConnMode = 0; - static constexpr uint8_t kOffsetIfState = 3; - static constexpr uint8_t kOffsetAvailability = 5; - static constexpr uint8_t kOffsetBbrIsActive = 7; - static constexpr uint8_t kOffsetBbrIsPrimary = 8; - static constexpr uint8_t kOffsetRole = 9; - static constexpr uint8_t kOffsetEpskcSupported = 11; - static constexpr uint8_t kOffsetMultiAilState = 12; + static constexpr uint8_t kOffsetConnMode = 0; + static constexpr uint8_t kOffsetIfState = 3; + static constexpr uint8_t kOffsetAvailability = 5; + static constexpr uint8_t kOffsetBbrIsActive = 7; + static constexpr uint8_t kOffsetBbrIsPrimary = 8; + static constexpr uint8_t kOffsetRole = 9; + static constexpr uint8_t kOffsetEpskcSupported = 11; + static constexpr uint8_t kOffsetMultiAilState = 12; + static constexpr uint8_t kOffsetAdmitterSupported = 14; - static constexpr uint32_t kMaskConnMode = 7 << kOffsetConnMode; - static constexpr uint32_t kMaskIfState = 3 << kOffsetIfState; - static constexpr uint32_t kMaskAvailability = 3 << kOffsetAvailability; - static constexpr uint32_t kFlagBbrIsActive = 1 << kOffsetBbrIsActive; - static constexpr uint32_t kFlagBbrIsPrimary = 1 << kOffsetBbrIsPrimary; - static constexpr uint32_t kMaskRole = 3 << kOffsetRole; - static constexpr uint32_t kFlagEpskcSupported = 1 << kOffsetEpskcSupported; - static constexpr uint32_t kMaskMultiAilState = 3 << kOffsetMultiAilState; + static constexpr uint32_t kMaskConnMode = 7 << kOffsetConnMode; + static constexpr uint32_t kMaskIfState = 3 << kOffsetIfState; + static constexpr uint32_t kMaskAvailability = 3 << kOffsetAvailability; + static constexpr uint32_t kFlagBbrIsActive = 1 << kOffsetBbrIsActive; + static constexpr uint32_t kFlagBbrIsPrimary = 1 << kOffsetBbrIsPrimary; + static constexpr uint32_t kMaskRole = 3 << kOffsetRole; + static constexpr uint32_t kFlagEpskcSupported = 1 << kOffsetEpskcSupported; + static constexpr uint32_t kMaskMultiAilState = 3 << kOffsetMultiAilState; + static constexpr uint32_t kFlagAdmitterSupported = 1 << kOffsetAdmitterSupported; static_assert(kConnModeDisabled == 0, "kConnModeDisabled is incorrect"); static_assert(kConnModePskc == 1, "kConnModePskc is incorrect"); diff --git a/src/core/meshcop/meshcop_tlvs.hpp b/src/core/meshcop/meshcop_tlvs.hpp index 469332372..7cbf20e6a 100644 --- a/src/core/meshcop/meshcop_tlvs.hpp +++ b/src/core/meshcop/meshcop_tlvs.hpp @@ -113,6 +113,9 @@ public: kEnergyList = OT_MESHCOP_TLV_ENERGY_LIST, ///< Energy List TLV kThreadDomainName = OT_MESHCOP_TLV_THREAD_DOMAIN_NAME, ///< Thread Domain Name TLV kWakeupChannel = OT_MESHCOP_TLV_WAKEUP_CHANNEL, ///< Wakeup Channel TLV + kAdmitterState = OT_MESHCOP_TLV_ADMITTER_STATE, ///< Admitter State TLV + kEnrollerId = OT_MESHCOP_TLV_ENROLLER_ID, ///< Enroller ID TLV + kEnrollerMode = OT_MESHCOP_TLV_ENROLLER_MODE, ///< Enroller Mode TLV kDiscoveryRequest = OT_MESHCOP_TLV_DISCOVERYREQUEST, ///< Discovery Request TLV kDiscoveryResponse = OT_MESHCOP_TLV_DISCOVERYRESPONSE, ///< Discovery Response TLV kJoinerAdvertisement = OT_MESHCOP_TLV_JOINERADVERTISEMENT, ///< Joiner Advertisement TLV @@ -124,6 +127,7 @@ public: static constexpr uint8_t kMaxProvisioningUrlLength = OT_PROVISIONING_URL_MAX_SIZE; static constexpr uint8_t kMaxCommissionerIdLength = 64; ///< Max length of Commissioner ID TLV. + static constexpr uint8_t kMaxEnrollerIdLength = 64; ///< Max length of Enroller ID TLV. static constexpr uint8_t kMaxVendorNameLength = 32; ///< Max length of Vendor Name TLV. static constexpr uint8_t kMaxVendorModelLength = 32; ///< Max length of Vendor Model TLV. static constexpr uint8_t kMaxVendorSwVersionLength = 16; ///< Max length of Vendor SW Version TLV. @@ -1154,6 +1158,30 @@ private: #endif // OPENTHREAD_CONFIG_JOINER_ADV_EXPERIMENTAL_ENABLE +/** + * Defines Enroller ID TLV constants and types. + */ +typedef StringTlvInfo EnrollerIdTlv; + +/** + * Defines Admitter State TLV constants and types. + * + *State values are defined in `BorderAgent::Admitter` + */ +typedef UintTlvInfo AdmitterStateTlv; + +/** + * Implements Enroller Mode TLV generation and parsing. + */ +class EnrollerModeTlv : public UintTlvInfo +{ +public: + EnrollerModeTlv(void) = delete; + + static constexpr uint8_t kForwardJoinerRelayRx = 1 << 7; ///< Forward RelayRx (from Joiners) to Enroller. + static constexpr uint8_t kForwardUdpProxyRx = 1 << 6; ///< Forward UDP ProxyRx to Enroller. +}; + } // namespace MeshCoP } // namespace ot diff --git a/src/core/meshcop/steering_data.cpp b/src/core/meshcop/steering_data.cpp index cf9f10e80..f3bd2c1af 100644 --- a/src/core/meshcop/steering_data.cpp +++ b/src/core/meshcop/steering_data.cpp @@ -150,6 +150,11 @@ exit: return contains; } +bool SteeringData::operator==(const SteeringData &aOther) const +{ + return (GetLength() == aOther.GetLength()) && (memcmp(GetData(), aOther.GetData(), GetLength()) == 0); +} + void SteeringData::CalculateHashBitIndexes(const Mac::ExtAddress &aJoinerId, HashBitIndexes &aIndexes) { aIndexes.mIndex[0] = CrcCalculator(kCrc16CcittPolynomial).Feed(aJoinerId); diff --git a/src/core/meshcop/steering_data.hpp b/src/core/meshcop/steering_data.hpp index 3a0654e1c..7cafba1d8 100644 --- a/src/core/meshcop/steering_data.hpp +++ b/src/core/meshcop/steering_data.hpp @@ -41,6 +41,7 @@ #include "common/as_core_type.hpp" #include "common/bit_utils.hpp" #include "common/code_utils.hpp" +#include "common/equatable.hpp" #include "common/error.hpp" #include "common/string.hpp" #include "mac/mac_types.hpp" @@ -53,7 +54,7 @@ class JoinerDiscerner; /** * Represents Steering Data (bloom filter). */ -class SteeringData : public otSteeringData +class SteeringData : public otSteeringData, public Unequatable { public: static constexpr uint8_t kMinLength = OT_STEERING_DATA_MIN_LENGTH; ///< Minimum Steering Data length (in bytes). @@ -216,6 +217,16 @@ public: */ bool Contains(const HashBitIndexes &aIndexes) const; + /** + * Overloads operator `==` to compare two Steering Data instances. + * + * @param[in] aOther The other `SteeringData` to compare with. + * + * @retval TRUE If the two Steering Data instances are equal. + * @retval FALSE If the two Steering Data instances are not equal. + */ + bool operator==(const SteeringData &aOther) const; + /** * Converts the Steering Data to a human-readable string representation. * diff --git a/src/core/net/netif.hpp b/src/core/net/netif.hpp index 0213b4b49..3581af5c0 100644 --- a/src/core/net/netif.hpp +++ b/src/core/net/netif.hpp @@ -148,6 +148,13 @@ public: */ Address &GetAddress(void) { return AsCoreType(&mAddress); } + /** + * Sets the unicast address. + * + * @param[in] aAddress The unicast address. + */ + void SetAddress(const Address &aAddress) { mAddress = aAddress; } + /** * Returns the address's prefix length (in bits). * diff --git a/src/core/openthread-core-config.h b/src/core/openthread-core-config.h index e669c4c25..0fe8f0631 100644 --- a/src/core/openthread-core-config.h +++ b/src/core/openthread-core-config.h @@ -67,6 +67,7 @@ #include "config/announce_sender.h" #include "config/backbone_router.h" #include "config/border_agent.h" +#include "config/border_agent_admitter.h" #include "config/border_router.h" #include "config/border_routing.h" #include "config/channel_manager.h" diff --git a/src/core/thread/network_data_publisher.cpp b/src/core/thread/network_data_publisher.cpp index 049d968f0..25604346c 100644 --- a/src/core/thread/network_data_publisher.cpp +++ b/src/core/thread/network_data_publisher.cpp @@ -49,6 +49,9 @@ Publisher::Publisher(Instance &aInstance) : InstanceLocator(aInstance) #if OPENTHREAD_CONFIG_TMF_NETDATA_SERVICE_ENABLE , mDnsSrpServiceEntry(aInstance) +#if OPENTHREAD_CONFIG_BORDER_AGENT_ENABLE && OPENTHREAD_CONFIG_BORDER_AGENT_ADMITTER_ENABLE + , mBorderAdmitterEntry(aInstance) +#endif #endif , mTimer(aInstance) { @@ -220,6 +223,9 @@ void Publisher::HandleNotifierEvents(Events aEvents) { #if OPENTHREAD_CONFIG_TMF_NETDATA_SERVICE_ENABLE mDnsSrpServiceEntry.HandleNotifierEvents(aEvents); +#if OPENTHREAD_CONFIG_BORDER_AGENT_ENABLE && OPENTHREAD_CONFIG_BORDER_AGENT_ADMITTER_ENABLE + mBorderAdmitterEntry.HandleNotifierEvents(aEvents); +#endif #endif #if OPENTHREAD_CONFIG_BORDER_ROUTER_ENABLE @@ -234,6 +240,9 @@ void Publisher::HandleTimer(void) { #if OPENTHREAD_CONFIG_TMF_NETDATA_SERVICE_ENABLE mDnsSrpServiceEntry.HandleTimer(); +#if OPENTHREAD_CONFIG_BORDER_AGENT_ENABLE && OPENTHREAD_CONFIG_BORDER_AGENT_ADMITTER_ENABLE + mBorderAdmitterEntry.HandleTimer(); +#endif #endif #if OPENTHREAD_CONFIG_BORDER_ROUTER_ENABLE @@ -386,6 +395,12 @@ void Publisher::Entry::Add(void) { static_cast(this)->Add(); } +#if OPENTHREAD_CONFIG_BORDER_AGENT_ENABLE && OPENTHREAD_CONFIG_BORDER_AGENT_ADMITTER_ENABLE + if (Get().IsBorderAdmitterEntry(*this)) + { + static_cast(this)->Add(); + } +#endif #endif #if OPENTHREAD_CONFIG_BORDER_ROUTER_ENABLE @@ -403,6 +418,12 @@ void Publisher::Entry::Remove(State aNextState) { static_cast(this)->Remove(aNextState); } +#if OPENTHREAD_CONFIG_BORDER_AGENT_ENABLE && OPENTHREAD_CONFIG_BORDER_AGENT_ADMITTER_ENABLE + if (Get().IsBorderAdmitterEntry(*this)) + { + static_cast(this)->Remove(aNextState); + } +#endif #endif #if OPENTHREAD_CONFIG_BORDER_ROUTER_ENABLE @@ -423,6 +444,13 @@ Publisher::Entry::InfoString Publisher::Entry::ToString(bool aIncludeState) cons string.Append("DNS/SRP service"); ExitNow(); } +#if OPENTHREAD_CONFIG_BORDER_AGENT_ENABLE && OPENTHREAD_CONFIG_BORDER_AGENT_ADMITTER_ENABLE + if (Get().IsBorderAdmitterEntry(*this)) + { + string.Append("Border Admitter service"); + ExitNow(); + } +#endif #endif #if OPENTHREAD_CONFIG_BORDER_ROUTER_ENABLE @@ -766,6 +794,107 @@ Publisher::DnsSrpServiceEntry::Info::Info(Type aType, } } +#if OPENTHREAD_CONFIG_BORDER_AGENT_ENABLE && OPENTHREAD_CONFIG_BORDER_AGENT_ADMITTER_ENABLE + +//--------------------------------------------------------------------------------------------------------------------- +// Publisher::BorderAddmitterEntry + +Publisher::BorderAdmitterEntry::BorderAdmitterEntry(Instance &aInstance) { Init(aInstance); } + +void Publisher::BorderAdmitterEntry::Publish(void) +{ + VerifyOrExit(GetState() == kNoEntry); + LogInfo("Publishing Border Admitter service"); + SetState(kToAdd); + Process(); + +exit: + return; +} + +void Publisher::BorderAdmitterEntry::Unpublish(void) +{ + VerifyOrExit(GetState() != kNoEntry); + LogInfo("Unpublishing Border Admitter service"); + Remove(/* aNextState */ kNoEntry); + +exit: + return; +} + +void Publisher::BorderAdmitterEntry::HandleNotifierEvents(Events aEvents) +{ + if (aEvents.ContainsAny(kEventThreadNetdataChanged | kEventThreadRoleChanged)) + { + Process(); + } +} + +void Publisher::BorderAdmitterEntry::Add(void) +{ + SuccessOrExit(Get().AddBorderAdmitterService()); + Get().HandleServerDataUpdated(); + SetState(kAdded); + Notify(kEventEntryAdded); + +exit: + return; +} + +void Publisher::BorderAdmitterEntry::Remove(State aNextState) +{ + VerifyOrExit((GetState() == kAdded) || (GetState() == kRemoving)); + + SuccessOrExit(Get().RemoveBorderAdmitterService()); + Get().HandleServerDataUpdated(); + Notify(kEventEntryRemoved); + +exit: + SetState(aNextState); +} + +void Publisher::BorderAdmitterEntry::Notify(Event aEvent) const +{ + Get().HandleNetDataPublisherEvent(aEvent); +} + +void Publisher::BorderAdmitterEntry::Process(void) +{ + // This method checks the entries currently present in Network Data + // based on which it then decides whether or not take action + // (add/remove or keep monitoring). + + uint8_t numEntries = 0; + uint8_t numPreferredEntries = 0; + + VerifyOrExit(Get().IsAttached()); + VerifyOrExit(GetState() != kNoEntry); + + CountEntries(numEntries, numPreferredEntries); + UpdateState(numEntries, numPreferredEntries, kDesiredNum); + +exit: + return; +} + +void Publisher::BorderAdmitterEntry::CountEntries(uint8_t &aNumEntries, uint8_t &aNumPreferredEntries) const +{ + Service::Iterator iterator(GetInstance()); + uint16_t rloc16; + + while (iterator.GetNextBorderAdmitterInfo(rloc16) == kErrorNone) + { + aNumEntries++; + + if (IsPreferred(rloc16)) + { + aNumPreferredEntries++; + } + } +} + +#endif // OPENTHREAD_CONFIG_BORDER_AGENT_ENABLE && OPENTHREAD_CONFIG_BORDER_AGENT_ADMITTER_ENABLE + #endif // OPENTHREAD_CONFIG_TMF_NETDATA_SERVICE_ENABLE #if OPENTHREAD_CONFIG_BORDER_ROUTER_ENABLE diff --git a/src/core/thread/network_data_publisher.hpp b/src/core/thread/network_data_publisher.hpp index 16e747b52..f149a211a 100644 --- a/src/core/thread/network_data_publisher.hpp +++ b/src/core/thread/network_data_publisher.hpp @@ -188,6 +188,26 @@ public: */ void UnpublishDnsSrpService(void) { mDnsSrpServiceEntry.Unpublish(); } +#if OPENTHREAD_CONFIG_BORDER_AGENT_ENABLE && OPENTHREAD_CONFIG_BORDER_AGENT_ADMITTER_ENABLE + /** + * Requests Border Admitter service entry to be published in the Thread Network Data. + */ + void PublishBorderAdmitterService(void) { mBorderAdmitterEntry.Publish(); } + + /** + * Unpublishes any previously added Border Admitter service entry from the Network Data. + */ + void UnpublishBorderAdmitterService(void) { mBorderAdmitterEntry.Unpublish(); } + + /** + * Indicates whether or not the Border Admitter service entry has been added in the Thread Network Data. + * + * @retval TRUE The published service entry is added to the Thread Network Data. + * @retval FALSE The entry is not added to Thread Network Data or there is no entry to publish. + */ + bool IsBorderAdmitterServicePublished(void) { return mBorderAdmitterEntry.IsAdded(); } +#endif + #endif // OPENTHREAD_CONFIG_TMF_NETDATA_SERVICE_ENABLE #if OPENTHREAD_CONFIG_BORDER_ROUTER_ENABLE @@ -440,6 +460,30 @@ private: Info mInfo; Callback mCallback; }; + +#if OPENTHREAD_CONFIG_BORDER_AGENT_ENABLE && OPENTHREAD_CONFIG_BORDER_AGENT_ADMITTER_ENABLE + class BorderAdmitterEntry : public Entry, private NonCopyable + { + friend class Entry; + + public: + explicit BorderAdmitterEntry(Instance &aInstance); + void Publish(void); + void Unpublish(void); + void HandleTimer(void) { Entry::HandleTimer(); } + void HandleNotifierEvents(Events aEvents); + + private: + static constexpr uint8_t kDesiredNum = 1; + + void Add(void); + void Remove(State aNextState); + void Notify(Event aEvent) const; + void Process(void); + void CountEntries(uint8_t &aNumEntries, uint8_t &aNumPreferredEntries) const; + }; +#endif // OPENTHREAD_CONFIG_BORDER_AGENT_ENABLE && OPENTHREAD_CONFIG_BORDER_AGENT_ADMITTER_ENABLE + #endif // OPENTHREAD_CONFIG_TMF_NETDATA_SERVICE_ENABLE #if OPENTHREAD_CONFIG_BORDER_ROUTER_ENABLE @@ -499,6 +543,9 @@ private: #if OPENTHREAD_CONFIG_TMF_NETDATA_SERVICE_ENABLE bool IsADnsSrpServiceEntry(const Entry &aEntry) const { return (&aEntry == &mDnsSrpServiceEntry); } +#if OPENTHREAD_CONFIG_BORDER_AGENT_ENABLE && OPENTHREAD_CONFIG_BORDER_AGENT_ADMITTER_ENABLE + bool IsBorderAdmitterEntry(const Entry &aEntry) const { return (&aEntry == &mBorderAdmitterEntry); } +#endif #endif #if OPENTHREAD_CONFIG_BORDER_ROUTER_ENABLE @@ -517,6 +564,9 @@ private: #if OPENTHREAD_CONFIG_TMF_NETDATA_SERVICE_ENABLE DnsSrpServiceEntry mDnsSrpServiceEntry; +#if OPENTHREAD_CONFIG_BORDER_AGENT_ENABLE && OPENTHREAD_CONFIG_BORDER_AGENT_ADMITTER_ENABLE + BorderAdmitterEntry mBorderAdmitterEntry; +#endif #endif #if OPENTHREAD_CONFIG_BORDER_ROUTER_ENABLE diff --git a/src/core/thread/network_data_service.cpp b/src/core/thread/network_data_service.cpp index 1dc903147..dce8f14b0 100644 --- a/src/core/thread/network_data_service.cpp +++ b/src/core/thread/network_data_service.cpp @@ -183,6 +183,40 @@ exit: return error; } +#if OPENTHREAD_CONFIG_BORDER_AGENT_ENABLE && OPENTHREAD_CONFIG_BORDER_AGENT_ADMITTER_ENABLE + +Error Iterator::GetNextBorderAdmitterInfo(uint16_t &aRloc16) +{ + Error error = kErrorNone; + uint8_t serviceNumber = Manager::kBorderAdmitterServiceNumber; + do + { + ServiceData serviceData; + + // Process the next Server sub-TLV within the current Service TLV. + + if (AdvanceToNextServer() == kErrorNone) + { + aRloc16 = mServerSubTlv->GetServer16(); + ExitNow(); + } + + // Find the next matching Service TLV. + + serviceData.InitFrom(serviceNumber); + mServiceTlv = mNetworkData.FindNextThreadService(mServiceTlv, serviceData, NetworkData::kServicePrefixMatch); + mServerSubTlv = nullptr; + + } while (mServiceTlv != nullptr); + + error = kErrorNotFound; + +exit: + return error; +} + +#endif // OPENTHREAD_CONFIG_BORDER_AGENT_ENABLE && OPENTHREAD_CONFIG_BORDER_AGENT_ADMITTER_ENABLE + Error Iterator::AdvanceToNextServer(void) { Error error = kErrorNotFound; diff --git a/src/core/thread/network_data_service.hpp b/src/core/thread/network_data_service.hpp index b4f627253..89c0a6d19 100644 --- a/src/core/thread/network_data_service.hpp +++ b/src/core/thread/network_data_service.hpp @@ -144,6 +144,21 @@ public: */ Error GetNextDnsSrpUnicastInfo(DnsSrpUnicastType aType, DnsSrpUnicastInfo &aInfo); +#if OPENTHREAD_CONFIG_BORDER_AGENT_ENABLE && OPENTHREAD_CONFIG_BORDER_AGENT_ADMITTER_ENABLE + /** + * Gets the next Border Admitter service info from the Thread Network Data Border Admitter servcie entries. + * + * To start from the first service entry, ensure the iterator is reset (e.g., by creating a new `Iterator` + * instance, or by calling `Reset()`). + * + * @param[out] aRloc16 On success, returns the RLOC16 or device which added Border Admitter service entry. + * + * @retval kErrorNone Successfully got the next info. @p aRloc16 is updated. + * @retval kErrorNotFound No more matching entries in the Network Data. + */ + Error GetNextBorderAdmitterInfo(uint16_t &aRloc16); +#endif + private: Error AdvanceToNextServer(void); @@ -277,6 +292,24 @@ public: Error RemoveBackboneRouterService(void) { return RemoveService(kBackboneRouterServiceNumber); } #endif +#if OPENTHREAD_CONFIG_BORDER_AGENT_ENABLE && OPENTHREAD_CONFIG_BORDER_AGENT_ADMITTER_ENABLE + /** + * Adds a Border Admitter Service entry to the local Thread Network Data. + * + * @retval kErrorNone Successfully added the Service entry. + * @retval kErrorNoBufs Insufficient space to add the Service entry. + */ + Error AddBorderAdmitterService(void) { return AddService(kBorderAdmitterServiceNumber); } + + /** + * Removes the Border Admitter Service entry from the local Thread Network Data. + * + * @retval kErrorNone Successfully removed the Service entry. + * @retval kErrorNotFound Could not find the Service entry. + */ + Error RemoveBorderAdmitterService(void) { return RemoveService(kBorderAdmitterServiceNumber); } +#endif + #endif // OPENTHREAD_CONFIG_TMF_NETDATA_SERVICE_ENABLE #if (OPENTHREAD_CONFIG_THREAD_VERSION >= OT_THREAD_VERSION_1_2) @@ -322,6 +355,7 @@ private: static constexpr uint8_t kBackboneRouterServiceNumber = 0x01; static constexpr uint8_t kDnsSrpAnycastServiceNumber = 0x5c; static constexpr uint8_t kDnsSrpUnicastServiceNumber = 0x5d; + static constexpr uint8_t kBorderAdmitterServiceNumber = 0xad; //- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - @@ -504,6 +538,8 @@ private: return AddService(&aServiceData, aServiceData.GetLength(), &aServerData, sizeof(ServerDataType)); } + Error AddService(uint8_t aServiceNumber) { return AddService(&aServiceNumber, sizeof(uint8_t), nullptr, 0); } + Error AddService(const void *aServiceData, uint8_t aServiceDataLength, const void *aServerData, diff --git a/src/core/thread/tmf.cpp b/src/core/thread/tmf.cpp index a6c30d05c..291c619c8 100644 --- a/src/core/thread/tmf.cpp +++ b/src/core/thread/tmf.cpp @@ -284,7 +284,7 @@ SecureAgent::SecureAgent(Instance &aInstance) { SetAcceptCallback(&HandleDtlsAccept, this); -#if OPENTHREAD_FTD && OPENTHREAD_CONFIG_COMMISSIONER_ENABLE +#if (OPENTHREAD_FTD && OPENTHREAD_CONFIG_COMMISSIONER_ENABLE) || OPENTHREAD_PLATFORM_NEXUS SetResourceHandler(&HandleResource); #endif } @@ -301,7 +301,7 @@ Coap::SecureSession *SecureAgent::HandleDtlsAccept(void) return IsSessionInUse() ? nullptr : static_cast(this); } -#if OPENTHREAD_FTD && OPENTHREAD_CONFIG_COMMISSIONER_ENABLE +#if (OPENTHREAD_FTD && OPENTHREAD_CONFIG_COMMISSIONER_ENABLE) || OPENTHREAD_PLATFORM_NEXUS bool SecureAgent::HandleResource(CoapBase &aCoapBase, const char *aUriPath, Msg &aMsg) { @@ -313,16 +313,25 @@ bool SecureAgent::HandleResource(const char *aUriPath, Msg &aMsg) bool didHandle = false; Uri uri = UriFromPath(aUriPath); +#if (OPENTHREAD_FTD && OPENTHREAD_CONFIG_COMMISSIONER_ENABLE) if (uri == kUriJoinerFinalize) { Get().HandleTmf(aMsg); didHandle = true; } +#endif + +#if OPENTHREAD_PLATFORM_NEXUS + if (mResourceHandler.IsSet()) + { + didHandle = mResourceHandler.Invoke(uri, aMsg); + } +#endif return didHandle; } -#endif // OPENTHREAD_FTD && OPENTHREAD_CONFIG_COMMISSIONER_ENABLE +#endif // (OPENTHREAD_FTD && OPENTHREAD_CONFIG_COMMISSIONER_ENABLE) || OPENTHREAD_PLATFORM_NEXUS #endif // OPENTHREAD_CONFIG_SECURE_TRANSPORT_ENABLE diff --git a/src/core/thread/tmf.hpp b/src/core/thread/tmf.hpp index 71ae38e0e..1c8dca1ba 100644 --- a/src/core/thread/tmf.hpp +++ b/src/core/thread/tmf.hpp @@ -39,6 +39,7 @@ #include "coap/coap.hpp" #include "coap/coap_secure.hpp" #include "common/as_core_type.hpp" +#include "common/callback.hpp" #include "common/locator.hpp" namespace ot { @@ -229,14 +230,40 @@ public: */ explicit SecureAgent(Instance &aInstance); +#if OPENTHREAD_PLATFORM_NEXUS + /** + * Represents a resource handler callback function. + * + * @param[in] aContext An arbitrary context (provided when callback is registered). + * @param[in] aUriPath The URI. + * @param[in] aMsg The received message. + * + * @retval TRUE Indicates that the URI was known and the message was processed by the handler. + * @retval FALSE Indicates that the URI was not known and the message was not processed by the handler. + */ + typedef bool (*ResourceHandler)(void *aContext, Uri aUri, Msg &aMsg); + + /** + * Registers a resource handler callback. + * + * @param[in] aHandler The handler function pointer. + * @param[in] aContext An arbitrary context (passed to the @p aHandler when it is invoked). + */ + void RegisterResourceHandler(ResourceHandler aHandler, void *aContext) { mResourceHandler.Set(aHandler, aContext); } +#endif + private: static MeshCoP::SecureSession *HandleDtlsAccept(void *aContext, const Ip6::MessageInfo &aMessageInfo); Coap::SecureSession *HandleDtlsAccept(void); -#if OPENTHREAD_FTD && OPENTHREAD_CONFIG_COMMISSIONER_ENABLE +#if (OPENTHREAD_FTD && OPENTHREAD_CONFIG_COMMISSIONER_ENABLE) || OPENTHREAD_PLATFORM_NEXUS static bool HandleResource(CoapBase &aCoapBase, const char *aUriPath, Msg &aMsg); bool HandleResource(const char *aUriPath, Msg &aMsg); #endif + +#if OPENTHREAD_PLATFORM_NEXUS + Callback mResourceHandler; +#endif }; #endif diff --git a/src/core/thread/uri_paths.cpp b/src/core/thread/uri_paths.cpp index aafdab496..2e6c7fb51 100644 --- a/src/core/thread/uri_paths.cpp +++ b/src/core/thread/uri_paths.cpp @@ -78,6 +78,11 @@ static constexpr Entry kEntries[] = { {"c/jf"}, // kUriJoinerFinalize {"c/la"}, // kUriLeaderKeepAlive {"c/lp"}, // kUriLeaderPetition + {"c/nj"}, // kUriEnrollerJoinerAccept + {"c/nk"}, // kUriEnrollerKeepAlive + {"c/nl"}, // kUriEnrollerJoinerRelease + {"c/nr"}, // kUriEnrollerRegister + {"c/ns"}, // kUriEnrollerReportState {"c/pc"}, // kUriPanIdConflict {"c/pg"}, // kUriPendingGet {"c/pq"}, // kUriPanIdQuery @@ -126,6 +131,11 @@ static_assert(AreConstStringsEqual(kEntries[kUriJoinerEntrust].mPath, "c/je"), " static_assert(AreConstStringsEqual(kEntries[kUriJoinerFinalize].mPath, "c/jf"), "kEntries is invalid"); static_assert(AreConstStringsEqual(kEntries[kUriLeaderKeepAlive].mPath, "c/la"), "kEntries is invalid"); static_assert(AreConstStringsEqual(kEntries[kUriLeaderPetition].mPath, "c/lp"), "kEntries is invalid"); +static_assert(AreConstStringsEqual(kEntries[kUriEnrollerJoinerAccept].mPath, "c/nj"), "kEntries is invalid"); +static_assert(AreConstStringsEqual(kEntries[kUriEnrollerKeepAlive].mPath, "c/nk"), "kEntries is invalid"); +static_assert(AreConstStringsEqual(kEntries[kUriEnrollerJoinerRelease].mPath, "c/nl"), "kEntries is invalid"); +static_assert(AreConstStringsEqual(kEntries[kUriEnrollerRegister].mPath, "c/nr"), "kEntries is invalid"); +static_assert(AreConstStringsEqual(kEntries[kUriEnrollerReportState].mPath, "c/ns"), "kEntries is invalid"); static_assert(AreConstStringsEqual(kEntries[kUriPanIdConflict].mPath, "c/pc"), "kEntries is invalid"); static_assert(AreConstStringsEqual(kEntries[kUriPendingGet].mPath, "c/pg"), "kEntries is invalid"); static_assert(AreConstStringsEqual(kEntries[kUriPanIdQuery].mPath, "c/pq"), "kEntries is invalid"); @@ -191,6 +201,11 @@ template <> const char *UriToString(void) { return "JoinerEnt template <> const char *UriToString(void) { return "JoinerFinalize"; } template <> const char *UriToString(void) { return "LeaderKeepAlive"; } template <> const char *UriToString(void) { return "LeaderPetition"; } +template <> const char *UriToString(void) { return "EnrollerJoinerAccept"; } +template <> const char *UriToString(void) { return "EnrollerKeepAlive"; } +template <> const char *UriToString(void) { return "EnrollerJoinerRelease"; } +template <> const char *UriToString(void) { return "EnrollerRegister"; } +template <> const char *UriToString(void) { return "EnrollerReportState"; } template <> const char *UriToString(void) { return "PanIdConflict"; } template <> const char *UriToString(void) { return "PendingGet"; } template <> const char *UriToString(void) { return "PanIdQuery"; } diff --git a/src/core/thread/uri_paths.hpp b/src/core/thread/uri_paths.hpp index 5d352d424..46efd9a91 100644 --- a/src/core/thread/uri_paths.hpp +++ b/src/core/thread/uri_paths.hpp @@ -70,6 +70,11 @@ enum Uri : uint8_t kUriJoinerFinalize, ///< Joiner Finalize ("c/jf") kUriLeaderKeepAlive, ///< Leader Keep Alive ("c/la") kUriLeaderPetition, ///< Leader Petition ("c/lp") + kUriEnrollerJoinerAccept, ///< Enroller Joiner Accept ("c/nj") + kUriEnrollerKeepAlive, ///< Enroller KeepAlive ("c/nk") + kUriEnrollerJoinerRelease, ///< Enroller Joiner Release ("c/nl") + kUriEnrollerRegister, ///< Enroller Register ("c/nr") + kUriEnrollerReportState, ///< Report Admitter State (to enroller) ("c/ns") kUriPanIdConflict, ///< PAN ID Conflict ("c/pc") kUriPendingGet, ///< MGMT_PENDING_GET ("c/pg") kUriPanIdQuery, ///< PAN ID Query ("c/pq") @@ -144,6 +149,11 @@ template <> const char *UriToString(void); template <> const char *UriToString(void); template <> const char *UriToString(void); template <> const char *UriToString(void); +template <> const char *UriToString(void); +template <> const char *UriToString(void); +template <> const char *UriToString(void); +template <> const char *UriToString(void); +template <> const char *UriToString(void); template <> const char *UriToString(void); template <> const char *UriToString(void); template <> const char *UriToString(void); diff --git a/tests/nexus/CMakeLists.txt b/tests/nexus/CMakeLists.txt index b0cee5a94..383644e20 100644 --- a/tests/nexus/CMakeLists.txt +++ b/tests/nexus/CMakeLists.txt @@ -132,6 +132,7 @@ ot_nexus_test(5_1_13 "cert;nexus") ot_nexus_test(5_2_1 "cert;nexus") # Misc tests +ot_nexus_test(border_admitter "core;nexus") ot_nexus_test(border_agent "core;nexus") ot_nexus_test(border_agent_tracker "core;nexus") ot_nexus_test(discover_scan "core;nexus") diff --git a/tests/nexus/openthread-core-nexus-config.h b/tests/nexus/openthread-core-nexus-config.h index db5ad273d..754d6113b 100644 --- a/tests/nexus/openthread-core-nexus-config.h +++ b/tests/nexus/openthread-core-nexus-config.h @@ -44,8 +44,11 @@ #define OPENTHREAD_CONFIG_BACKBONE_ROUTER_ENABLE 1 #define OPENTHREAD_CONFIG_BLE_TCAT_ENABLE 0 #define OPENTHREAD_CONFIG_BORDER_AGENT_ENABLE 1 +#define OPENTHREAD_CONFIG_BORDER_AGENT_ADMITTER_ENABLE 1 +#define OPENTHREAD_CONFIG_BORDER_AGENT_ADMITTER_ENABLED_BY_DEFAULT 0 #define OPENTHREAD_CONFIG_BORDER_AGENT_EPHEMERAL_KEY_ENABLE 1 #define OPENTHREAD_CONFIG_BORDER_AGENT_ID_ENABLE 1 +#define OPENTHREAD_CONFIG_BORDER_AGENT_COMMISSIONER_EVICTION_API_ENABLE 1 #define OPENTHREAD_CONFIG_BORDER_AGENT_TRACKER_ENABLE 1 #define OPENTHREAD_CONFIG_BORDER_AGENT_TXT_DATA_PARSER_ENABLE 1 #define OPENTHREAD_CONFIG_BORDER_ROUTER_ENABLE 1 diff --git a/tests/nexus/test_border_admitter.cpp b/tests/nexus/test_border_admitter.cpp new file mode 100644 index 000000000..80b8b0d9c --- /dev/null +++ b/tests/nexus/test_border_admitter.cpp @@ -0,0 +1,3343 @@ +/* + * Copyright (c) 2025, The OpenThread Authors. + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * 3. Neither the name of the copyright holder nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ + +#include +#include +#include + +#include "platform/nexus_core.hpp" +#include "platform/nexus_node.hpp" + +namespace ot { +namespace Nexus { + +using Admitter = MeshCoP::BorderAgent::Admitter; +using Manager = MeshCoP::BorderAgent::Manager; +using Commissioner = MeshCoP::Commissioner; +using Joiner = MeshCoP::Joiner; + +static LogLevel kLogLevel = kLogLevelCrit; + +void TestBorderAdmitterPrimeSelection(void) +{ + Core nexus; + Node &leader = nexus.CreateNode(); + Node &node1 = nexus.CreateNode(); + Node &node2 = nexus.CreateNode(); + Node &node3 = nexus.CreateNode(); + Node *nodes[] = {&node1, &node2, &node3}; + NetworkData::Service::Iterator netDataIter(leader.GetInstance()); + uint16_t rloc16; + bool found; + + Log("------------------------------------------------------------------------------------------------------"); + Log("TestBorderAdmitterPrimeSelection"); + + nexus.AdvanceTime(0); + + leader.GetInstance().SetLogLevel(kLogLevel); + + // Form the topology. + + leader.Form(); + nexus.AdvanceTime(50 * Time::kOneSecondInMsec); + node1.Join(leader); + node2.Join(leader); + node3.Join(leader); + + nexus.AdvanceTime(10 * Time::kOneMinuteInMsec); + + VerifyOrQuit(leader.Get().IsLeader()); + VerifyOrQuit(node1.Get().IsRouter()); + VerifyOrQuit(node2.Get().IsRouter()); + VerifyOrQuit(node3.Get().IsRouter()); + + // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + Log("Check Border Admitter initial state"); + + VerifyOrQuit(!node1.Get().IsEnabled()); + VerifyOrQuit(!node2.Get().IsEnabled()); + VerifyOrQuit(!node3.Get().IsEnabled()); + + // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + Log("Enable Admitter role on `node1` and validate that it becomes the Prime Admitter"); + + node1.Get().SetEnabled(true); + VerifyOrQuit(node1.Get().IsEnabled()); + + nexus.AdvanceTime(45 * Time::kOneSecondInMsec); + + VerifyOrQuit(node1.Get().IsPrimeAdmitter()); + VerifyOrQuit(!node1.Get().IsActiveCommissioner()); + + // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + Log("Validate that the NetworkData contains a single Admitter Service from `node1`"); + + netDataIter.Reset(); + + SuccessOrQuit(netDataIter.GetNextBorderAdmitterInfo(rloc16)); + VerifyOrQuit(rloc16 == node1.Get().GetRloc16()); + + VerifyOrQuit(netDataIter.GetNextBorderAdmitterInfo(rloc16) == kErrorNotFound); + + // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + Log("Enable Admitter role on `node2` & `node3` and validate that `node1` remains the Prime Admitter"); + + node2.Get().SetEnabled(true); + VerifyOrQuit(node2.Get().IsEnabled()); + + node3.Get().SetEnabled(true); + VerifyOrQuit(node3.Get().IsEnabled()); + + nexus.AdvanceTime(45 * Time::kOneSecondInMsec); + + VerifyOrQuit(node1.Get().IsPrimeAdmitter()); + VerifyOrQuit(!node1.Get().IsActiveCommissioner()); + + VerifyOrQuit(!node2.Get().IsPrimeAdmitter()); + VerifyOrQuit(!node3.Get().IsPrimeAdmitter()); + + // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + Log("Validate that the NetworkData contains the Admitter Service from `node1`"); + + netDataIter.Reset(); + + SuccessOrQuit(netDataIter.GetNextBorderAdmitterInfo(rloc16)); + VerifyOrQuit(rloc16 == node1.Get().GetRloc16()); + + VerifyOrQuit(netDataIter.GetNextBorderAdmitterInfo(rloc16) == kErrorNotFound); + + // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + Log("Disable Admitter role on `node1` and check that another Prime Admitter is elected"); + + node1.Get().SetEnabled(false); + VerifyOrQuit(!node1.Get().IsEnabled()); + VerifyOrQuit(!node1.Get().IsPrimeAdmitter()); + + nexus.AdvanceTime(75 * Time::kOneSecondInMsec); + + // We use `!=` to do an "exclusive or" logic check (either node2 or node3 is prime and not both) + VerifyOrQuit(node2.Get().IsPrimeAdmitter() != node3.Get().IsPrimeAdmitter()); + + VerifyOrQuit(!node2.Get().IsActiveCommissioner()); + VerifyOrQuit(!node3.Get().IsActiveCommissioner()); + + // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + Log("Validate that the NetworkData contains a single Admitter Service entry"); + + netDataIter.Reset(); + + SuccessOrQuit(netDataIter.GetNextBorderAdmitterInfo(rloc16)); + + VerifyOrQuit(rloc16 == (node2.Get().IsPrimeAdmitter() ? node2.Get().GetRloc16() + : node3.Get().GetRloc16())); + + VerifyOrQuit(netDataIter.GetNextBorderAdmitterInfo(rloc16) == kErrorNotFound); + + // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + Log("Disable Admitter role on all nodes (`node2` and `node3`)"); + + node2.Get().SetEnabled(false); + VerifyOrQuit(!node2.Get().IsEnabled()); + VerifyOrQuit(!node2.Get().IsPrimeAdmitter()); + + node3.Get().SetEnabled(false); + VerifyOrQuit(!node3.Get().IsEnabled()); + VerifyOrQuit(!node3.Get().IsPrimeAdmitter()); + + nexus.AdvanceTime(5 * Time::kOneSecondInMsec); + + // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + Log("Validate that the NetworkData contains no Admitter Service entry"); + + netDataIter.Reset(); + VerifyOrQuit(netDataIter.GetNextBorderAdmitterInfo(rloc16) == kErrorNotFound); + + nexus.AdvanceTime(10 * Time::kOneSecondInMsec); + + // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + Log("Enable Admitter role on all 3 nodes at the same time"); + + node1.Get().SetEnabled(true); + node2.Get().SetEnabled(true); + node3.Get().SetEnabled(true); + + VerifyOrQuit(node1.Get().IsEnabled()); + VerifyOrQuit(node2.Get().IsEnabled()); + VerifyOrQuit(node3.Get().IsEnabled()); + + nexus.AdvanceTime(75 * Time::kOneSecondInMsec); + + Log("Validate that we end up with a single Prime Admitter"); + + found = false; + + for (Node *node : nodes) + { + if (node->Get().IsPrimeAdmitter()) + { + VerifyOrQuit(!found); + found = true; + rloc16 = node->Get().GetRloc16(); + } + + VerifyOrQuit(!node->Get().IsActiveCommissioner()); + } + + VerifyOrQuit(found); +} + +//--------------------------------------------------------------------------------------------------------------------- + +enum AdmitterState : uint8_t +{ + kAdmitterUnavailable = 0, + kAdmitterReady = 1, + kAdmitterActive = 2, + kAdmitterConflictError = 3, +}; + +const char *AdmitterStateToString(uint8_t aState) +{ + const char *str = "Unknown"; + + switch (aState) + { + case kAdmitterUnavailable: + str = "AdmitterUnavailable"; + break; + case kAdmitterReady: + str = "AdmitterReady"; + break; + case kAdmitterActive: + str = "AdmitterActive"; + break; + case kAdmitterConflictError: + str = "AdmitterConflictError"; + break; + default: + break; + } + return str; +} + +//--------------------------------------------------------------------------------------------------------------------- + +struct AdmitterInfo +{ + // Tracks information about Admitter from received + // messages on an Enroller. + + void ParseAdmitterInfo(const Coap::Message &aResponse) + { + Error error; + + error = Tlv::Find(aResponse, mAdmitterState); + VerifyOrQuit(error == kErrorNone || error == kErrorNotFound); + mHasAdmitterState = (error == kErrorNone); + + error = Tlv::Find(aResponse, mCommrSessionId); + VerifyOrQuit(error == kErrorNone || error == kErrorNotFound); + mHasCommrSessionId = (error == kErrorNone); + + error = Tlv::Find(aResponse, mJoinerUdp); + VerifyOrQuit(error == kErrorNone || error == kErrorNotFound); + mHasJoinerUdp = (error == kErrorNone); + } + + bool mHasAdmitterState; + bool mHasCommrSessionId; + bool mHasJoinerUdp; + uint8_t mResponseState; + uint8_t mAdmitterState; + uint16_t mCommrSessionId; + uint16_t mJoinerUdp; +}; + +struct ResponseContext : public AdmitterInfo, public Clearable +{ + // Tracks information in a TMF response received by an enroller. + // (populated by `HandleResponse()`). + + bool mReceived; +}; + +void HandleResponse(void *aContext, Coap::Msg *aMsg, Error aResult) +{ + ResponseContext *responseContext; + + VerifyOrQuit(aContext != nullptr); + VerifyOrQuit(aMsg != nullptr); + + responseContext = static_cast(aContext); + + VerifyOrQuit(!responseContext->mReceived); // Duplicate response + responseContext->mReceived = true; + + SuccessOrQuit(Tlv::Find(aMsg->mMessage, responseContext->mResponseState)); + + responseContext->ParseAdmitterInfo(aMsg->mMessage); + + Log(" Received response - %s", + MeshCoP::StateTlv::StateToString(static_cast(responseContext->mResponseState))); +} + +//--------------------------------------------------------------------------------------------------------------------- + +struct ReceiveContext +{ + // Tracks information from received TMF on an Enroller send by + // Admitter. + + static constexpr uint8_t kMaxEntries = 16; + + void Clear(void) + { + mStateReports.Clear(); + mRelayRxMsgs.DequeueAndFreeAll(); + mProxyRxMsgs.DequeueAndFreeAll(); + } + + bool HasReceivedReportState(void) const { return !mStateReports.IsEmpty(); } + uint8_t GetLastReportedAdmitterState(void) { return mStateReports.Back()->mAdmitterState; } + + Array mStateReports; + MessageQueue mRelayRxMsgs; + MessageQueue mProxyRxMsgs; +}; + +bool HandleResource(void *aContext, Uri aUri, Coap::Msg &aMsg) +{ + bool didHandle = false; + ReceiveContext *recvContext; + AdmitterInfo *info; + Message *msgClone; + uint16_t joinerPort; + uint16_t joinerRouterRloc; + Ip6::InterfaceIdentifier joinerIid; + + VerifyOrQuit(aContext != nullptr); + recvContext = static_cast(aContext); + + switch (aUri) + { + case kUriEnrollerReportState: + didHandle = true; + info = recvContext->mStateReports.PushBack(); + VerifyOrQuit(info != nullptr); + info->ParseAdmitterInfo(aMsg.mMessage); + VerifyOrQuit(info->mHasAdmitterState); + Log(" Received `EnrollerReportState` with state %s", AdmitterStateToString(info->mAdmitterState)); + break; + + case kUriRelayRx: + SuccessOrQuit(Tlv::Find(aMsg.mMessage, joinerPort)); + SuccessOrQuit(Tlv::Find(aMsg.mMessage, joinerIid)); + Log(" Received `RelayRx` from joiner - port:%u iid:%s", joinerPort, joinerIid.ToString().AsCString()); + + msgClone = aMsg.mMessage.Clone(); + VerifyOrQuit(msgClone != nullptr); + recvContext->mRelayRxMsgs.Enqueue(*msgClone); + break; + + case kUriProxyRx: + Log(" Received `ProxyRx`"); + msgClone = aMsg.mMessage.Clone(); + VerifyOrQuit(msgClone != nullptr); + recvContext->mProxyRxMsgs.Enqueue(*msgClone); + break; + default: + Log(" Received unexpected URI %u", aUri); + break; + } + +exit: + return didHandle; +} + +//--------------------------------------------------------------------------------------------------------------------- + +void TestBorderAdmitterEnrollerInteraction(void) +{ + static const char kEnrollerId[] = "en00"; + static const char kEnrollerIdAlt[] = "en01"; + + static const uint8_t kEnrollerTimeoutInSec = 50; + + Core nexus; + Node &admitter = nexus.CreateNode(); + Node &enroller = nexus.CreateNode(); + Ip6::SockAddr sockAddr; + Pskc pskc; + Admitter::Iterator iter; + Admitter::EnrollerInfo enrollerInfo; + Admitter::JoinerInfo joinerInfo; + Coap::Message *message; + uint8_t mode; + MeshCoP::SteeringData steeringData; + MeshCoP::SteeringData leaderSteeringData; + ResponseContext responseContext; + ReceiveContext recvContext; + Manager::SessionInfo *sessionInfo; + uint16_t rloc16; + uint16_t sessionId; + + Log("------------------------------------------------------------------------------------------------------"); + Log("TestBorderAdmitterEnrollerInteraction"); + + nexus.AdvanceTime(0); + + // Form the topology: + // - `admitter` forms its own network (acting as leader) + // - `enroller` stays disconnected. + + admitter.Form(); + nexus.AdvanceTime(50 * Time::kOneSecondInMsec); + VerifyOrQuit(admitter.Get().IsLeader()); + + SuccessOrQuit(enroller.Get().SetPanChannel(admitter.Get().GetPanChannel())); + enroller.Get().SetPanId(admitter.Get().GetPanId()); + enroller.Get().Up(); + + // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + Log("Enable Border Admitter on `admitter`"); + + admitter.Get().SetEnabled(true); + VerifyOrQuit(admitter.Get().IsEnabled()); + VerifyOrQuit(!admitter.Get().IsPrimeAdmitter()); + + nexus.AdvanceTime(45 * Time::kOneSecondInMsec); + + VerifyOrQuit(admitter.Get().IsPrimeAdmitter()); + VerifyOrQuit(!admitter.Get().IsActiveCommissioner()); + + SuccessOrQuit(admitter.Get().AddUnsecurePort(admitter.Get().GetUdpPort())); + + // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + Log("Establish a DTLS connection from `enroller` to `admitter`"); + + sockAddr.SetAddress(admitter.Get().GetLinkLocalAddress()); + sockAddr.SetPort(admitter.Get().GetUdpPort()); + + admitter.Get().GetPskc(pskc); + SuccessOrQuit(enroller.Get().SetPsk(pskc.m8, Pskc::kSize)); + + enroller.Get().RegisterResourceHandler(HandleResource, &recvContext); + + SuccessOrQuit(enroller.Get().Open()); + SuccessOrQuit(enroller.Get().Connect(sockAddr)); + + nexus.AdvanceTime(Time::kOneSecondInMsec); + + VerifyOrQuit(enroller.Get().IsConnected()); + + // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + Log("Check that Enroller session list on `admitter` is empty"); + + iter.Init(admitter.GetInstance()); + VerifyOrQuit(iter.GetNextEnrollerInfo(enrollerInfo) == kErrorNotFound); + VerifyOrQuit(iter.GetNextJoinerInfo(joinerInfo) == kErrorNotFound); + + // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + Log("Send an `EnrollerRegister` message from `enroller` to `admitter`"); + + message = enroller.Get().NewPriorityConfirmablePostMessage(kUriEnrollerRegister); + VerifyOrQuit(message != nullptr); + + mode = MeshCoP::EnrollerModeTlv::kForwardJoinerRelayRx | MeshCoP::EnrollerModeTlv::kForwardUdpProxyRx; + + steeringData.SetToPermitAllJoiners(); + + SuccessOrQuit(Tlv::Append(*message, kEnrollerId)); + SuccessOrQuit(Tlv::Append(*message, mode)); + SuccessOrQuit(Tlv::Append(*message, steeringData.GetData(), steeringData.GetLength())); + + responseContext.Clear(); + SuccessOrQuit(enroller.Get().SendMessage(*message, HandleResponse, &responseContext)); + + nexus.AdvanceTime(Time::kOneSecondInMsec); + + VerifyOrQuit(responseContext.mReceived); + VerifyOrQuit(responseContext.mResponseState == MeshCoP::StateTlv::kAccept); + VerifyOrQuit(responseContext.mHasAdmitterState); + + // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + Log("Validate the enroller list on `admitter`"); + + iter.Init(admitter.GetInstance()); + SuccessOrQuit(iter.GetNextEnrollerInfo(enrollerInfo)); + + sessionInfo = &enrollerInfo.mSessionInfo; + VerifyOrQuit(sessionInfo->mIsConnected); + VerifyOrQuit(!sessionInfo->mIsCommissioner); + VerifyOrQuit(enroller.Get().HasUnicastAddress(AsCoreType(&sessionInfo->mPeerSockAddr.mAddress))); + + VerifyOrQuit(StringMatch(enrollerInfo.mId, kEnrollerId)); + VerifyOrQuit(AsCoreType(&enrollerInfo.mSteeringData) == steeringData); + VerifyOrQuit(enrollerInfo.mMode == mode); + + VerifyOrQuit(iter.GetNextJoinerInfo(joinerInfo) == kErrorNotFound); + + VerifyOrQuit(iter.GetNextEnrollerInfo(enrollerInfo) == kErrorNotFound); + + // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + Log("Validate that `admitter` becomes active commissioner"); + + nexus.AdvanceTime(Time::kOneSecondInMsec); + + VerifyOrQuit(admitter.Get().IsPrimeAdmitter()); + VerifyOrQuit(admitter.Get().IsActiveCommissioner()); + + // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + Log("Validate that `EnrollerReportState` is received with the updated Admitter state"); + + VerifyOrQuit(recvContext.HasReceivedReportState()); + VerifyOrQuit(recvContext.GetLastReportedAdmitterState() == kAdmitterActive); + + // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + Log("Validate that commissioner steering data and session ID are properly set"); + + SuccessOrQuit(admitter.Get().FindBorderAgentRloc(rloc16)); + VerifyOrQuit(rloc16 == admitter.Get().GetRloc16()); + + SuccessOrQuit(admitter.Get().FindCommissioningSessionId(sessionId)); + VerifyOrQuit(sessionId == admitter.Get().GetCommissionerSessionId()); + + SuccessOrQuit(admitter.Get().FindSteeringData(leaderSteeringData)); + VerifyOrQuit(leaderSteeringData == steeringData); + + // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + Log("Ensure no changes before enroller timeout"); + + recvContext.Clear(); + + nexus.AdvanceTime((kEnrollerTimeoutInSec - 3) * Time::kOneSecondInMsec); + + VerifyOrQuit(!recvContext.HasReceivedReportState()); + + VerifyOrQuit(admitter.Get().IsPrimeAdmitter()); + VerifyOrQuit(admitter.Get().IsActiveCommissioner()); + + iter.Init(admitter.GetInstance()); + SuccessOrQuit(iter.GetNextEnrollerInfo(enrollerInfo)); + + VerifyOrQuit(StringMatch(enrollerInfo.mId, kEnrollerId)); + VerifyOrQuit(AsCoreType(&enrollerInfo.mSteeringData) == steeringData); + VerifyOrQuit(enrollerInfo.mMode == mode); + + VerifyOrQuit(enrollerInfo.mRegisterDuration >= 48 * Time::kOneSecondInMsec); + + VerifyOrQuit(iter.GetNextJoinerInfo(joinerInfo) == kErrorNotFound); + + VerifyOrQuit(iter.GetNextEnrollerInfo(enrollerInfo) == kErrorNotFound); + + // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + Log("Send an `EnrollerKeepAlive` message"); + + message = enroller.Get().NewPriorityConfirmablePostMessage(kUriEnrollerKeepAlive); + VerifyOrQuit(message != nullptr); + + SuccessOrQuit(Tlv::Append(*message, MeshCoP::StateTlv::kAccept)); + + responseContext.Clear(); + SuccessOrQuit(enroller.Get().SendMessage(*message, HandleResponse, &responseContext)); + + nexus.AdvanceTime(Time::kOneSecondInMsec); + + VerifyOrQuit(responseContext.mReceived); + VerifyOrQuit(responseContext.mResponseState == MeshCoP::StateTlv::kAccept); + VerifyOrQuit(responseContext.mHasAdmitterState); + VerifyOrQuit(responseContext.mAdmitterState == kAdmitterActive); + + iter.Init(admitter.GetInstance()); + SuccessOrQuit(iter.GetNextEnrollerInfo(enrollerInfo)); + + VerifyOrQuit(StringMatch(enrollerInfo.mId, kEnrollerId)); + VerifyOrQuit(AsCoreType(&enrollerInfo.mSteeringData) == steeringData); + VerifyOrQuit(enrollerInfo.mMode == mode); + + VerifyOrQuit(iter.GetNextJoinerInfo(joinerInfo) == kErrorNotFound); + + VerifyOrQuit(iter.GetNextEnrollerInfo(enrollerInfo) == kErrorNotFound); + VerifyOrQuit(iter.GetNextJoinerInfo(joinerInfo) == kErrorNotFound); + + VerifyOrQuit(!recvContext.HasReceivedReportState()); + + // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + Log("Wait until just before the timeout and validate that `admitter` remains active commissioner"); + + nexus.AdvanceTime((kEnrollerTimeoutInSec - 2) * Time::kOneSecondInMsec); + + VerifyOrQuit(!recvContext.HasReceivedReportState()); + + VerifyOrQuit(admitter.Get().IsPrimeAdmitter()); + VerifyOrQuit(admitter.Get().IsActiveCommissioner()); + + iter.Init(admitter.GetInstance()); + SuccessOrQuit(iter.GetNextEnrollerInfo(enrollerInfo)); + VerifyOrQuit(StringMatch(enrollerInfo.mId, kEnrollerId)); + VerifyOrQuit(iter.GetNextEnrollerInfo(enrollerInfo) == kErrorNotFound); + + // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + Log("Send an `EnrollerKeepAlive` message with an Enroller Mode TLV changing the mode"); + + message = enroller.Get().NewPriorityConfirmablePostMessage(kUriEnrollerKeepAlive); + VerifyOrQuit(message != nullptr); + + mode = MeshCoP::EnrollerModeTlv::kForwardJoinerRelayRx; + + SuccessOrQuit(Tlv::Append(*message, MeshCoP::StateTlv::kAccept)); + SuccessOrQuit(Tlv::Append(*message, mode)); + + responseContext.Clear(); + SuccessOrQuit(enroller.Get().SendMessage(*message, HandleResponse, &responseContext)); + + nexus.AdvanceTime(Time::kOneSecondInMsec); + + VerifyOrQuit(responseContext.mReceived); + VerifyOrQuit(responseContext.mResponseState == MeshCoP::StateTlv::kAccept); + VerifyOrQuit(responseContext.mHasAdmitterState); + VerifyOrQuit(responseContext.mAdmitterState == kAdmitterActive); + + iter.Init(admitter.GetInstance()); + SuccessOrQuit(iter.GetNextEnrollerInfo(enrollerInfo)); + + VerifyOrQuit(StringMatch(enrollerInfo.mId, kEnrollerId)); + VerifyOrQuit(AsCoreType(&enrollerInfo.mSteeringData) == steeringData); + VerifyOrQuit(enrollerInfo.mMode == mode); + + VerifyOrQuit(iter.GetNextEnrollerInfo(enrollerInfo) == kErrorNotFound); + + VerifyOrQuit(!recvContext.HasReceivedReportState()); + + nexus.AdvanceTime((kEnrollerTimeoutInSec - 2) * Time::kOneSecondInMsec); + + VerifyOrQuit(!recvContext.HasReceivedReportState()); + + VerifyOrQuit(admitter.Get().IsPrimeAdmitter()); + VerifyOrQuit(admitter.Get().IsActiveCommissioner()); + + // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + Log("Send an `EnrollerKeepAlive` message with Steering Data TLV"); + + message = enroller.Get().NewPriorityConfirmablePostMessage(kUriEnrollerKeepAlive); + VerifyOrQuit(message != nullptr); + + SuccessOrQuit(steeringData.Init(MeshCoP::SteeringData::kMaxLength)); + SuccessOrQuit(steeringData.UpdateBloomFilter(admitter.Get().GetExtAddress())); + + SuccessOrQuit(Tlv::Append(*message, MeshCoP::StateTlv::kAccept)); + SuccessOrQuit(Tlv::Append(*message, steeringData.GetData(), steeringData.GetLength())); + + responseContext.Clear(); + SuccessOrQuit(enroller.Get().SendMessage(*message, HandleResponse, &responseContext)); + + nexus.AdvanceTime(Time::kOneSecondInMsec); + + VerifyOrQuit(responseContext.mReceived); + VerifyOrQuit(responseContext.mResponseState == MeshCoP::StateTlv::kAccept); + VerifyOrQuit(responseContext.mHasAdmitterState); + VerifyOrQuit(responseContext.mAdmitterState == kAdmitterActive); + + iter.Init(admitter.GetInstance()); + SuccessOrQuit(iter.GetNextEnrollerInfo(enrollerInfo)); + + VerifyOrQuit(StringMatch(enrollerInfo.mId, kEnrollerId)); + VerifyOrQuit(AsCoreType(&enrollerInfo.mSteeringData) == steeringData); + VerifyOrQuit(enrollerInfo.mMode == mode); + + VerifyOrQuit(iter.GetNextEnrollerInfo(enrollerInfo) == kErrorNotFound); + + // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + Log("Validate Network Data is updated with the new Steering Data"); + + nexus.AdvanceTime(Time::kOneSecondInMsec); + + VerifyOrQuit(!recvContext.HasReceivedReportState()); + + VerifyOrQuit(admitter.Get().IsPrimeAdmitter()); + VerifyOrQuit(admitter.Get().IsActiveCommissioner()); + + SuccessOrQuit(admitter.Get().FindBorderAgentRloc(rloc16)); + VerifyOrQuit(rloc16 == admitter.Get().GetRloc16()); + + SuccessOrQuit(admitter.Get().FindCommissioningSessionId(sessionId)); + VerifyOrQuit(sessionId == admitter.Get().GetCommissionerSessionId()); + + SuccessOrQuit(admitter.Get().FindSteeringData(leaderSteeringData)); + VerifyOrQuit(leaderSteeringData == steeringData); + + // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + Log("Validate the enroller timeout"); + + // Already 2 seconds has passed since sending last `EnrollerKeepAlive` + nexus.AdvanceTime((kEnrollerTimeoutInSec - 2) * Time::kOneSecondInMsec - 50); + + VerifyOrQuit(!recvContext.HasReceivedReportState()); + + VerifyOrQuit(admitter.Get().IsPrimeAdmitter()); + VerifyOrQuit(admitter.Get().IsActiveCommissioner()); + + iter.Init(admitter.GetInstance()); + SuccessOrQuit(iter.GetNextEnrollerInfo(enrollerInfo)); + VerifyOrQuit(StringMatch(enrollerInfo.mId, kEnrollerId)); + VerifyOrQuit(iter.GetNextEnrollerInfo(enrollerInfo) == kErrorNotFound); + + nexus.AdvanceTime(75); + + iter.Init(admitter.GetInstance()); + VerifyOrQuit(iter.GetNextEnrollerInfo(enrollerInfo) == kErrorNotFound); + + // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + Log("Validate that the `admitter` resigns from being active commissioner"); + + nexus.AdvanceTime(Time::kOneSecondInMsec); + + VerifyOrQuit(admitter.Get().IsPrimeAdmitter()); + VerifyOrQuit(!admitter.Get().IsActiveCommissioner()); + + VerifyOrQuit(admitter.Get().FindBorderAgentRloc(rloc16) == kErrorNotFound); + + nexus.AdvanceTime(10 * Time::kOneSecondInMsec); + + VerifyOrQuit(!enroller.Get().IsConnected()); + + // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + Log("Re-establish DTLS session"); + + SuccessOrQuit(enroller.Get().Connect(sockAddr)); + nexus.AdvanceTime(Time::kOneSecondInMsec); + VerifyOrQuit(enroller.Get().IsConnected()); + + // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + Log("Register as enroller again"); + + message = enroller.Get().NewPriorityConfirmablePostMessage(kUriEnrollerRegister); + VerifyOrQuit(message != nullptr); + + SuccessOrQuit(Tlv::Append(*message, kEnrollerId)); + SuccessOrQuit(Tlv::Append(*message, mode)); + SuccessOrQuit(Tlv::Append(*message, steeringData.GetData(), steeringData.GetLength())); + + responseContext.Clear(); + SuccessOrQuit(enroller.Get().SendMessage(*message, HandleResponse, &responseContext)); + + nexus.AdvanceTime(Time::kOneSecondInMsec); + + VerifyOrQuit(responseContext.mReceived); + VerifyOrQuit(responseContext.mResponseState == MeshCoP::StateTlv::kAccept); + VerifyOrQuit(responseContext.mHasAdmitterState); + + iter.Init(admitter.GetInstance()); + SuccessOrQuit(iter.GetNextEnrollerInfo(enrollerInfo)); + VerifyOrQuit(StringMatch(enrollerInfo.mId, kEnrollerId)); + VerifyOrQuit(AsCoreType(&enrollerInfo.mSteeringData) == steeringData); + VerifyOrQuit(enrollerInfo.mMode == mode); + VerifyOrQuit(iter.GetNextEnrollerInfo(enrollerInfo) == kErrorNotFound); + + // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + Log("Validate that `admitter` becomes active commissioner again"); + + nexus.AdvanceTime(Time::kOneSecondInMsec); + + VerifyOrQuit(admitter.Get().IsPrimeAdmitter()); + VerifyOrQuit(admitter.Get().IsActiveCommissioner()); + + SuccessOrQuit(admitter.Get().FindBorderAgentRloc(rloc16)); + VerifyOrQuit(rloc16 == admitter.Get().GetRloc16()); + + SuccessOrQuit(admitter.Get().FindCommissioningSessionId(sessionId)); + VerifyOrQuit(sessionId == admitter.Get().GetCommissionerSessionId()); + + SuccessOrQuit(admitter.Get().FindSteeringData(leaderSteeringData)); + VerifyOrQuit(leaderSteeringData == steeringData); + + nexus.AdvanceTime((kEnrollerTimeoutInSec / 2) * Time::kOneSecondInMsec); + + // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + Log("Send an `EnrollerKeepAlive` message from `enroller` with `kReject` status, resigning enroller role"); + + VerifyOrQuit(admitter.Get().IsPrimeAdmitter()); + VerifyOrQuit(admitter.Get().IsActiveCommissioner()); + + message = enroller.Get().NewPriorityConfirmablePostMessage(kUriEnrollerKeepAlive); + VerifyOrQuit(message != nullptr); + + SuccessOrQuit(Tlv::Append(*message, MeshCoP::StateTlv::kReject)); + + responseContext.Clear(); + SuccessOrQuit(enroller.Get().SendMessage(*message, HandleResponse, &responseContext)); + + nexus.AdvanceTime(Time::kOneSecondInMsec); + + VerifyOrQuit(responseContext.mReceived); + VerifyOrQuit(responseContext.mResponseState == MeshCoP::StateTlv::kReject); + + iter.Init(admitter.GetInstance()); + VerifyOrQuit(iter.GetNextEnrollerInfo(enrollerInfo) == kErrorNotFound); + + nexus.AdvanceTime(Time::kOneSecondInMsec); + + VerifyOrQuit(admitter.Get().IsPrimeAdmitter()); + VerifyOrQuit(!admitter.Get().IsActiveCommissioner()); + + VerifyOrQuit(enroller.Get().IsConnected()); + + // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + Log("Register as enroller again"); + + message = enroller.Get().NewPriorityConfirmablePostMessage(kUriEnrollerRegister); + VerifyOrQuit(message != nullptr); + + SuccessOrQuit(Tlv::Append(*message, kEnrollerId)); + SuccessOrQuit(Tlv::Append(*message, mode)); + SuccessOrQuit(Tlv::Append(*message, steeringData.GetData(), steeringData.GetLength())); + + responseContext.Clear(); + SuccessOrQuit(enroller.Get().SendMessage(*message, HandleResponse, &responseContext)); + + nexus.AdvanceTime(Time::kOneSecondInMsec); + + VerifyOrQuit(responseContext.mReceived); + VerifyOrQuit(responseContext.mResponseState == MeshCoP::StateTlv::kAccept); + VerifyOrQuit(responseContext.mHasAdmitterState); + + iter.Init(admitter.GetInstance()); + SuccessOrQuit(iter.GetNextEnrollerInfo(enrollerInfo)); + VerifyOrQuit(StringMatch(enrollerInfo.mId, kEnrollerId)); + VerifyOrQuit(AsCoreType(&enrollerInfo.mSteeringData) == steeringData); + VerifyOrQuit(enrollerInfo.mMode == mode); + VerifyOrQuit(iter.GetNextEnrollerInfo(enrollerInfo) == kErrorNotFound); + + nexus.AdvanceTime((kEnrollerTimeoutInSec / 2) * Time::kOneSecondInMsec); + + VerifyOrQuit(admitter.Get().IsPrimeAdmitter()); + VerifyOrQuit(admitter.Get().IsActiveCommissioner()); + + // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + Log("Send an `EnrollerRegister` message while already registered, with different parameters"); + + message = enroller.Get().NewPriorityConfirmablePostMessage(kUriEnrollerRegister); + VerifyOrQuit(message != nullptr); + + mode = 0; + SuccessOrQuit(steeringData.Init(8)); + SuccessOrQuit(steeringData.UpdateBloomFilter(admitter.Get().GetExtAddress())); + + SuccessOrQuit(Tlv::Append(*message, kEnrollerIdAlt)); + SuccessOrQuit(Tlv::Append(*message, mode)); + SuccessOrQuit(Tlv::Append(*message, steeringData.GetData(), steeringData.GetLength())); + + responseContext.Clear(); + SuccessOrQuit(enroller.Get().SendMessage(*message, HandleResponse, &responseContext)); + + nexus.AdvanceTime(Time::kOneSecondInMsec); + + VerifyOrQuit(responseContext.mReceived); + VerifyOrQuit(responseContext.mResponseState == MeshCoP::StateTlv::kAccept); + + VerifyOrQuit(admitter.Get().IsPrimeAdmitter()); + VerifyOrQuit(admitter.Get().IsActiveCommissioner()); + + Log("Validate that enroller info is updated accordingly"); + + iter.Init(admitter.GetInstance()); + SuccessOrQuit(iter.GetNextEnrollerInfo(enrollerInfo)); + + VerifyOrQuit(StringMatch(enrollerInfo.mId, kEnrollerIdAlt)); + VerifyOrQuit(AsCoreType(&enrollerInfo.mSteeringData) == steeringData); + VerifyOrQuit(enrollerInfo.mMode == mode); + + VerifyOrQuit(iter.GetNextEnrollerInfo(enrollerInfo) == kErrorNotFound); + + // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + Log("Validate that the Network Data (Commissioner Data) is also updated"); + + nexus.AdvanceTime(Time::kOneSecondInMsec); + + SuccessOrQuit(admitter.Get().FindBorderAgentRloc(rloc16)); + VerifyOrQuit(rloc16 == admitter.Get().GetRloc16()); + + SuccessOrQuit(admitter.Get().FindCommissioningSessionId(sessionId)); + VerifyOrQuit(sessionId == admitter.Get().GetCommissionerSessionId()); + + SuccessOrQuit(admitter.Get().FindSteeringData(leaderSteeringData)); + VerifyOrQuit(leaderSteeringData == steeringData); + + // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + Log("Validate that the `EnrollerRegister` extended the keep-alive timeout"); + + recvContext.Clear(); + + nexus.AdvanceTime((kEnrollerTimeoutInSec - 3) * Time::kOneSecondInMsec); + + VerifyOrQuit(!recvContext.HasReceivedReportState()); + + VerifyOrQuit(admitter.Get().IsPrimeAdmitter()); + VerifyOrQuit(admitter.Get().IsActiveCommissioner()); + + iter.Init(admitter.GetInstance()); + SuccessOrQuit(iter.GetNextEnrollerInfo(enrollerInfo)); + VerifyOrQuit(StringMatch(enrollerInfo.mId, kEnrollerIdAlt)); + VerifyOrQuit(iter.GetNextEnrollerInfo(enrollerInfo) == kErrorNotFound); + + // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + Log("Send an invalid `EnrollerKeepAlive` message without State TLV and validate that it is rejected"); + + message = enroller.Get().NewPriorityConfirmablePostMessage(kUriEnrollerKeepAlive); + VerifyOrQuit(message != nullptr); + + responseContext.Clear(); + SuccessOrQuit(enroller.Get().SendMessage(*message, HandleResponse, &responseContext)); + + nexus.AdvanceTime(Time::kOneSecondInMsec); + + VerifyOrQuit(responseContext.mReceived); + VerifyOrQuit(responseContext.mResponseState == MeshCoP::StateTlv::kReject); + + // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + Log("Check that enroller is removed on `admitter`, and it stops being active commissioner"); + + iter.Init(admitter.GetInstance()); + VerifyOrQuit(iter.GetNextEnrollerInfo(enrollerInfo) == kErrorNotFound); + + nexus.AdvanceTime(10 * Time::kOneSecondInMsec); + + VerifyOrQuit(!enroller.Get().IsConnected()); + + VerifyOrQuit(admitter.Get().IsPrimeAdmitter()); + VerifyOrQuit(!admitter.Get().IsActiveCommissioner()); + + VerifyOrQuit(admitter.Get().FindBorderAgentRloc(rloc16) == kErrorNotFound); + + // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + Log("Re-establish DTLS session"); + + SuccessOrQuit(enroller.Get().Connect(sockAddr)); + nexus.AdvanceTime(Time::kOneSecondInMsec); + VerifyOrQuit(enroller.Get().IsConnected()); + + // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + Log("Send `EnrollerRegister` with missing TLVs, validate that it is rejected"); + + for (uint16_t testIter = 0; testIter < 3; testIter++) + { + message = enroller.Get().NewPriorityConfirmablePostMessage(kUriEnrollerRegister); + VerifyOrQuit(message != nullptr); + + // Skip one of the required TLVs for each `testIter`. + + if (testIter != 0) + { + SuccessOrQuit(Tlv::Append(*message, kEnrollerId)); + } + + if (testIter != 1) + { + SuccessOrQuit(Tlv::Append(*message, mode)); + } + + if (testIter != 2) + { + SuccessOrQuit( + Tlv::Append(*message, steeringData.GetData(), steeringData.GetLength())); + } + + responseContext.Clear(); + SuccessOrQuit(enroller.Get().SendMessage(*message, HandleResponse, &responseContext)); + + nexus.AdvanceTime(250); + + VerifyOrQuit(responseContext.mReceived); + VerifyOrQuit(responseContext.mResponseState == MeshCoP::StateTlv::kReject); + + iter.Init(admitter.GetInstance()); + VerifyOrQuit(iter.GetNextEnrollerInfo(enrollerInfo) == kErrorNotFound); + } + + // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + Log("Register as enroller with invalid Steering Data, validate that Admitter rejects"); + + for (uint8_t length = 1; length <= 16; length++) + { + // Steering Data Length 1 can only be used with + // `SetPermitAllJoiners()` or empty. Lengths of 8 + // and 16 are valid. + + if ((length == 8) || (length == 16)) + { + continue; + } + + Log("Send `EnrollerRegister` with invalid Steering Data length %u, validate that it is rejected", length); + + SuccessOrQuit(steeringData.Init(length)); + SuccessOrQuit(steeringData.UpdateBloomFilter(admitter.Get().GetExtAddress())); + + message = enroller.Get().NewPriorityConfirmablePostMessage(kUriEnrollerRegister); + VerifyOrQuit(message != nullptr); + + SuccessOrQuit(Tlv::Append(*message, kEnrollerId)); + SuccessOrQuit(Tlv::Append(*message, mode)); + SuccessOrQuit( + Tlv::Append(*message, steeringData.GetData(), steeringData.GetLength())); + + responseContext.Clear(); + SuccessOrQuit(enroller.Get().SendMessage(*message, HandleResponse, &responseContext)); + + nexus.AdvanceTime(250); + + VerifyOrQuit(responseContext.mReceived); + VerifyOrQuit(responseContext.mResponseState == MeshCoP::StateTlv::kReject); + + iter.Init(admitter.GetInstance()); + VerifyOrQuit(iter.GetNextEnrollerInfo(enrollerInfo) == kErrorNotFound); + } + + // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + Log("Register as enroller with empty Steering Data, validate that Admitter accepts"); + + SuccessOrQuit(steeringData.Init(1)); + + message = enroller.Get().NewPriorityConfirmablePostMessage(kUriEnrollerRegister); + VerifyOrQuit(message != nullptr); + + SuccessOrQuit(Tlv::Append(*message, kEnrollerId)); + SuccessOrQuit(Tlv::Append(*message, mode)); + SuccessOrQuit(Tlv::Append(*message, steeringData.GetData(), steeringData.GetLength())); + + responseContext.Clear(); + SuccessOrQuit(enroller.Get().SendMessage(*message, HandleResponse, &responseContext)); + + nexus.AdvanceTime(250); + + VerifyOrQuit(responseContext.mReceived); + VerifyOrQuit(responseContext.mResponseState == MeshCoP::StateTlv::kAccept); + + iter.Init(admitter.GetInstance()); + SuccessOrQuit(iter.GetNextEnrollerInfo(enrollerInfo)); + VerifyOrQuit(StringMatch(enrollerInfo.mId, kEnrollerId)); + VerifyOrQuit(AsCoreType(&enrollerInfo.mSteeringData) == steeringData); + VerifyOrQuit(enrollerInfo.mMode == mode); + VerifyOrQuit(iter.GetNextEnrollerInfo(enrollerInfo) == kErrorNotFound); +} + +//--------------------------------------------------------------------------------------------------------------------- + +void TestBorderAdmitterCommissionerConflictAndPetitionerRetry(void) +{ + static const char kEnrollerId[] = "TestEnroller1234"; + + static const uint8_t kEnrollerTimeoutInSec = 50; + + Core nexus; + Node &admitter = nexus.CreateNode(); + Node &enroller = nexus.CreateNode(); + Node &otherCommr = nexus.CreateNode(); + Ip6::SockAddr sockAddr; + Pskc pskc; + Admitter::Iterator iter; + Admitter::EnrollerInfo enrollerInfo; + Coap::Message *message; + uint8_t mode; + MeshCoP::SteeringData steeringData; + MeshCoP::SteeringData leaderSteeringData; + ReceiveContext recvContext; + uint16_t rloc16; + uint16_t sessionId; + uint16_t numStateChanges; + + Log("------------------------------------------------------------------------------------------------------"); + Log("TestBorderAdmitterCommissionerConflictAndPetitionerRetry"); + + nexus.AdvanceTime(0); + + // Form the topology: + // - `admitter` forms the network (as leader) + // - `otherCommr` joins the same network. + // - `enroller` stays disconnected. + + admitter.Form(); + nexus.AdvanceTime(50 * Time::kOneSecondInMsec); + + otherCommr.Join(admitter); + + nexus.AdvanceTime(10 * Time::kOneMinuteInMsec); + + VerifyOrQuit(admitter.Get().IsLeader()); + VerifyOrQuit(otherCommr.Get().IsRouter()); + + SuccessOrQuit(enroller.Get().SetPanChannel(admitter.Get().GetPanChannel())); + enroller.Get().SetPanId(admitter.Get().GetPanId()); + enroller.Get().Up(); + + // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + Log("Enable Border Admitter on `admitter`"); + + admitter.Get().SetEnabled(true); + VerifyOrQuit(admitter.Get().IsEnabled()); + VerifyOrQuit(!admitter.Get().IsPrimeAdmitter()); + + nexus.AdvanceTime(45 * Time::kOneSecondInMsec); + + VerifyOrQuit(admitter.Get().IsPrimeAdmitter()); + VerifyOrQuit(!admitter.Get().IsActiveCommissioner()); + + SuccessOrQuit(admitter.Get().AddUnsecurePort(admitter.Get().GetUdpPort())); + + // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + Log("Make `otherCommr` the active commissioner"); + + SuccessOrQuit(otherCommr.Get().Start(nullptr, nullptr, nullptr)); + + nexus.AdvanceTime(2 * Time::kOneSecondInMsec); + + VerifyOrQuit(otherCommr.Get().GetState() == Commissioner::kStateActive); + + SuccessOrQuit(admitter.Get().FindBorderAgentRloc(rloc16)); + VerifyOrQuit(rloc16 == otherCommr.Get().GetRloc16()); + + nexus.AdvanceTime(5 * Time::kOneSecondInMsec); + + // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + Log("Establish a DTLS connection from `enroller` to `admitter`"); + + sockAddr.SetAddress(admitter.Get().GetLinkLocalAddress()); + sockAddr.SetPort(admitter.Get().GetUdpPort()); + + admitter.Get().GetPskc(pskc); + SuccessOrQuit(enroller.Get().SetPsk(pskc.m8, Pskc::kSize)); + + enroller.Get().RegisterResourceHandler(HandleResource, &recvContext); + + SuccessOrQuit(enroller.Get().Open()); + SuccessOrQuit(enroller.Get().Connect(sockAddr)); + + nexus.AdvanceTime(Time::kOneSecondInMsec); + + VerifyOrQuit(enroller.Get().IsConnected()); + + // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + Log("Send an `EnrollerRegister` message from `enroller` to `admitter`"); + + message = enroller.Get().NewPriorityConfirmablePostMessage(kUriEnrollerRegister); + VerifyOrQuit(message != nullptr); + + mode = MeshCoP::EnrollerModeTlv::kForwardJoinerRelayRx | MeshCoP::EnrollerModeTlv::kForwardUdpProxyRx; + + steeringData.SetToPermitAllJoiners(); + + SuccessOrQuit(Tlv::Append(*message, kEnrollerId)); + SuccessOrQuit(Tlv::Append(*message, mode)); + SuccessOrQuit(Tlv::Append(*message, steeringData.GetData(), steeringData.GetLength())); + + SuccessOrQuit(enroller.Get().SendMessage(*message)); + + nexus.AdvanceTime(Time::kOneSecondInMsec); + + // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + Log("Validate the enroller list on `admitter`"); + + iter.Init(admitter.GetInstance()); + SuccessOrQuit(iter.GetNextEnrollerInfo(enrollerInfo)); + + VerifyOrQuit(StringMatch(enrollerInfo.mId, kEnrollerId)); + VerifyOrQuit(AsCoreType(&enrollerInfo.mSteeringData) == steeringData); + VerifyOrQuit(enrollerInfo.mMode == mode); + + VerifyOrQuit(iter.GetNextEnrollerInfo(enrollerInfo) == kErrorNotFound); + + // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + Log("Since there is another commissioner active, validate that `admitter` fails to become active"); + + nexus.AdvanceTime(Time::kOneSecondInMsec); + + VerifyOrQuit(admitter.Get().IsPrimeAdmitter()); + VerifyOrQuit(!admitter.Get().IsActiveCommissioner()); + VerifyOrQuit(admitter.Get().IsPetitionRejected()); + + // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + Log("Validate that `EnrollerReportState` is received with `ConflictError` state"); + + VerifyOrQuit(recvContext.HasReceivedReportState()); + VerifyOrQuit(recvContext.GetLastReportedAdmitterState() == kAdmitterConflictError); + + nexus.AdvanceTime(10 * Time::kOneSecondInMsec); + + VerifyOrQuit(admitter.Get().IsPrimeAdmitter()); + VerifyOrQuit(!admitter.Get().IsActiveCommissioner()); + VerifyOrQuit(admitter.Get().IsPetitionRejected()); + + VerifyOrQuit(otherCommr.Get().GetState() == Commissioner::kStateActive); + + // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + Log("Stop `otherCommr` from acting as active commissioner"); + + recvContext.Clear(); + + SuccessOrQuit(otherCommr.Get().Stop()); + + // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + Log("Validate that the `admitter` will detect this, petitions, and becomes active commissioner"); + + nexus.AdvanceTime(2 * Time::kOneSecondInMsec); + + VerifyOrQuit(admitter.Get().IsPrimeAdmitter()); + VerifyOrQuit(admitter.Get().IsActiveCommissioner()); + VerifyOrQuit(!admitter.Get().IsPetitionRejected()); + + // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + Log("Validate that `EnrollerReportState` is received now with `Active` state"); + + VerifyOrQuit(recvContext.HasReceivedReportState()); + VerifyOrQuit(recvContext.GetLastReportedAdmitterState() == kAdmitterActive); + + // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + Log("Check the Network Data (Commissioner Data) to be properly set"); + + SuccessOrQuit(admitter.Get().FindBorderAgentRloc(rloc16)); + VerifyOrQuit(rloc16 == admitter.Get().GetRloc16()); + + SuccessOrQuit(admitter.Get().FindCommissioningSessionId(sessionId)); + VerifyOrQuit(sessionId == admitter.Get().GetCommissionerSessionId()); + + SuccessOrQuit(admitter.Get().FindSteeringData(leaderSteeringData)); + VerifyOrQuit(leaderSteeringData == steeringData); + + // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + Log("From `otherCommr` forcefully evict the current active commissioner (`admitter`)"); + + recvContext.Clear(); + + SuccessOrQuit(otherCommr.Get().EvictActiveCommissioner()); + + // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + Log("Send `EnrollerKeepAlive` three times, 20 seconds apart to maintain the enroller connection"); + + for (uint8_t i = 0; i < 3; i++) + { + nexus.AdvanceTime(20 * Time::kOneSecondInMsec); + + message = enroller.Get().NewPriorityConfirmablePostMessage(kUriEnrollerKeepAlive); + VerifyOrQuit(message != nullptr); + + SuccessOrQuit(Tlv::Append(*message, MeshCoP::StateTlv::kAccept)); + + SuccessOrQuit(enroller.Get().SendMessage(*message)); + + nexus.AdvanceTime(Time::kOneSecondInMsec); + + iter.Init(admitter.GetInstance()); + SuccessOrQuit(iter.GetNextEnrollerInfo(enrollerInfo)); + + VerifyOrQuit(StringMatch(enrollerInfo.mId, kEnrollerId)); + VerifyOrQuit(AsCoreType(&enrollerInfo.mSteeringData) == steeringData); + VerifyOrQuit(enrollerInfo.mMode == mode); + + VerifyOrQuit(iter.GetNextEnrollerInfo(enrollerInfo) == kErrorNotFound); + } + + // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + Log("Validate that the eviction was properly detected, and petitioner retry mechanism did restore it"); + + VerifyOrQuit(recvContext.HasReceivedReportState()); + VerifyOrQuit(recvContext.GetLastReportedAdmitterState() == kAdmitterActive); + + numStateChanges = recvContext.mStateReports.GetLength(); + VerifyOrQuit(numStateChanges >= 2); + VerifyOrQuit(recvContext.mStateReports[numStateChanges - 2].mAdmitterState == kAdmitterReady); + VerifyOrQuit(recvContext.mStateReports[numStateChanges - 1].mAdmitterState == kAdmitterActive); +} + +//--------------------------------------------------------------------------------------------------------------------- + +template +uint8_t FindMatchingEnroller(const Admitter::EnrollerInfo &aInfo, + const char *(&aEnrollerIds)[kNumEnrollers], + BitSet &aFoundIndexes) +{ + // Finds a matching enroller by comparing `aInfo.mId` against + // `aEnrollerIds[]` and returns the matched index. This function + // ensures each enroller is found only once by checking that the + // index is not already in `aFoundIndexes`, then updates + // `aFoundIndexes`. + + uint8_t matchedIndex = kNumEnrollers; + + for (uint8_t index = 0; index < kNumEnrollers; index++) + { + if (StringMatch(aInfo.mId, aEnrollerIds[index])) + { + matchedIndex = index; + break; + } + } + + VerifyOrQuit(matchedIndex < kNumEnrollers); + + VerifyOrQuit(!aFoundIndexes.Has(matchedIndex)); + aFoundIndexes.Add(matchedIndex); + + return matchedIndex; +} + +template bool DidFindAllEnrollers(const BitSet &aFoundIndexes) +{ + bool didFindAll = true; + + for (uint8_t index = 0; index < kNumEnrollers; index++) + { + if (!aFoundIndexes.Has(index)) + { + didFindAll = false; + break; + } + } + + return didFindAll; +} + +//--------------------------------------------------------------------------------------------------------------------- + +void LogEnroller(const Admitter::EnrollerInfo &aInfo) +{ + Log(" Enroller - id:%s steeringData:%s mode:0x%02x", aInfo.mId, + AsCoreType(&aInfo.mSteeringData).ToString().AsCString(), aInfo.mMode); +} + +void LogJoiner(const Admitter::JoinerInfo &aInfo) +{ + Log(" Joiner - iid:%s, msec-till-expire:%lu", AsCoreType(&aInfo.mIid).ToString().AsCString(), + ToUlong(aInfo.mMsecTillExpiration)); +} + +//--------------------------------------------------------------------------------------------------------------------- + +void TestBorderAdmitterMultipleEnrollers(void) +{ + static constexpr uint8_t kNumEnrollers = 4; + + static const char *kEnrollerIds[kNumEnrollers] = {"earth", "water", "wind", "fire"}; + + Core nexus; + Node &admitter = nexus.CreateNode(); + Node *enrollers[kNumEnrollers]; + Ip6::SockAddr sockAddr; + Pskc pskc; + Admitter::Iterator iter; + Admitter::EnrollerInfo enrollerInfo; + Admitter::JoinerInfo joinerInfo; + Coap::Message *message; + uint8_t mode; + MeshCoP::SteeringData steeringData[kNumEnrollers]; + ReceiveContext recvContext[kNumEnrollers]; + ResponseContext responseContexts[kNumEnrollers]; + BitSet foundEnrollers; + MeshCoP::SteeringData leaderSteeringData; + MeshCoP::SteeringData combinedSteeringData; + uint16_t rloc16; + uint16_t sessionId; + Mac::ExtAddress joinerIid; + + Log("------------------------------------------------------------------------------------------------------"); + Log("TestBorderAdmitterMultipleEnrollers"); + + for (uint8_t i = 0; i < kNumEnrollers; i++) + { + enrollers[i] = &nexus.CreateNode(); + } + + nexus.AdvanceTime(0); + + // Form the topology: + // - `admitter` forms the network (as leader) + // - All enrollers stay disconnected. + + admitter.Form(); + nexus.AdvanceTime(50 * Time::kOneSecondInMsec); + + VerifyOrQuit(admitter.Get().IsLeader()); + + for (Node *enroller : enrollers) + { + SuccessOrQuit(enroller->Get().SetPanChannel(admitter.Get().GetPanChannel())); + enroller->Get().SetPanId(admitter.Get().GetPanId()); + enroller->Get().Up(); + } + + // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + Log("Enable Border Admitter on `admitter`"); + + admitter.Get().SetEnabled(true); + VerifyOrQuit(admitter.Get().IsEnabled()); + VerifyOrQuit(!admitter.Get().IsPrimeAdmitter()); + + nexus.AdvanceTime(45 * Time::kOneSecondInMsec); + + VerifyOrQuit(admitter.Get().IsPrimeAdmitter()); + VerifyOrQuit(!admitter.Get().IsActiveCommissioner()); + + SuccessOrQuit(admitter.Get().AddUnsecurePort(admitter.Get().GetUdpPort())); + + // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + Log("Establish a DTLS connection from all `enrollers` to `admitter`"); + + sockAddr.SetAddress(admitter.Get().GetLinkLocalAddress()); + sockAddr.SetPort(admitter.Get().GetUdpPort()); + + admitter.Get().GetPskc(pskc); + + for (uint8_t i = 0; i < kNumEnrollers; i++) + { + Node *enroller = enrollers[i]; + + SuccessOrQuit(enroller->Get().SetPsk(pskc.m8, Pskc::kSize)); + + recvContext[i].Clear(); + enroller->Get().RegisterResourceHandler(HandleResource, &recvContext[i]); + + SuccessOrQuit(enroller->Get().Open()); + SuccessOrQuit(enroller->Get().Connect(sockAddr)); + + nexus.AdvanceTime(Time::kOneSecondInMsec); + + VerifyOrQuit(enroller->Get().IsConnected()); + } + + // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + Log("Prepare Steering Data for each enroller"); + + mode = MeshCoP::EnrollerModeTlv::kForwardJoinerRelayRx | MeshCoP::EnrollerModeTlv::kForwardUdpProxyRx; + + steeringData[0].SetToPermitAllJoiners(); + + SuccessOrQuit(steeringData[1].Init(16)); + + for (uint8_t numJoiners = 0; numJoiners < 3; numJoiners++) + { + joinerIid.GenerateRandom(); + SuccessOrQuit(steeringData[1].UpdateBloomFilter(joinerIid)); + } + + SuccessOrQuit(steeringData[2].Init(8)); + joinerIid.GenerateRandom(); + SuccessOrQuit(steeringData[2].UpdateBloomFilter(joinerIid)); + + SuccessOrQuit(steeringData[3].Init(8)); + joinerIid.GenerateRandom(); + SuccessOrQuit(steeringData[3].UpdateBloomFilter(joinerIid)); + + // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + Log("Send an `EnrollerRegister` message from all `enrollers`"); + + for (uint8_t i = 0; i < kNumEnrollers; i++) + { + message = enrollers[i]->Get().NewPriorityConfirmablePostMessage(kUriEnrollerRegister); + VerifyOrQuit(message != nullptr); + + SuccessOrQuit(Tlv::Append(*message, kEnrollerIds[i])); + SuccessOrQuit(Tlv::Append(*message, mode)); + SuccessOrQuit( + Tlv::Append(*message, steeringData[i].GetData(), steeringData[i].GetLength())); + + responseContexts[i].Clear(); + SuccessOrQuit( + enrollers[i]->Get().SendMessage(*message, HandleResponse, &responseContexts[i])); + } + + nexus.AdvanceTime(Time::kOneSecondInMsec); + + // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + Log("Check that all registrations were accepted"); + + for (uint8_t i = 0; i < kNumEnrollers; i++) + { + VerifyOrQuit(responseContexts[i].mReceived); + VerifyOrQuit(responseContexts[i].mResponseState == MeshCoP::StateTlv::kAccept); + VerifyOrQuit(responseContexts[i].mHasAdmitterState); + } + + // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + Log("Validate that `admitter` becomes active commissioner"); + + nexus.AdvanceTime(Time::kOneSecondInMsec); + + VerifyOrQuit(admitter.Get().IsPrimeAdmitter()); + VerifyOrQuit(admitter.Get().IsActiveCommissioner()); + + SuccessOrQuit(admitter.Get().FindBorderAgentRloc(rloc16)); + VerifyOrQuit(rloc16 == admitter.Get().GetRloc16()); + + SuccessOrQuit(admitter.Get().FindCommissioningSessionId(sessionId)); + VerifyOrQuit(sessionId == admitter.Get().GetCommissionerSessionId()); + + SuccessOrQuit(admitter.Get().FindSteeringData(leaderSteeringData)); + VerifyOrQuit(leaderSteeringData.GetLength() == 1); + VerifyOrQuit(leaderSteeringData.PermitsAllJoiners()); + + // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + Log("Validate that `EnrollerReportState` is received with the updated Admitter state"); + + for (uint8_t i = 0; i < kNumEnrollers; i++) + { + // Some enrollers may already get the updated state in the Register response + if (recvContext[i].HasReceivedReportState()) + { + VerifyOrQuit(recvContext[i].GetLastReportedAdmitterState() == kAdmitterActive); + } + } + + // - - - - - - - - - - - - - - - - - - - - - - -. - - - - - - - - - - - - - - - - - - - - - - - - - - + Log("Validate the enroller list on `admitter`"); + + foundEnrollers.Clear(); + iter.Init(admitter.GetInstance()); + + while (iter.GetNextEnrollerInfo(enrollerInfo) == kErrorNone) + { + uint8_t matchedIndex; + + LogEnroller(enrollerInfo); + + matchedIndex = FindMatchingEnroller(enrollerInfo, kEnrollerIds, foundEnrollers); + + VerifyOrQuit(AsCoreType(&enrollerInfo.mSteeringData) == steeringData[matchedIndex]); + VerifyOrQuit(enrollerInfo.mMode == mode); + + VerifyOrQuit(iter.GetNextJoinerInfo(joinerInfo) == kErrorNotFound); + } + + VerifyOrQuit(DidFindAllEnrollers(foundEnrollers)); + + nexus.AdvanceTime(10 * Time::kOneSecondInMsec); + + // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + Log("Send a keep alive from first enroller with reject status (to unregister the enroller)"); + + message = enrollers[0]->Get().NewPriorityConfirmablePostMessage(kUriEnrollerKeepAlive); + VerifyOrQuit(message != nullptr); + + SuccessOrQuit(Tlv::Append(*message, MeshCoP::StateTlv::kReject)); + + responseContexts[0].Clear(); + SuccessOrQuit(enrollers[0]->Get().SendMessage(*message, HandleResponse, &responseContexts[0])); + + nexus.AdvanceTime(Time::kOneSecondInMsec); + + VerifyOrQuit(responseContexts[0].mReceived); + VerifyOrQuit(responseContexts[0].mResponseState == MeshCoP::StateTlv::kReject); + + // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + Log("Check enroller info and that enroller 0 is no longer present"); + + foundEnrollers.Clear(); + + iter.Init(admitter.GetInstance()); + + while (iter.GetNextEnrollerInfo(enrollerInfo) == kErrorNone) + { + uint8_t matchedIndex; + + LogEnroller(enrollerInfo); + + matchedIndex = FindMatchingEnroller(enrollerInfo, kEnrollerIds, foundEnrollers); + VerifyOrQuit(AsCoreType(&enrollerInfo.mSteeringData) == steeringData[matchedIndex]); + VerifyOrQuit(enrollerInfo.mMode == mode); + + VerifyOrQuit(iter.GetNextJoinerInfo(joinerInfo) == kErrorNotFound); + } + + VerifyOrQuit(!foundEnrollers.Has(0)); + VerifyOrQuit(foundEnrollers.Has(1)); + VerifyOrQuit(foundEnrollers.Has(2)); + VerifyOrQuit(foundEnrollers.Has(3)); + + // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + Log("Validate that `admitter` is still active commissioner and the steering data is updated"); + + nexus.AdvanceTime(Time::kOneSecondInMsec); + + VerifyOrQuit(admitter.Get().IsPrimeAdmitter()); + VerifyOrQuit(admitter.Get().IsActiveCommissioner()); + + SuccessOrQuit(admitter.Get().FindBorderAgentRloc(rloc16)); + VerifyOrQuit(rloc16 == admitter.Get().GetRloc16()); + + SuccessOrQuit(admitter.Get().FindCommissioningSessionId(sessionId)); + VerifyOrQuit(sessionId == admitter.Get().GetCommissionerSessionId()); + + combinedSteeringData = steeringData[1]; + SuccessOrQuit(combinedSteeringData.MergeBloomFilterWith(steeringData[2])); + SuccessOrQuit(combinedSteeringData.MergeBloomFilterWith(steeringData[3])); + + SuccessOrQuit(admitter.Get().FindSteeringData(leaderSteeringData)); + VerifyOrQuit(leaderSteeringData == combinedSteeringData); + + // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + Log("Wait 20 seconds, send keep-alive from enrollers[2,3] but not from [1]"); + + nexus.AdvanceTime(20 * Time::kOneSecondInMsec); + + for (uint8_t i = 2; i < kNumEnrollers; i++) + { + message = enrollers[i]->Get().NewPriorityConfirmablePostMessage(kUriEnrollerKeepAlive); + VerifyOrQuit(message != nullptr); + + SuccessOrQuit(Tlv::Append(*message, MeshCoP::StateTlv::kAccept)); + + responseContexts[i].Clear(); + SuccessOrQuit( + enrollers[i]->Get().SendMessage(*message, HandleResponse, &responseContexts[i])); + } + + nexus.AdvanceTime(Time::kOneSecondInMsec); + + for (uint8_t i = 2; i < kNumEnrollers; i++) + { + VerifyOrQuit(responseContexts[i].mReceived); + VerifyOrQuit(responseContexts[i].mResponseState == MeshCoP::StateTlv::kAccept); + } + + // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + Log("Wait 35 more seconds and check that enroller 1 is now removed"); + + nexus.AdvanceTime(35 * Time::kOneSecondInMsec); + + foundEnrollers.Clear(); + + iter.Init(admitter.GetInstance()); + + while (iter.GetNextEnrollerInfo(enrollerInfo) == kErrorNone) + { + LogEnroller(enrollerInfo); + + FindMatchingEnroller(enrollerInfo, kEnrollerIds, foundEnrollers); + + VerifyOrQuit(iter.GetNextJoinerInfo(joinerInfo) == kErrorNotFound); + } + + VerifyOrQuit(!foundEnrollers.Has(0)); + VerifyOrQuit(!foundEnrollers.Has(1)); + VerifyOrQuit(foundEnrollers.Has(2)); + VerifyOrQuit(foundEnrollers.Has(3)); + + // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + Log("Validate that `admitter` updates the steering data"); + + nexus.AdvanceTime(Time::kOneSecondInMsec); + + VerifyOrQuit(admitter.Get().IsPrimeAdmitter()); + VerifyOrQuit(admitter.Get().IsActiveCommissioner()); + + combinedSteeringData = steeringData[2]; + SuccessOrQuit(combinedSteeringData.MergeBloomFilterWith(steeringData[3])); + + SuccessOrQuit(admitter.Get().FindSteeringData(leaderSteeringData)); + VerifyOrQuit(leaderSteeringData == combinedSteeringData); +} + +//--------------------------------------------------------------------------------------------------------------------- + +void TestBorderAdmitterJoinerEnrollerInteraction(void) +{ + static constexpr uint8_t kNumEnrollers = 4; + static constexpr uint8_t kNumJoiners = 2; + + static const char *kEnrollerIds[kNumEnrollers] = {"diamond", "ruby", "sapphire", "emerald"}; + + static const char kPskd[] = "J01NME1234"; + + Core nexus; + Node &admitter = nexus.CreateNode(); + Node *enrollers[kNumEnrollers]; + Node *joiners[kNumJoiners]; + Ip6::SockAddr sockAddr; + Pskc pskc; + Coap::Message *message; + uint8_t modes[kNumEnrollers]; + ResponseContext responseContexts[kNumEnrollers]; + ReceiveContext recvContext[kNumEnrollers]; + MeshCoP::SteeringData steeringData; + MeshCoP::SteeringData leaderSteeringData; + Ip6::InterfaceIdentifier joinerIids[kNumJoiners]; + Ip6::InterfaceIdentifier wildcardJoinerIid; + Admitter::Iterator iter; + Admitter::EnrollerInfo enrollerInfo; + Admitter::JoinerInfo joinerInfo; + BitSet foundEnrollers; + BitSet foundJoiners; + uint16_t sessionId; + uint16_t rloc16; + + Log("------------------------------------------------------------------------------------------------------"); + Log("TestBorderAdmitterJoinerEnrollerInteraction"); + + for (uint8_t i = 0; i < kNumEnrollers; i++) + { + enrollers[i] = &nexus.CreateNode(); + } + + for (uint8_t j = 0; j < kNumJoiners; j++) + { + joiners[j] = &nexus.CreateNode(); + } + + nexus.AdvanceTime(0); + + // Form the topology: + // - `admitter` forms the network (as leader) + // - All enrollers stay disconnected. + + admitter.Form(); + nexus.AdvanceTime(50 * Time::kOneSecondInMsec); + + VerifyOrQuit(admitter.Get().IsLeader()); + + for (Node *enroller : enrollers) + { + SuccessOrQuit(enroller->Get().SetPanChannel(admitter.Get().GetPanChannel())); + enroller->Get().SetPanId(admitter.Get().GetPanId()); + enroller->Get().Up(); + } + + // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + Log("Enable Border Admitter on `admitter`"); + + admitter.Get().SetEnabled(true); + VerifyOrQuit(admitter.Get().IsEnabled()); + VerifyOrQuit(!admitter.Get().IsPrimeAdmitter()); + + nexus.AdvanceTime(45 * Time::kOneSecondInMsec); + + VerifyOrQuit(admitter.Get().IsPrimeAdmitter()); + VerifyOrQuit(!admitter.Get().IsActiveCommissioner()); + + SuccessOrQuit(admitter.Get().AddUnsecurePort(admitter.Get().GetUdpPort())); + + // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + Log("Establish a DTLS connection from all `enrollers` to `admitter`"); + + sockAddr.SetAddress(admitter.Get().GetLinkLocalAddress()); + sockAddr.SetPort(admitter.Get().GetUdpPort()); + + admitter.Get().GetPskc(pskc); + + for (uint8_t i = 0; i < kNumEnrollers; i++) + { + Node *enroller = enrollers[i]; + + SuccessOrQuit(enroller->Get().SetPsk(pskc.m8, Pskc::kSize)); + + recvContext[i].Clear(); + enroller->Get().RegisterResourceHandler(HandleResource, &recvContext[i]); + + SuccessOrQuit(enroller->Get().Open()); + SuccessOrQuit(enroller->Get().Connect(sockAddr)); + + nexus.AdvanceTime(Time::kOneSecondInMsec); + + VerifyOrQuit(enroller->Get().IsConnected()); + } + + // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + Log("Prepare mode for each enroller"); + + modes[0] = MeshCoP::EnrollerModeTlv::kForwardJoinerRelayRx | MeshCoP::EnrollerModeTlv::kForwardUdpProxyRx; + modes[1] = MeshCoP::EnrollerModeTlv::kForwardJoinerRelayRx | MeshCoP::EnrollerModeTlv::kForwardUdpProxyRx; + modes[2] = MeshCoP::EnrollerModeTlv::kForwardJoinerRelayRx; + modes[3] = MeshCoP::EnrollerModeTlv::kForwardUdpProxyRx; + + steeringData.SetToPermitAllJoiners(); + + // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + Log("Send an `EnrollerRegister` message from all `enrollers`"); + + for (uint8_t i = 0; i < kNumEnrollers; i++) + { + message = enrollers[i]->Get().NewPriorityConfirmablePostMessage(kUriEnrollerRegister); + VerifyOrQuit(message != nullptr); + + SuccessOrQuit(Tlv::Append(*message, kEnrollerIds[i])); + SuccessOrQuit(Tlv::Append(*message, modes[i])); + SuccessOrQuit( + Tlv::Append(*message, steeringData.GetData(), steeringData.GetLength())); + + responseContexts[i].Clear(); + SuccessOrQuit( + enrollers[i]->Get().SendMessage(*message, HandleResponse, &responseContexts[i])); + } + + nexus.AdvanceTime(Time::kOneSecondInMsec); + + // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + Log("Check that all registrations were accepted"); + + for (uint8_t i = 0; i < kNumEnrollers; i++) + { + VerifyOrQuit(responseContexts[i].mReceived); + VerifyOrQuit(responseContexts[i].mResponseState == MeshCoP::StateTlv::kAccept); + VerifyOrQuit(responseContexts[i].mHasAdmitterState); + } + + // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + Log("Validate that `admitter` becomes active commissioner"); + + nexus.AdvanceTime(Time::kOneSecondInMsec); + + VerifyOrQuit(admitter.Get().IsPrimeAdmitter()); + VerifyOrQuit(admitter.Get().IsActiveCommissioner()); + + SuccessOrQuit(admitter.Get().FindBorderAgentRloc(rloc16)); + VerifyOrQuit(rloc16 == admitter.Get().GetRloc16()); + + SuccessOrQuit(admitter.Get().FindCommissioningSessionId(sessionId)); + VerifyOrQuit(sessionId == admitter.Get().GetCommissionerSessionId()); + + SuccessOrQuit(admitter.Get().FindSteeringData(leaderSteeringData)); + VerifyOrQuit(leaderSteeringData == steeringData); + VerifyOrQuit(leaderSteeringData.PermitsAllJoiners()); + + // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + Log("Validate the Enroller info on `admitter`"); + + foundEnrollers.Clear(); + iter.Init(admitter.GetInstance()); + + while (iter.GetNextEnrollerInfo(enrollerInfo) == kErrorNone) + { + uint8_t matchedIndex; + + LogEnroller(enrollerInfo); + + matchedIndex = FindMatchingEnroller(enrollerInfo, kEnrollerIds, foundEnrollers); + + VerifyOrQuit(AsCoreType(&enrollerInfo.mSteeringData) == steeringData); + VerifyOrQuit(enrollerInfo.mMode == modes[matchedIndex]); + + VerifyOrQuit(iter.GetNextJoinerInfo(joinerInfo) == kErrorNotFound); + } + + VerifyOrQuit(DidFindAllEnrollers(foundEnrollers)); + + // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + Log("Start `joiners[0]`"); + + joiners[0]->Get().Up(); + SuccessOrQuit(joiners[0]->Get().Start(kPskd, + /* aProvisioningUrl */ nullptr, + /* aVendorName */ nullptr, + /* aVendorModel */ nullptr, + /* aVendorSwVersion */ nullptr, + /* aVendorData */ nullptr, + /* aCallback */ nullptr, + /* aContext */ nullptr)); + + joinerIids[0].SetFromExtAddress(joiners[0]->Get().GetId()); + + nexus.AdvanceTime(8 * Time::kOneSecondInMsec); + + // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + Log("Validate that `joiner` `RelayRx` are forwarded to all `enrollers` with `kForwardJoinerRelayRx` mode flag"); + + for (uint8_t i = 0; i < kNumEnrollers; i++) + { + Coap::Message *message = AsCoapMessagePtr(recvContext[i].mRelayRxMsgs.GetHead()); + Ip6::InterfaceIdentifier readIid; + uint16_t joinerRouterRloc; + + if ((modes[i] & MeshCoP::EnrollerModeTlv::kForwardJoinerRelayRx) == 0) + { + VerifyOrQuit(message == nullptr); + continue; + } + + VerifyOrQuit(message != nullptr); + + VerifyOrQuit(message->ReadType() == Coap::kTypeNonConfirmable); + VerifyOrQuit(message->ReadCode() == Coap::kCodePost); + SuccessOrQuit(Tlv::Find(*message, readIid)); + SuccessOrQuit(Tlv::Find(*message, joinerRouterRloc)); + + VerifyOrQuit(readIid == joinerIids[0]); + VerifyOrQuit(joinerRouterRloc = admitter.Get().GetRloc16()); + } + + // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + Log("Stop `joiners[0]`"); + + joiners[0]->Get().Stop(); + + for (uint8_t i = 0; i < kNumEnrollers; i++) + { + recvContext[i].Clear(); + } + + // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + Log("Send an `EnrollerJoinerAccept` message from `enrollers[0]` to `admitter` accepting `joiners[0]`"); + + message = enrollers[0]->Get().NewPriorityConfirmablePostMessage(kUriEnrollerJoinerAccept); + VerifyOrQuit(message != nullptr); + + SuccessOrQuit(Tlv::Append(*message, joinerIids[0])); + + responseContexts[0].Clear(); + SuccessOrQuit(enrollers[0]->Get().SendMessage(*message, HandleResponse, &responseContexts[0])); + + nexus.AdvanceTime(Time::kOneSecondInMsec); + + VerifyOrQuit(responseContexts[0].mReceived); + VerifyOrQuit(responseContexts[0].mResponseState == MeshCoP::StateTlv::kAccept); + VerifyOrQuit(!responseContexts[0].mHasAdmitterState); + + // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + Log("Validate that the accepted `joiners[0]` is tracked by `enrollers[0]` entry on `admitter`"); + + foundEnrollers.Clear(); + iter.Init(admitter.GetInstance()); + + while (iter.GetNextEnrollerInfo(enrollerInfo) == kErrorNone) + { + uint8_t matchedIndex; + + LogEnroller(enrollerInfo); + + matchedIndex = FindMatchingEnroller(enrollerInfo, kEnrollerIds, foundEnrollers); + + VerifyOrQuit(AsCoreType(&enrollerInfo.mSteeringData) == steeringData); + VerifyOrQuit(enrollerInfo.mMode == modes[matchedIndex]); + + if (matchedIndex == 0) + { + SuccessOrQuit(iter.GetNextJoinerInfo(joinerInfo)); + VerifyOrQuit(AsCoreType(&joinerInfo.mIid) == joinerIids[0]); + LogJoiner(joinerInfo); + } + + VerifyOrQuit(iter.GetNextJoinerInfo(joinerInfo) == kErrorNotFound); + } + + VerifyOrQuit(DidFindAllEnrollers(foundEnrollers)); + + nexus.AdvanceTime(Time::kOneSecondInMsec); + + // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + Log("Start `joiners[0]` again and validate that its `RelayRx` are only forwarded to `enrollers[0]`"); + + joiners[0]->Get().Up(); + SuccessOrQuit(joiners[0]->Get().Start(kPskd, + /* aProvisioningUrl */ nullptr, + /* aVendorName */ nullptr, + /* aVendorModel */ nullptr, + /* aVendorSwVersion */ nullptr, + /* aVendorData */ nullptr, + /* aCallback */ nullptr, + /* aContext */ nullptr)); + + nexus.AdvanceTime(8 * Time::kOneSecondInMsec); + + for (uint8_t i = 0; i < kNumEnrollers; i++) + { + Coap::Message *message = AsCoapMessagePtr(recvContext[i].mRelayRxMsgs.GetHead()); + Ip6::InterfaceIdentifier readIid; + uint16_t joinerRouterRloc; + + if (i != 0) + { + VerifyOrQuit(message == nullptr); + continue; + } + + VerifyOrQuit(message != nullptr); + + VerifyOrQuit(message->ReadType() == Coap::kTypeNonConfirmable); + VerifyOrQuit(message->ReadCode() == Coap::kCodePost); + SuccessOrQuit(Tlv::Find(*message, readIid)); + SuccessOrQuit(Tlv::Find(*message, joinerRouterRloc)); + + VerifyOrQuit(readIid == joinerIids[0]); + VerifyOrQuit(joinerRouterRloc = admitter.Get().GetRloc16()); + } + + joiners[0]->Get().Stop(); + + for (uint8_t i = 0; i < kNumEnrollers; i++) + { + recvContext[i].Clear(); + } + + // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + Log("Start `joiners[1]` and validate that its `RelayRx` are forwarded to all `enrollers`"); + + joiners[1]->Get().Up(); + SuccessOrQuit(joiners[1]->Get().Start(kPskd, + /* aProvisioningUrl */ nullptr, + /* aVendorName */ nullptr, + /* aVendorModel */ nullptr, + /* aVendorSwVersion */ nullptr, + /* aVendorData */ nullptr, + /* aCallback */ nullptr, + /* aContext */ nullptr)); + + joinerIids[1].SetFromExtAddress(joiners[1]->Get().GetId()); + + nexus.AdvanceTime(8 * Time::kOneSecondInMsec); + + for (uint8_t i = 0; i < kNumEnrollers; i++) + { + Coap::Message *message = AsCoapMessagePtr(recvContext[i].mRelayRxMsgs.GetHead()); + Ip6::InterfaceIdentifier readIid; + uint16_t joinerRouterRloc; + + if ((modes[i] & MeshCoP::EnrollerModeTlv::kForwardJoinerRelayRx) == 0) + { + VerifyOrQuit(message == nullptr); + continue; + } + + VerifyOrQuit(message != nullptr); + + VerifyOrQuit(message->ReadType() == Coap::kTypeNonConfirmable); + VerifyOrQuit(message->ReadCode() == Coap::kCodePost); + SuccessOrQuit(Tlv::Find(*message, readIid)); + SuccessOrQuit(Tlv::Find(*message, joinerRouterRloc)); + + VerifyOrQuit(readIid == joinerIids[1]); + VerifyOrQuit(joinerRouterRloc = admitter.Get().GetRloc16()); + } + + joiners[1]->Get().Stop(); + + // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + Log("Send `EnrollerKeepAlive` message from all `enrollers` to maintain the connection"); + + for (uint8_t i = 0; i < kNumEnrollers; i++) + { + message = enrollers[i]->Get().NewPriorityConfirmablePostMessage(kUriEnrollerKeepAlive); + VerifyOrQuit(message != nullptr); + + SuccessOrQuit(Tlv::Append(*message, MeshCoP::StateTlv::kAccept)); + + responseContexts[i].Clear(); + SuccessOrQuit( + enrollers[i]->Get().SendMessage(*message, HandleResponse, &responseContexts[i])); + } + + nexus.AdvanceTime(Time::kOneSecondInMsec); + + for (uint8_t i = 0; i < kNumEnrollers; i++) + { + VerifyOrQuit(responseContexts[i].mReceived); + VerifyOrQuit(responseContexts[i].mResponseState == MeshCoP::StateTlv::kAccept); + VerifyOrQuit(responseContexts[i].mHasAdmitterState); + } + + // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + Log("Send an `EnrollerJoinerAccept` message from `enrollers[0]` to `admitter` accepting `joiners[1]`"); + + message = enrollers[0]->Get().NewPriorityConfirmablePostMessage(kUriEnrollerJoinerAccept); + VerifyOrQuit(message != nullptr); + + SuccessOrQuit(Tlv::Append(*message, joinerIids[1])); + + responseContexts[0].Clear(); + SuccessOrQuit(enrollers[0]->Get().SendMessage(*message, HandleResponse, &responseContexts[0])); + + nexus.AdvanceTime(Time::kOneSecondInMsec); + + VerifyOrQuit(responseContexts[0].mReceived); + VerifyOrQuit(responseContexts[0].mResponseState == MeshCoP::StateTlv::kAccept); + VerifyOrQuit(!responseContexts[0].mHasAdmitterState); + + // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + Log("Validate that both accepted `joiners` are tracked by `enrollers[0]` on `admitter`"); + + foundEnrollers.Clear(); + iter.Init(admitter.GetInstance()); + + while (iter.GetNextEnrollerInfo(enrollerInfo) == kErrorNone) + { + uint8_t matchedIndex; + + LogEnroller(enrollerInfo); + + matchedIndex = FindMatchingEnroller(enrollerInfo, kEnrollerIds, foundEnrollers); + + VerifyOrQuit(AsCoreType(&enrollerInfo.mSteeringData) == steeringData); + VerifyOrQuit(enrollerInfo.mMode == modes[matchedIndex]); + + if (matchedIndex == 0) + { + uint16_t numJoiners = 0; + + foundJoiners.Clear(); + + while (iter.GetNextJoinerInfo(joinerInfo) == kErrorNone) + { + LogJoiner(joinerInfo); + + numJoiners++; + + for (uint8_t j = 0; j < 2; j++) + { + if (joinerIids[j] == AsCoreType(&joinerInfo.mIid)) + { + VerifyOrQuit(!foundJoiners.Has(j)); + foundJoiners.Add(j); + } + } + } + + VerifyOrQuit(numJoiners == 2); + } + else + { + VerifyOrQuit(iter.GetNextJoinerInfo(joinerInfo) == kErrorNotFound); + } + } + + VerifyOrQuit(DidFindAllEnrollers(foundEnrollers)); + + // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + Log("Start `joiners[1]` again and validate that its `RelayRx` are only forwarded to `enrollers[0]`"); + + for (uint8_t i = 0; i < kNumEnrollers; i++) + { + recvContext[i].Clear(); + } + + joiners[1]->Get().Up(); + SuccessOrQuit(joiners[1]->Get().Start(kPskd, + /* aProvisioningUrl */ nullptr, + /* aVendorName */ nullptr, + /* aVendorModel */ nullptr, + /* aVendorSwVersion */ nullptr, + /* aVendorData */ nullptr, + /* aCallback */ nullptr, + /* aContext */ nullptr)); + + nexus.AdvanceTime(8 * Time::kOneSecondInMsec); + + for (uint8_t i = 0; i < kNumEnrollers; i++) + { + Coap::Message *message = AsCoapMessagePtr(recvContext[i].mRelayRxMsgs.GetHead()); + Ip6::InterfaceIdentifier readIid; + uint16_t joinerRouterRloc; + + if (i != 0) + { + VerifyOrQuit(message == nullptr); + continue; + } + + VerifyOrQuit(message != nullptr); + + VerifyOrQuit(message->ReadType() == Coap::kTypeNonConfirmable); + VerifyOrQuit(message->ReadCode() == Coap::kCodePost); + SuccessOrQuit(Tlv::Find(*message, readIid)); + SuccessOrQuit(Tlv::Find(*message, joinerRouterRloc)); + + VerifyOrQuit(readIid == joinerIids[1]); + VerifyOrQuit(joinerRouterRloc = admitter.Get().GetRloc16()); + } + + joiners[1]->Get().Stop(); + + // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + Log("From `enrollers[1]` send `EnrollerJoinerAccept` for `joiners[1]`"); + + message = enrollers[1]->Get().NewPriorityConfirmablePostMessage(kUriEnrollerJoinerAccept); + VerifyOrQuit(message != nullptr); + + SuccessOrQuit(Tlv::Append(*message, joinerIids[1])); + + responseContexts[1].Clear(); + SuccessOrQuit(enrollers[1]->Get().SendMessage(*message, HandleResponse, &responseContexts[1])); + + nexus.AdvanceTime(Time::kOneSecondInMsec); + + Log("Validate that the request is rejected since `joiners[1]` is already accepted by `enrollers[0]`"); + + VerifyOrQuit(responseContexts[1].mReceived); + VerifyOrQuit(responseContexts[1].mResponseState == MeshCoP::StateTlv::kReject); + VerifyOrQuit(!responseContexts[1].mHasAdmitterState); + + // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + Log("Validate `joiners[1]` is still accepted by `enrollers[0]`"); + + foundEnrollers.Clear(); + iter.Init(admitter.GetInstance()); + + while (iter.GetNextEnrollerInfo(enrollerInfo) == kErrorNone) + { + uint8_t matchedIndex; + + LogEnroller(enrollerInfo); + + matchedIndex = FindMatchingEnroller(enrollerInfo, kEnrollerIds, foundEnrollers); + + VerifyOrQuit(AsCoreType(&enrollerInfo.mSteeringData) == steeringData); + VerifyOrQuit(enrollerInfo.mMode == modes[matchedIndex]); + + if (matchedIndex == 0) + { + uint16_t numJoiners = 0; + + foundJoiners.Clear(); + + while (iter.GetNextJoinerInfo(joinerInfo) == kErrorNone) + { + LogJoiner(joinerInfo); + + numJoiners++; + + for (uint8_t j = 0; j < 2; j++) + { + if (joinerIids[j] == AsCoreType(&joinerInfo.mIid)) + { + VerifyOrQuit(!foundJoiners.Has(j)); + foundJoiners.Add(j); + } + } + } + + VerifyOrQuit(numJoiners == 2); + } + else + { + VerifyOrQuit(iter.GetNextJoinerInfo(joinerInfo) == kErrorNotFound); + } + + VerifyOrQuit(iter.GetNextJoinerInfo(joinerInfo) == kErrorNotFound); + } + + // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + Log("Start `joiners[1]` again and validate that its `RelayRx` are only forwarded to `enrollers[0]`"); + + for (uint8_t i = 0; i < kNumEnrollers; i++) + { + recvContext[i].Clear(); + } + + joiners[1]->Get().Up(); + SuccessOrQuit(joiners[1]->Get().Start(kPskd, + /* aProvisioningUrl */ nullptr, + /* aVendorName */ nullptr, + /* aVendorModel */ nullptr, + /* aVendorSwVersion */ nullptr, + /* aVendorData */ nullptr, + /* aCallback */ nullptr, + /* aContext */ nullptr)); + + nexus.AdvanceTime(8 * Time::kOneSecondInMsec); + + for (uint8_t i = 0; i < kNumEnrollers; i++) + { + Coap::Message *message = AsCoapMessagePtr(recvContext[i].mRelayRxMsgs.GetHead()); + Ip6::InterfaceIdentifier readIid; + uint16_t joinerRouterRloc; + + if (i != 0) + { + VerifyOrQuit(message == nullptr); + continue; + } + + VerifyOrQuit(message != nullptr); + + VerifyOrQuit(message->ReadType() == Coap::kTypeNonConfirmable); + VerifyOrQuit(message->ReadCode() == Coap::kCodePost); + SuccessOrQuit(Tlv::Find(*message, readIid)); + SuccessOrQuit(Tlv::Find(*message, joinerRouterRloc)); + + VerifyOrQuit(readIid == joinerIids[1]); + VerifyOrQuit(joinerRouterRloc = admitter.Get().GetRloc16()); + } + + joiners[1]->Get().Stop(); + + // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + Log("Send an `EnrollerJoinerAccept` message again accepting `joiners[1]` from `enrollers[0]`"); + + message = enrollers[0]->Get().NewPriorityConfirmablePostMessage(kUriEnrollerJoinerAccept); + VerifyOrQuit(message != nullptr); + + SuccessOrQuit(Tlv::Append(*message, joinerIids[1])); + + responseContexts[0].Clear(); + SuccessOrQuit(enrollers[0]->Get().SendMessage(*message, HandleResponse, &responseContexts[0])); + + nexus.AdvanceTime(Time::kOneSecondInMsec); + + VerifyOrQuit(responseContexts[0].mReceived); + VerifyOrQuit(responseContexts[0].mResponseState == MeshCoP::StateTlv::kAccept); + VerifyOrQuit(!responseContexts[0].mHasAdmitterState); + + // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + Log("Validate that there is no change in the `enrollers` list and the tracked `joiners` on `admitter`"); + + foundEnrollers.Clear(); + iter.Init(admitter.GetInstance()); + + while (iter.GetNextEnrollerInfo(enrollerInfo) == kErrorNone) + { + uint8_t matchedIndex; + + LogEnroller(enrollerInfo); + + matchedIndex = FindMatchingEnroller(enrollerInfo, kEnrollerIds, foundEnrollers); + + VerifyOrQuit(AsCoreType(&enrollerInfo.mSteeringData) == steeringData); + VerifyOrQuit(enrollerInfo.mMode == modes[matchedIndex]); + + if (matchedIndex == 0) + { + uint16_t numJoiners = 0; + + foundJoiners.Clear(); + + while (iter.GetNextJoinerInfo(joinerInfo) == kErrorNone) + { + LogJoiner(joinerInfo); + + numJoiners++; + + for (uint8_t j = 0; j < 2; j++) + { + if (joinerIids[j] == AsCoreType(&joinerInfo.mIid)) + { + VerifyOrQuit(!foundJoiners.Has(j)); + foundJoiners.Add(j); + } + } + } + + VerifyOrQuit(numJoiners == 2); + } + else + { + VerifyOrQuit(iter.GetNextJoinerInfo(joinerInfo) == kErrorNotFound); + } + + VerifyOrQuit(iter.GetNextJoinerInfo(joinerInfo) == kErrorNotFound); + } + + // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + Log("Send an `EnrollerJoinerRelease` message from `enrollers[0]` to `admitter` releasing `joiners[0]`"); + + message = enrollers[0]->Get().NewPriorityConfirmablePostMessage(kUriEnrollerJoinerRelease); + VerifyOrQuit(message != nullptr); + + SuccessOrQuit(Tlv::Append(*message, joinerIids[0])); + + responseContexts[0].Clear(); + SuccessOrQuit(enrollers[0]->Get().SendMessage(*message, HandleResponse, &responseContexts[0])); + + nexus.AdvanceTime(Time::kOneSecondInMsec); + + VerifyOrQuit(responseContexts[0].mReceived); + VerifyOrQuit(responseContexts[0].mResponseState == MeshCoP::StateTlv::kAccept); + VerifyOrQuit(!responseContexts[0].mHasAdmitterState); + + // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + Log("Validate that the released `joiners[0]` is removed on `admitter`"); + + foundEnrollers.Clear(); + iter.Init(admitter.GetInstance()); + + while (iter.GetNextEnrollerInfo(enrollerInfo) == kErrorNone) + { + uint8_t matchedIndex; + + LogEnroller(enrollerInfo); + + matchedIndex = FindMatchingEnroller(enrollerInfo, kEnrollerIds, foundEnrollers); + + VerifyOrQuit(AsCoreType(&enrollerInfo.mSteeringData) == steeringData); + VerifyOrQuit(enrollerInfo.mMode == modes[matchedIndex]); + + if (matchedIndex == 0) + { + SuccessOrQuit(iter.GetNextJoinerInfo(joinerInfo)); + VerifyOrQuit(AsCoreType(&joinerInfo.mIid) == joinerIids[1]); + LogJoiner(joinerInfo); + } + + VerifyOrQuit(iter.GetNextJoinerInfo(joinerInfo) == kErrorNotFound); + } + + VerifyOrQuit(DidFindAllEnrollers(foundEnrollers)); + + nexus.AdvanceTime(Time::kOneSecondInMsec); + + // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + Log("Send an `EnrollerJoinerRelease` message again releasing `joiners[0]` from `enrollers[0]`"); + + message = enrollers[0]->Get().NewPriorityConfirmablePostMessage(kUriEnrollerJoinerRelease); + VerifyOrQuit(message != nullptr); + + SuccessOrQuit(Tlv::Append(*message, joinerIids[0])); + + responseContexts[0].Clear(); + SuccessOrQuit(enrollers[0]->Get().SendMessage(*message, HandleResponse, &responseContexts[0])); + + Log("Validate that `EnrollerJoinerRelease` is accepted, even though the given IID is already removed"); + + nexus.AdvanceTime(Time::kOneSecondInMsec); + + VerifyOrQuit(responseContexts[0].mReceived); + VerifyOrQuit(responseContexts[0].mResponseState == MeshCoP::StateTlv::kAccept); + VerifyOrQuit(!responseContexts[0].mHasAdmitterState); + + // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + Log("Send an `EnrollerJoinerRelease` message releasing `joiners[1]` from `enrollers[0]`"); + + message = enrollers[0]->Get().NewPriorityConfirmablePostMessage(kUriEnrollerJoinerRelease); + VerifyOrQuit(message != nullptr); + + SuccessOrQuit(Tlv::Append(*message, joinerIids[1])); + + responseContexts[0].Clear(); + SuccessOrQuit(enrollers[0]->Get().SendMessage(*message, HandleResponse, &responseContexts[0])); + + nexus.AdvanceTime(Time::kOneSecondInMsec); + + VerifyOrQuit(responseContexts[0].mReceived); + VerifyOrQuit(responseContexts[0].mResponseState == MeshCoP::StateTlv::kAccept); + VerifyOrQuit(!responseContexts[0].mHasAdmitterState); + + // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + Log("Send two `EnrollerJoinerAccept` messages from `enrollers[2]` accepting both `joiners`"); + + for (uint8_t j = 0; j < 2; j++) + { + message = enrollers[2]->Get().NewPriorityConfirmablePostMessage(kUriEnrollerJoinerAccept); + VerifyOrQuit(message != nullptr); + + SuccessOrQuit(Tlv::Append(*message, joinerIids[j])); + + responseContexts[2].Clear(); + SuccessOrQuit( + enrollers[2]->Get().SendMessage(*message, HandleResponse, &responseContexts[2])); + + nexus.AdvanceTime(Time::kOneSecondInMsec); + + VerifyOrQuit(responseContexts[2].mReceived); + VerifyOrQuit(responseContexts[2].mResponseState == MeshCoP::StateTlv::kAccept); + VerifyOrQuit(!responseContexts[2].mHasAdmitterState); + } + + // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + Log("Validate that both accepted joiners are tracked by `enrollers[2]` on `admitter`"); + + foundEnrollers.Clear(); + iter.Init(admitter.GetInstance()); + + while (iter.GetNextEnrollerInfo(enrollerInfo) == kErrorNone) + { + uint8_t matchedIndex; + + LogEnroller(enrollerInfo); + + matchedIndex = FindMatchingEnroller(enrollerInfo, kEnrollerIds, foundEnrollers); + + VerifyOrQuit(AsCoreType(&enrollerInfo.mSteeringData) == steeringData); + VerifyOrQuit(enrollerInfo.mMode == modes[matchedIndex]); + + if (matchedIndex == 2) + { + uint16_t numJoiners = 0; + + foundJoiners.Clear(); + + while (iter.GetNextJoinerInfo(joinerInfo) == kErrorNone) + { + LogJoiner(joinerInfo); + + numJoiners++; + + for (uint8_t j = 0; j < 2; j++) + { + if (joinerIids[j] == AsCoreType(&joinerInfo.mIid)) + { + VerifyOrQuit(!foundJoiners.Has(j)); + foundJoiners.Add(j); + } + } + } + + VerifyOrQuit(numJoiners == 2); + } + else + { + VerifyOrQuit(iter.GetNextJoinerInfo(joinerInfo) == kErrorNotFound); + } + } + + VerifyOrQuit(DidFindAllEnrollers(foundEnrollers)); + + // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + Log("Send an `EnrollerJoinerRelease` message from `enrollers[2]` with wildcard IID releasing all joiners"); + + message = enrollers[2]->Get().NewPriorityConfirmablePostMessage(kUriEnrollerJoinerRelease); + VerifyOrQuit(message != nullptr); + + wildcardJoinerIid.Clear(); + SuccessOrQuit(Tlv::Append(*message, wildcardJoinerIid)); + + responseContexts[2].Clear(); + SuccessOrQuit(enrollers[2]->Get().SendMessage(*message, HandleResponse, &responseContexts[2])); + + nexus.AdvanceTime(Time::kOneSecondInMsec); + + VerifyOrQuit(responseContexts[2].mReceived); + VerifyOrQuit(responseContexts[2].mResponseState == MeshCoP::StateTlv::kAccept); + VerifyOrQuit(!responseContexts[2].mHasAdmitterState); + + // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + Log("Validate that all previously accepted joiners by `enrollers[2]` on `admitter` are now removed"); + + foundEnrollers.Clear(); + iter.Init(admitter.GetInstance()); + + while (iter.GetNextEnrollerInfo(enrollerInfo) == kErrorNone) + { + uint8_t matchedIndex; + + LogEnroller(enrollerInfo); + + matchedIndex = FindMatchingEnroller(enrollerInfo, kEnrollerIds, foundEnrollers); + + VerifyOrQuit(AsCoreType(&enrollerInfo.mSteeringData) == steeringData); + VerifyOrQuit(enrollerInfo.mMode == modes[matchedIndex]); + + VerifyOrQuit(iter.GetNextJoinerInfo(joinerInfo) == kErrorNotFound); + } + + // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + Log("Send an invalid `EnrollerJoinerAccept` message from enrollers[2] with wildcard IID"); + + message = enrollers[2]->Get().NewPriorityConfirmablePostMessage(kUriEnrollerJoinerAccept); + VerifyOrQuit(message != nullptr); + + wildcardJoinerIid.Clear(); + SuccessOrQuit(Tlv::Append(*message, wildcardJoinerIid)); + + responseContexts[2].Clear(); + SuccessOrQuit(enrollers[2]->Get().SendMessage(*message, HandleResponse, &responseContexts[2])); + + Log("Validate that the invalid `EnrollerJoinerAccept` is correctly rejected"); + + nexus.AdvanceTime(Time::kOneSecondInMsec); + + VerifyOrQuit(responseContexts[2].mReceived); + VerifyOrQuit(responseContexts[2].mResponseState == MeshCoP::StateTlv::kReject); + VerifyOrQuit(!responseContexts[2].mHasAdmitterState); + + // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + Log("Send two `EnrollerJoinerAccept` messages from `enrollers[2]` accepting both `joiners`"); + + for (uint8_t j = 0; j < 2; j++) + { + message = enrollers[2]->Get().NewPriorityConfirmablePostMessage(kUriEnrollerJoinerAccept); + VerifyOrQuit(message != nullptr); + + SuccessOrQuit(Tlv::Append(*message, joinerIids[j])); + + responseContexts[2].Clear(); + SuccessOrQuit( + enrollers[2]->Get().SendMessage(*message, HandleResponse, &responseContexts[2])); + + nexus.AdvanceTime(Time::kOneSecondInMsec); + + VerifyOrQuit(responseContexts[2].mReceived); + VerifyOrQuit(responseContexts[2].mResponseState == MeshCoP::StateTlv::kAccept); + VerifyOrQuit(!responseContexts[2].mHasAdmitterState); + } + + // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + Log("Validate that both accepted Joiners are tracked by `enrollers[2]` on `admitter`"); + + foundEnrollers.Clear(); + iter.Init(admitter.GetInstance()); + + while (iter.GetNextEnrollerInfo(enrollerInfo) == kErrorNone) + { + uint8_t matchedIndex; + + LogEnroller(enrollerInfo); + + matchedIndex = FindMatchingEnroller(enrollerInfo, kEnrollerIds, foundEnrollers); + + VerifyOrQuit(AsCoreType(&enrollerInfo.mSteeringData) == steeringData); + VerifyOrQuit(enrollerInfo.mMode == modes[matchedIndex]); + + if (matchedIndex == 2) + { + uint16_t numJoiners = 0; + + foundJoiners.Clear(); + + while (iter.GetNextJoinerInfo(joinerInfo) == kErrorNone) + { + LogJoiner(joinerInfo); + + numJoiners++; + + for (uint8_t j = 0; j < 2; j++) + { + if (joinerIids[j] == AsCoreType(&joinerInfo.mIid)) + { + VerifyOrQuit(!foundJoiners.Has(j)); + foundJoiners.Add(j); + } + } + + VerifyOrQuit(joinerInfo.mMsecTillExpiration >= 6 * Time::kOneMinuteInMsec); + } + + VerifyOrQuit(numJoiners == 2); + } + else + { + VerifyOrQuit(iter.GetNextJoinerInfo(joinerInfo) == kErrorNotFound); + } + } + + VerifyOrQuit(DidFindAllEnrollers(foundEnrollers)); + + // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + Log("Wait for 4 minutes, sending `EnrollerKeepAlive` every 30 seconds to maintain enroller connections"); + + for (uint8_t interval = 0; interval < 4 * 2; interval++) + { + Log("Send `EnrollerKeepAlive` message from all `enrollers` to maintain the connection"); + + for (uint8_t i = 0; i < kNumEnrollers; i++) + { + message = enrollers[i]->Get().NewPriorityConfirmablePostMessage(kUriEnrollerKeepAlive); + VerifyOrQuit(message != nullptr); + + SuccessOrQuit(Tlv::Append(*message, MeshCoP::StateTlv::kAccept)); + + responseContexts[i].Clear(); + SuccessOrQuit( + enrollers[i]->Get().SendMessage(*message, HandleResponse, &responseContexts[i])); + } + + nexus.AdvanceTime(Time::kOneSecondInMsec); + + for (uint8_t i = 0; i < kNumEnrollers; i++) + { + VerifyOrQuit(responseContexts[i].mReceived); + VerifyOrQuit(responseContexts[i].mResponseState == MeshCoP::StateTlv::kAccept); + VerifyOrQuit(responseContexts[i].mHasAdmitterState); + } + + nexus.AdvanceTime(29 * Time::kOneSecondInMsec); + } + + // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + Log("Validate the enroller list on `admitter` and that both joiners are still accepted by `enrollers[2]`"); + + foundEnrollers.Clear(); + iter.Init(admitter.GetInstance()); + + while (iter.GetNextEnrollerInfo(enrollerInfo) == kErrorNone) + { + uint8_t matchedIndex; + + LogEnroller(enrollerInfo); + + matchedIndex = FindMatchingEnroller(enrollerInfo, kEnrollerIds, foundEnrollers); + + VerifyOrQuit(AsCoreType(&enrollerInfo.mSteeringData) == steeringData); + VerifyOrQuit(enrollerInfo.mMode == modes[matchedIndex]); + + if (matchedIndex == 2) + { + uint16_t numJoiners = 0; + + foundJoiners.Clear(); + + while (iter.GetNextJoinerInfo(joinerInfo) == kErrorNone) + { + LogJoiner(joinerInfo); + + numJoiners++; + + for (uint8_t j = 0; j < 2; j++) + { + if (joinerIids[j] == AsCoreType(&joinerInfo.mIid)) + { + VerifyOrQuit(!foundJoiners.Has(j)); + foundJoiners.Add(j); + } + } + + // Since we waited for 4 minutes, the joiner expiration time should be closer + + VerifyOrQuit(joinerInfo.mMsecTillExpiration < 6 * Time::kOneMinuteInMsec); + } + + VerifyOrQuit(numJoiners == 2); + } + else + { + VerifyOrQuit(iter.GetNextJoinerInfo(joinerInfo) == kErrorNotFound); + } + } + + VerifyOrQuit(DidFindAllEnrollers(foundEnrollers)); + + // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + Log("Start `joiners[1]` and validate that its RelayRx are only forwarded to `enrollers[2]`"); + + for (uint8_t i = 0; i < kNumEnrollers; i++) + { + recvContext[i].Clear(); + } + + joiners[1]->Get().Up(); + SuccessOrQuit(joiners[1]->Get().Start(kPskd, + /* aProvisioningUrl */ nullptr, + /* aVendorName */ nullptr, + /* aVendorModel */ nullptr, + /* aVendorSwVersion */ nullptr, + /* aVendorData */ nullptr, + /* aCallback */ nullptr, + /* aContext */ nullptr)); + + nexus.AdvanceTime(8 * Time::kOneSecondInMsec); + + for (uint8_t i = 0; i < kNumEnrollers; i++) + { + Coap::Message *message = AsCoapMessagePtr(recvContext[i].mRelayRxMsgs.GetHead()); + Ip6::InterfaceIdentifier readIid; + uint16_t joinerRouterRloc; + + if (i != 2) + { + VerifyOrQuit(message == nullptr); + continue; + } + + VerifyOrQuit(message != nullptr); + + VerifyOrQuit(message->ReadType() == Coap::kTypeNonConfirmable); + VerifyOrQuit(message->ReadCode() == Coap::kCodePost); + SuccessOrQuit(Tlv::Find(*message, readIid)); + SuccessOrQuit(Tlv::Find(*message, joinerRouterRloc)); + + VerifyOrQuit(readIid == joinerIids[1]); + VerifyOrQuit(joinerRouterRloc = admitter.Get().GetRloc16()); + } + + joiners[1]->Get().Stop(); + + // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + Log("Validate expiration is extended for joiners[1] after it transmitted"); + + foundEnrollers.Clear(); + iter.Init(admitter.GetInstance()); + + while (iter.GetNextEnrollerInfo(enrollerInfo) == kErrorNone) + { + uint8_t matchedIndex; + + LogEnroller(enrollerInfo); + + matchedIndex = FindMatchingEnroller(enrollerInfo, kEnrollerIds, foundEnrollers); + + VerifyOrQuit(AsCoreType(&enrollerInfo.mSteeringData) == steeringData); + VerifyOrQuit(enrollerInfo.mMode == modes[matchedIndex]); + + if (matchedIndex == 2) + { + uint16_t numJoiners = 0; + + foundJoiners.Clear(); + + while (iter.GetNextJoinerInfo(joinerInfo) == kErrorNone) + { + LogJoiner(joinerInfo); + + numJoiners++; + + for (uint8_t j = 0; j < 2; j++) + { + if (joinerIids[j] == AsCoreType(&joinerInfo.mIid)) + { + VerifyOrQuit(!foundJoiners.Has(j)); + foundJoiners.Add(j); + + // `joiners[0]` expiration should still tick down, while `joiners[1]`'s should be extended + + if (j == 0) + { + VerifyOrQuit(joinerInfo.mMsecTillExpiration < 6 * Time::kOneMinuteInMsec); + } + else + { + VerifyOrQuit(joinerInfo.mMsecTillExpiration >= 6 * Time::kOneMinuteInMsec); + } + } + } + } + + VerifyOrQuit(numJoiners == 2); + } + else + { + VerifyOrQuit(iter.GetNextJoinerInfo(joinerInfo) == kErrorNotFound); + } + } + + VerifyOrQuit(DidFindAllEnrollers(foundEnrollers)); + + // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + Log("Wait for another 4 minutes, sending `EnrollerKeepAlive` every 30 seconds"); + + for (uint8_t interval = 0; interval < 4 * 2; interval++) + { + Log("Send `EnrollerKeepAlive` message from all `enrollers` to maintain the connection"); + + for (uint8_t i = 0; i < kNumEnrollers; i++) + { + message = enrollers[i]->Get().NewPriorityConfirmablePostMessage(kUriEnrollerKeepAlive); + VerifyOrQuit(message != nullptr); + + SuccessOrQuit(Tlv::Append(*message, MeshCoP::StateTlv::kAccept)); + + responseContexts[i].Clear(); + SuccessOrQuit( + enrollers[i]->Get().SendMessage(*message, HandleResponse, &responseContexts[i])); + } + + nexus.AdvanceTime(Time::kOneSecondInMsec); + + for (uint8_t i = 0; i < kNumEnrollers; i++) + { + VerifyOrQuit(responseContexts[i].mReceived); + VerifyOrQuit(responseContexts[i].mResponseState == MeshCoP::StateTlv::kAccept); + VerifyOrQuit(responseContexts[i].mHasAdmitterState); + } + + nexus.AdvanceTime(29 * Time::kOneSecondInMsec); + } + + // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + Log("Validate that `joiners[0]` is timed out and removed on `admitter`"); + + foundEnrollers.Clear(); + iter.Init(admitter.GetInstance()); + + while (iter.GetNextEnrollerInfo(enrollerInfo) == kErrorNone) + { + uint8_t matchedIndex; + + LogEnroller(enrollerInfo); + + matchedIndex = FindMatchingEnroller(enrollerInfo, kEnrollerIds, foundEnrollers); + + VerifyOrQuit(AsCoreType(&enrollerInfo.mSteeringData) == steeringData); + VerifyOrQuit(enrollerInfo.mMode == modes[matchedIndex]); + + if (matchedIndex == 2) + { + SuccessOrQuit(iter.GetNextJoinerInfo(joinerInfo)); + + LogJoiner(joinerInfo); + + VerifyOrQuit(joinerIids[1] == AsCoreType(&joinerInfo.mIid)); + VerifyOrQuit(joinerInfo.mMsecTillExpiration > 0); + } + + VerifyOrQuit(iter.GetNextJoinerInfo(joinerInfo) == kErrorNotFound); + } + + // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + Log("Wait for another 4 minutes, sending `EnrollerKeepAlive` every 30 seconds"); + + for (uint8_t interval = 0; interval < 4 * 2; interval++) + { + Log("Send `EnrollerKeepAlive` message from all `enrollers` to maintain the connection"); + + for (uint8_t i = 0; i < kNumEnrollers; i++) + { + message = enrollers[i]->Get().NewPriorityConfirmablePostMessage(kUriEnrollerKeepAlive); + VerifyOrQuit(message != nullptr); + + SuccessOrQuit(Tlv::Append(*message, MeshCoP::StateTlv::kAccept)); + + responseContexts[i].Clear(); + SuccessOrQuit( + enrollers[i]->Get().SendMessage(*message, HandleResponse, &responseContexts[i])); + } + + nexus.AdvanceTime(Time::kOneSecondInMsec); + + for (uint8_t i = 0; i < kNumEnrollers; i++) + { + VerifyOrQuit(responseContexts[i].mReceived); + VerifyOrQuit(responseContexts[i].mResponseState == MeshCoP::StateTlv::kAccept); + VerifyOrQuit(responseContexts[i].mHasAdmitterState); + } + + nexus.AdvanceTime(29 * Time::kOneSecondInMsec); + } + + // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + Log("Validate that `joiners[1]` is also timed out and removed on `admitter`"); + + foundEnrollers.Clear(); + iter.Init(admitter.GetInstance()); + + while (iter.GetNextEnrollerInfo(enrollerInfo) == kErrorNone) + { + uint8_t matchedIndex; + + LogEnroller(enrollerInfo); + + matchedIndex = FindMatchingEnroller(enrollerInfo, kEnrollerIds, foundEnrollers); + + VerifyOrQuit(AsCoreType(&enrollerInfo.mSteeringData) == steeringData); + VerifyOrQuit(enrollerInfo.mMode == modes[matchedIndex]); + VerifyOrQuit(iter.GetNextJoinerInfo(joinerInfo) == kErrorNotFound); + } + + VerifyOrQuit(DidFindAllEnrollers(foundEnrollers)); +} + +void TestBorderAdmitterForwardingUdpProxy(void) +{ + static constexpr uint8_t kNumEnrollers = 4; + + static const char *kEnrollerIds[kNumEnrollers] = {"1", "2", "3", "4"}; + + static const uint8_t kDiagTlvs[] = {NetworkDiagnostic::Tlv::kExtMacAddress, NetworkDiagnostic::Tlv::kVersion}; + + Core nexus; + Node &admitter = nexus.CreateNode(); + Node *enrollers[kNumEnrollers]; + Ip6::SockAddr sockAddr; + Pskc pskc; + Coap::Message *message; + Coap::Message *diagMessage; + uint8_t modes[kNumEnrollers]; + ResponseContext responseContexts[kNumEnrollers]; + ReceiveContext recvContext[kNumEnrollers]; + MeshCoP::SteeringData steeringData; + MeshCoP::SteeringData leaderSteeringData; + Admitter::Iterator iter; + Admitter::EnrollerInfo enrollerInfo; + Admitter::JoinerInfo joinerInfo; + BitSet foundEnrollers; + uint16_t sessionId; + uint16_t rloc16; + MeshCoP::UdpEncapsulationTlvHeader udpEncapHeader; + ExtendedTlv extTlv; + + Log("------------------------------------------------------------------------------------------------------"); + Log("TestBorderAdmitterForwardingUdpProxy"); + + for (uint8_t i = 0; i < kNumEnrollers; i++) + { + enrollers[i] = &nexus.CreateNode(); + } + + nexus.AdvanceTime(0); + + // Form the topology: + // - `admitter` forms the network (as leader) + // - All enrollers stay disconnected. + + admitter.Form(); + nexus.AdvanceTime(50 * Time::kOneSecondInMsec); + + VerifyOrQuit(admitter.Get().IsLeader()); + + for (Node *enroller : enrollers) + { + SuccessOrQuit(enroller->Get().SetPanChannel(admitter.Get().GetPanChannel())); + enroller->Get().SetPanId(admitter.Get().GetPanId()); + enroller->Get().Up(); + } + + // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + Log("Enable Border Admitter on `admitter`"); + + admitter.Get().SetEnabled(true); + VerifyOrQuit(admitter.Get().IsEnabled()); + VerifyOrQuit(!admitter.Get().IsPrimeAdmitter()); + + nexus.AdvanceTime(45 * Time::kOneSecondInMsec); + + VerifyOrQuit(admitter.Get().IsPrimeAdmitter()); + VerifyOrQuit(!admitter.Get().IsActiveCommissioner()); + + SuccessOrQuit(admitter.Get().AddUnsecurePort(admitter.Get().GetUdpPort())); + + // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + Log("Establish a DTLS connection from all `enrollers` to `admitter`"); + + sockAddr.SetAddress(admitter.Get().GetLinkLocalAddress()); + sockAddr.SetPort(admitter.Get().GetUdpPort()); + + admitter.Get().GetPskc(pskc); + + for (uint8_t i = 0; i < kNumEnrollers; i++) + { + Node *enroller = enrollers[i]; + + SuccessOrQuit(enroller->Get().SetPsk(pskc.m8, Pskc::kSize)); + + recvContext[i].Clear(); + enroller->Get().RegisterResourceHandler(HandleResource, &recvContext[i]); + + SuccessOrQuit(enroller->Get().Open()); + SuccessOrQuit(enroller->Get().Connect(sockAddr)); + + nexus.AdvanceTime(Time::kOneSecondInMsec); + + VerifyOrQuit(enroller->Get().IsConnected()); + } + + // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + Log("Prepare mode for each enroller"); + + modes[0] = MeshCoP::EnrollerModeTlv::kForwardJoinerRelayRx | MeshCoP::EnrollerModeTlv::kForwardUdpProxyRx; + modes[1] = MeshCoP::EnrollerModeTlv::kForwardJoinerRelayRx | MeshCoP::EnrollerModeTlv::kForwardUdpProxyRx; + modes[2] = MeshCoP::EnrollerModeTlv::kForwardUdpProxyRx; + modes[3] = MeshCoP::EnrollerModeTlv::kForwardJoinerRelayRx; + + steeringData.SetToPermitAllJoiners(); + + // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + Log("Send an `EnrollerRegister` message from all `enrollers`"); + + for (uint8_t i = 0; i < kNumEnrollers; i++) + { + message = enrollers[i]->Get().NewPriorityConfirmablePostMessage(kUriEnrollerRegister); + VerifyOrQuit(message != nullptr); + + SuccessOrQuit(Tlv::Append(*message, kEnrollerIds[i])); + SuccessOrQuit(Tlv::Append(*message, modes[i])); + SuccessOrQuit( + Tlv::Append(*message, steeringData.GetData(), steeringData.GetLength())); + + responseContexts[i].Clear(); + SuccessOrQuit( + enrollers[i]->Get().SendMessage(*message, HandleResponse, &responseContexts[i])); + } + + nexus.AdvanceTime(Time::kOneSecondInMsec); + + // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + Log("Check that all registrations were accepted"); + + for (uint8_t i = 0; i < kNumEnrollers; i++) + { + VerifyOrQuit(responseContexts[i].mReceived); + VerifyOrQuit(responseContexts[i].mResponseState == MeshCoP::StateTlv::kAccept); + VerifyOrQuit(responseContexts[i].mHasAdmitterState); + } + + // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + Log("Validate that `admitter` becomes active commissioner"); + + nexus.AdvanceTime(Time::kOneSecondInMsec); + + VerifyOrQuit(admitter.Get().IsPrimeAdmitter()); + VerifyOrQuit(admitter.Get().IsActiveCommissioner()); + + SuccessOrQuit(admitter.Get().FindBorderAgentRloc(rloc16)); + VerifyOrQuit(rloc16 == admitter.Get().GetRloc16()); + + SuccessOrQuit(admitter.Get().FindCommissioningSessionId(sessionId)); + VerifyOrQuit(sessionId == admitter.Get().GetCommissionerSessionId()); + + SuccessOrQuit(admitter.Get().FindSteeringData(leaderSteeringData)); + VerifyOrQuit(leaderSteeringData == steeringData); + VerifyOrQuit(leaderSteeringData.PermitsAllJoiners()); + + // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + Log("Validate the Enroller info on `admitter`"); + + foundEnrollers.Clear(); + iter.Init(admitter.GetInstance()); + + while (iter.GetNextEnrollerInfo(enrollerInfo) == kErrorNone) + { + uint8_t matchedIndex; + + LogEnroller(enrollerInfo); + + matchedIndex = FindMatchingEnroller(enrollerInfo, kEnrollerIds, foundEnrollers); + + VerifyOrQuit(AsCoreType(&enrollerInfo.mSteeringData) == steeringData); + VerifyOrQuit(enrollerInfo.mMode == modes[matchedIndex]); + + VerifyOrQuit(iter.GetNextJoinerInfo(joinerInfo) == kErrorNotFound); + } + + VerifyOrQuit(DidFindAllEnrollers(foundEnrollers)); + + // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + Log("Prepare a `DiagnosticGetQuery` message"); + + diagMessage = enrollers[0]->Get().NewNonConfirmablePostMessage(kUriDiagnosticGetQuery); + VerifyOrQuit(diagMessage != nullptr); + SuccessOrQuit(Tlv::Append(*diagMessage, kDiagTlvs, sizeof(kDiagTlvs))); + diagMessage->WriteMessageId(0); + + // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + Log("Embed the `DiagnosticGetQuery` into `ProxyTx` message and send it from `enrollers[0]`"); + + message = enrollers[0]->Get().NewPriorityNonConfirmablePostMessage(kUriProxyTx); + VerifyOrQuit(message != nullptr); + + udpEncapHeader.SetSourcePort(Tmf::kUdpPort); + udpEncapHeader.SetDestinationPort(Tmf::kUdpPort); + + extTlv.SetType(MeshCoP::Tlv::kUdpEncapsulation); + extTlv.SetLength(sizeof(udpEncapHeader) + diagMessage->GetLength()); + + SuccessOrQuit(message->Append(extTlv)); + SuccessOrQuit(message->Append(udpEncapHeader)); + SuccessOrQuit(message->AppendBytesFromMessage(*diagMessage, 0, diagMessage->GetLength())); + diagMessage->Free(); + + SuccessOrQuit(Tlv::Append(*message, admitter.Get().GetMeshLocalRloc())); + + SuccessOrQuit(enrollers[0]->Get().SendMessage(*message)); + + nexus.AdvanceTime(Time::kOneSecondInMsec); + + // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + Log("Validate that `admitter` receives the `DiagnosticGetQuery` response"); + Log("And that it forwards it as `ProxyRx` messages to all `enrollers` with `kForwardUdpProxyRx` mode flag"); + + for (uint8_t i = 0; i < kNumEnrollers; i++) + { + Coap::Message *message = AsCoapMessagePtr(recvContext[i].mProxyRxMsgs.GetHead()); + Ip6::Address senderAddr; + OffsetRange offsetRange; + + if ((modes[i] & MeshCoP::EnrollerModeTlv::kForwardUdpProxyRx) == 0) + { + VerifyOrQuit(message == nullptr); + Log(" Enroller %s does not set `kForwardUdpProxyRx` mode - so did not get `ProxyRx`", kEnrollerIds[i]); + continue; + } + + VerifyOrQuit(message != nullptr); + + VerifyOrQuit(message->ReadType() == Coap::kTypeNonConfirmable); + VerifyOrQuit(message->ReadCode() == Coap::kCodePost); + + SuccessOrQuit(Tlv::FindTlvValueOffsetRange(*message, MeshCoP::Tlv::kUdpEncapsulation, offsetRange)); + + SuccessOrQuit(Tlv::Find(*message, senderAddr)); + VerifyOrQuit(senderAddr == admitter.Get().GetMeshLocalRloc()); + + Log(" Enroller %s received `ProxyRx` from %s", kEnrollerIds[i], senderAddr.ToString().AsCString()); + } +} + +//--------------------------------------------------------------------------------------------------------------------- + +static constexpr uint32_t kInfraIfIndex = 1; + +void ValidateAdmitterMdnsService(Node &aNode) +{ + static const char kDefaultServiceBaseName[] = OPENTHREAD_CONFIG_BORDER_AGENT_MESHCOP_SERVICE_BASE_NAME; + + Dns::Multicast::Core::Iterator *iterator; + Dns::Multicast::Core::Service service; + Dns::Multicast::Core::EntryState entryState; + + iterator = aNode.Get().AllocateIterator(); + VerifyOrQuit(iterator != nullptr); + + SuccessOrQuit(aNode.Get().GetNextService(*iterator, service, entryState)); + + Log(" HostName: %s", service.mHostName); + Log(" ServiceInstance: %s", service.mServiceInstance); + Log(" ServiceType: %s", service.mServiceType); + + for (uint16_t i = 0; i < service.mSubTypeLabelsLength; i++) + { + Log(" SubType: %s", service.mSubTypeLabels[i]); + } + + Log(" Port: %u", service.mPort); + Log(" TTL: %lu", ToUlong(service.mTtl)); + + VerifyOrQuit(StringMatch(service.mServiceType, "_meshcop._udp")); + VerifyOrQuit(StringStartsWith(service.mServiceInstance, kDefaultServiceBaseName)); + VerifyOrQuit(StringStartsWith(service.mHostName, "ot")); + VerifyOrQuit(service.mPort == aNode.Get().GetUdpPort()); + VerifyOrQuit(service.mTtl > 0); + VerifyOrQuit(service.mInfraIfIndex == 1); + VerifyOrQuit(entryState == OT_MDNS_ENTRY_STATE_REGISTERED); + + if (aNode.Get().IsPrimeAdmitter()) + { + VerifyOrQuit(service.mSubTypeLabelsLength == 1); + VerifyOrQuit(StringMatch(service.mSubTypeLabels[0], "_admitter")); + } + else + { + VerifyOrQuit(service.mSubTypeLabelsLength == 0); + } + + VerifyOrQuit(aNode.Get().GetNextService(*iterator, service, entryState) == kErrorNotFound); + + aNode.Get().FreeIterator(*iterator); +} + +//--------------------------------------------------------------------------------------------------------------------- + +void TestBorderAdmitterDnssdService(void) +{ + Core nexus; + Node &node1 = nexus.CreateNode(); + Node &node2 = nexus.CreateNode(); + Dns::Multicast::Core::Iterator *iterator; + Dns::Multicast::Core::Service service; + Dns::Multicast::Core::EntryState entryState; + + Log("------------------------------------------------------------------------------------------------------"); + Log("TestBorderAdmitterDnssdService"); + + nexus.AdvanceTime(0); + + // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + // Enable mDNS + SuccessOrQuit(node1.Get().SetEnabled(true, kInfraIfIndex)); + VerifyOrQuit(node1.Get().IsEnabled()); + SuccessOrQuit(node2.Get().SetEnabled(true, kInfraIfIndex)); + VerifyOrQuit(node2.Get().IsEnabled()); + + // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + // Form the topology. + + node1.Form(); + nexus.AdvanceTime(50 * Time::kOneSecondInMsec); + node2.Join(node1); + + nexus.AdvanceTime(10 * Time::kOneMinuteInMsec); + + VerifyOrQuit(node1.Get().IsLeader()); + VerifyOrQuit(node2.Get().IsRouter()); + + // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + Log("Check Border Admitter initial state"); + + VerifyOrQuit(!node1.Get().IsEnabled()); + VerifyOrQuit(!node2.Get().IsEnabled()); + + // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + Log("Enable Admitter role on `node1` and validate that it becomes the Prime Admitter"); + + node1.Get().SetEnabled(true); + VerifyOrQuit(node1.Get().IsEnabled()); + + nexus.AdvanceTime(45 * Time::kOneSecondInMsec); + + VerifyOrQuit(node1.Get().IsPrimeAdmitter()); + VerifyOrQuit(!node1.Get().IsActiveCommissioner()); + + // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + Log("Validate the registered mDNS MeshCop service by `node1` including `_admitter` sub-type"); + + ValidateAdmitterMdnsService(node1); + // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + Log("Validate the registered mDNS MeshCop service by `node2` (should not have `_admitter` sub-type)"); + + ValidateAdmitterMdnsService(node2); + // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + Log("Enable Admitter role on `node2` and validate that `node1` remains the Prime Admitter"); + + node2.Get().SetEnabled(true); + VerifyOrQuit(node2.Get().IsEnabled()); + + nexus.AdvanceTime(45 * Time::kOneSecondInMsec); + + VerifyOrQuit(node1.Get().IsPrimeAdmitter()); + VerifyOrQuit(!node1.Get().IsActiveCommissioner()); + VerifyOrQuit(!node2.Get().IsPrimeAdmitter()); + + // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + Log("Validate the registered mDNS MeshCop service by `node1` including `_admitter` sub-type"); + + ValidateAdmitterMdnsService(node1); + // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + Log("Validate the registered mDNS MeshCop service by `node2` (should not have `_admitter` sub-type)"); + ValidateAdmitterMdnsService(node2); + + // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + Log("Disable Admitter role on `node1` and check that `node2` becomes the Prime Admitter"); + + node1.Get().SetEnabled(false); + VerifyOrQuit(!node1.Get().IsEnabled()); + VerifyOrQuit(!node1.Get().IsPrimeAdmitter()); + + nexus.AdvanceTime(75 * Time::kOneSecondInMsec); + + VerifyOrQuit(node2.Get().IsPrimeAdmitter()); + VerifyOrQuit(!node2.Get().IsActiveCommissioner()); + + // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + Log("Validate the registered mDNS MeshCop service by `node1` (no longer publishing `_admitter` sub-type)"); + + ValidateAdmitterMdnsService(node1); + + // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + Log("Validate the registered mDNS MeshCop service by `node2` (now should include `_admitter` sub-type)"); + ValidateAdmitterMdnsService(node2); +} + +} // namespace Nexus +} // namespace ot + +int main(void) +{ + ot::Nexus::TestBorderAdmitterPrimeSelection(); + ot::Nexus::TestBorderAdmitterEnrollerInteraction(); + ot::Nexus::TestBorderAdmitterCommissionerConflictAndPetitionerRetry(); + ot::Nexus::TestBorderAdmitterMultipleEnrollers(); + ot::Nexus::TestBorderAdmitterJoinerEnrollerInteraction(); + ot::Nexus::TestBorderAdmitterForwardingUdpProxy(); + ot::Nexus::TestBorderAdmitterDnssdService(); + + printf("\nAll tests passed\n"); + return 0; +} diff --git a/tests/nexus/test_border_agent.cpp b/tests/nexus/test_border_agent.cpp index 28239b48c..03db80d0b 100644 --- a/tests/nexus/test_border_agent.cpp +++ b/tests/nexus/test_border_agent.cpp @@ -40,6 +40,7 @@ using ActiveDatasetManager = MeshCoP::ActiveDatasetManager; using Manager = MeshCoP::BorderAgent::Manager; using BaTxtData = MeshCoP::BorderAgent::TxtData; using EphemeralKeyManager = MeshCoP::BorderAgent::EphemeralKeyManager; +using Admitter = MeshCoP::BorderAgent::Admitter; using EpskcEvent = HistoryTracker::EpskcEvent; using Iterator = HistoryTracker::Iterator; using NetworkIdentity = MeshCoP::NetworkIdentity; @@ -1308,6 +1309,7 @@ void ValidateMeshCoPTxtData(TxtData &aTxtData, Node &aNode) static constexpr uint32_t kThreadRoleRouter = 2 << 9; static constexpr uint32_t kThreadRoleLeader = 3 << 9; static constexpr uint32_t kFlagEpskcSupported = 1 << 11; + static constexpr uint32_t kFlagAdmitterSupported = 1 << 14; MeshCoP::BorderAgent::Id id; BaTxtData::Info info; @@ -1434,6 +1436,17 @@ void ValidateMeshCoPTxtData(TxtData &aTxtData, Node &aNode) VerifyOrQuit(!(stateBitmap & kFlagEpskcSupported)); VerifyOrQuit(!info.mStateBitmap.mEpskcSupported); } + + if (aNode.Get().IsEnabled()) + { + VerifyOrQuit(stateBitmap & kFlagAdmitterSupported); + VerifyOrQuit(info.mStateBitmap.mAdmitterSupported); + } + else + { + VerifyOrQuit(!(stateBitmap & kFlagAdmitterSupported)); + VerifyOrQuit(!info.mStateBitmap.mAdmitterSupported); + } } //---------------------------------------------------------------------------------------------------------------------- @@ -1888,6 +1901,86 @@ void TestBorderAgentServiceRegistration(void) sSrvOutcomes.Clear(); sTxtOutcomes.Clear(); + // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + Log("Enable Admitter function"); + + node0.Get().SetEnabled(true); + VerifyOrQuit(node0.Get().IsEnabled()); + + nexus.AdvanceTime(5 * Time::kOneSecondInMsec); + + // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + Log("Check the registered service"); + + iterator = node0.Get().AllocateIterator(); + VerifyOrQuit(iterator != nullptr); + + iterator = node0.Get().AllocateIterator(); + VerifyOrQuit(iterator != nullptr); + + SuccessOrQuit(node0.Get().GetNextService(*iterator, service, entryState)); + Log(" HostName: %s", service.mHostName); + Log(" ServiceInstance: %s", service.mServiceInstance); + Log(" ServiceType: %s", service.mServiceType); + Log(" Port: %u", service.mPort); + Log(" TTL: %lu", ToUlong(service.mTtl)); + + VerifyOrQuit(StringMatch(service.mServiceType, "_meshcop._udp")); + VerifyOrQuit(StringStartsWith(service.mServiceInstance, "OpenThreadAgent")); + VerifyOrQuit(StringStartsWith(service.mHostName, "ot")); + VerifyOrQuit(service.mSubTypeLabelsLength == 0); + VerifyOrQuit(service.mPort == node0.Get().GetUdpPort()); + VerifyOrQuit(service.mTtl > 0); + VerifyOrQuit(service.mInfraIfIndex == kInfraIfIndex); + VerifyOrQuit(entryState == OT_MDNS_ENTRY_STATE_REGISTERED); + ValidateRegisteredServiceData(service, node0); + + // Check that there is no more registered mDNS service + VerifyOrQuit(node0.Get().GetNextService(*iterator, service, entryState) == kErrorNotFound); + + node0.Get().FreeIterator(*iterator); + + nexus.AdvanceTime(10 * Time::kOneSecondInMsec); + + // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + Log("Disable Admitter function"); + + node0.Get().SetEnabled(false); + VerifyOrQuit(!node0.Get().IsEnabled()); + + nexus.AdvanceTime(5 * Time::kOneSecondInMsec); + + // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + Log("Check the registered service"); + + iterator = node0.Get().AllocateIterator(); + VerifyOrQuit(iterator != nullptr); + + iterator = node0.Get().AllocateIterator(); + VerifyOrQuit(iterator != nullptr); + + SuccessOrQuit(node0.Get().GetNextService(*iterator, service, entryState)); + Log(" HostName: %s", service.mHostName); + Log(" ServiceInstance: %s", service.mServiceInstance); + Log(" ServiceType: %s", service.mServiceType); + Log(" Port: %u", service.mPort); + Log(" TTL: %lu", ToUlong(service.mTtl)); + + VerifyOrQuit(StringMatch(service.mServiceType, "_meshcop._udp")); + VerifyOrQuit(StringStartsWith(service.mServiceInstance, "OpenThreadAgent")); + VerifyOrQuit(StringStartsWith(service.mHostName, "ot")); + VerifyOrQuit(service.mSubTypeLabelsLength == 0); + VerifyOrQuit(service.mPort == node0.Get().GetUdpPort()); + VerifyOrQuit(service.mTtl > 0); + VerifyOrQuit(service.mInfraIfIndex == kInfraIfIndex); + VerifyOrQuit(entryState == OT_MDNS_ENTRY_STATE_REGISTERED); + ValidateRegisteredServiceData(service, node0); + + // Check that there is no more registered mDNS service + VerifyOrQuit(node0.Get().GetNextService(*iterator, service, entryState) == kErrorNotFound); + + node0.Get().FreeIterator(*iterator); + // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Log("Disable Border Agent and validate that registered service is removed"); diff --git a/tests/toranj/openthread-core-toranj-config.h b/tests/toranj/openthread-core-toranj-config.h index 6bd406479..cacbe729b 100644 --- a/tests/toranj/openthread-core-toranj-config.h +++ b/tests/toranj/openthread-core-toranj-config.h @@ -87,6 +87,8 @@ #define OPENTHREAD_CONFIG_BORDER_AGENT_COMMISSIONER_EVICTION_API_ENABLE 1 +#define OPENTHREAD_CONFIG_BORDER_AGENT_ADMITTER_ENABLE 1 + #define OPENTHREAD_CONFIG_BORDER_AGENT_TRACKER_ENABLE 1 #define OPENTHREAD_CONFIG_BORDER_AGENT_TXT_DATA_PARSER_ENABLE 1