diff --git a/include/openthread/border_agent.h b/include/openthread/border_agent.h index 39779fcf6..415615d52 100644 --- a/include/openthread/border_agent.h +++ b/include/openthread/border_agent.h @@ -383,6 +383,23 @@ otError otBorderAgentGetNextSessionInfo(otBorderAgentSessionIterator *aIterator, */ const otBorderAgentCounters *otBorderAgentGetCounters(otInstance *aInstance); +/** + * Forcefully evicts the current active Thread Commissioner. + * + * Requires `OPENTHREAD_CONFIG_BORDER_AGENT_COMMISSIONER_EVICTION_API_ENABLE`. + * + * This is intended as an administrator tool to address a misbehaving or stale commissioner session that may be + * connected through a different Border Agent. It provides a mechanism to clear the single Active Commissioner role + * within the Thread network, allowing a new candidate to be selected as the Active commissioner. + * + * @param[in] aInstance A pointer to an OpenThread instance. + * + * @retval OT_ERROR_NONE Successfully sent the eviction request to the Leader. + * @retval OT_ERROR_NOT_FOUND There is no active commissioner session to evict. + * @retval OT_ERROR_NO_BUFS Could not allocate a message buffer to send the request. + */ +otError otBorderAgentEvictActiveCommissioner(otInstance *aInstance); + /*-------------------------------------------------------------------------------------------------------------------- * Border Agent Ephemeral Key feature */ diff --git a/include/openthread/instance.h b/include/openthread/instance.h index b6ef9ed06..f1ae1679c 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 (556) +#define OPENTHREAD_API_VERSION (557) /** * @addtogroup api-instance diff --git a/src/cli/README.md b/src/cli/README.md index fe89ff8ef..50ca489ed 100644 --- a/src/cli/README.md +++ b/src/cli/README.md @@ -443,6 +443,19 @@ ba sessions Done ``` +### ba evictcommissioner + +Forcefully evicts the current active Thread Commissioner. + +Requires `OPENTHREAD_CONFIG_BORDER_AGENT_COMMISSIONER_EVICTION_API_ENABLE`. + +This command is intended as an administrator tool to address a misbehaving or stale commissioner session that may be connected through a different Border Agent. It provides a mechanism to clear the single Active Commissioner role within the Thread network, allowing a new candidate to be selected as the Active commissioner. + +```bash +> ba evictcommissioner +Done +``` + ### ba ephemeralkey Print the Border Agent's Ephemeral Key Manager state. diff --git a/src/cli/cli.cpp b/src/cli/cli.cpp index e408190d2..f1ef5daa9 100644 --- a/src/cli/cli.cpp +++ b/src/cli/cli.cpp @@ -588,6 +588,22 @@ template <> otError Interpreter::Process(Arg aArgs[]) } } #endif // OPENTHREAD_CONFIG_BORDER_AGENT_ID_ENABLE +#if OPENTHREAD_CONFIG_BORDER_AGENT_COMMISSIONER_EVICTION_API_ENABLE + /** + * @cli ba evictcommissioner + * @code + * ba evictcommissioner + * Done + * @endcode + * @par api_copy + * #otBorderAgentEvictActiveCommissioner + */ + else if (aArgs[0] == "evictcommissioner") + { + VerifyOrExit(aArgs[1].IsEmpty(), error = OT_ERROR_INVALID_ARGS); + error = otBorderAgentEvictActiveCommissioner(GetInstancePtr()); + } +#endif #if OPENTHREAD_CONFIG_BORDER_AGENT_EPHEMERAL_KEY_ENABLE else if (aArgs[0] == "ephemeralkey") { diff --git a/src/core/api/border_agent_api.cpp b/src/core/api/border_agent_api.cpp index ebd1b5075..2a492efa7 100644 --- a/src/core/api/border_agent_api.cpp +++ b/src/core/api/border_agent_api.cpp @@ -119,6 +119,13 @@ const otBorderAgentCounters *otBorderAgentGetCounters(otInstance *aInstance) return &AsCoreType(aInstance).Get().GetCounters(); } +#if OPENTHREAD_CONFIG_BORDER_AGENT_COMMISSIONER_EVICTION_API_ENABLE +otError otBorderAgentEvictActiveCommissioner(otInstance *aInstance) +{ + return AsCoreType(aInstance).Get().EvictActiveCommissioner(); +} +#endif + #if OPENTHREAD_CONFIG_BORDER_AGENT_EPHEMERAL_KEY_ENABLE otBorderAgentEphemeralKeyState otBorderAgentEphemeralKeyGetState(otInstance *aInstance) diff --git a/src/core/config/border_agent.h b/src/core/config/border_agent.h index e5532a191..14385d653 100644 --- a/src/core/config/border_agent.h +++ b/src/core/config/border_agent.h @@ -122,6 +122,18 @@ #define OPENTHREAD_CONFIG_BORDER_AGENT_MESHCOP_SERVICE_BASE_NAME "OpenThread BR (unspecified vendor) " #endif +/** + * @def OPENTHREAD_CONFIG_BORDER_AGENT_COMMISSIONER_EVICTION_API_ENABLE + * + * Define to 1 to enable the `otBorderAgentEvictActiveCommissioner()` API. + * + * This API provides a mechanism to evict the active Thread Commissioner from the network. This is primarily intended + * for administrative use to handle misbehaving or stale commissioner sessions. + */ +#ifndef OPENTHREAD_CONFIG_BORDER_AGENT_COMMISSIONER_EVICTION_API_ENABLE +#define OPENTHREAD_CONFIG_BORDER_AGENT_COMMISSIONER_EVICTION_API_ENABLE 0 +#endif + /** * @def OPENTHREAD_CONFIG_BORDER_AGENT_TRACKER_ENABLE * diff --git a/src/core/meshcop/border_agent.cpp b/src/core/meshcop/border_agent.cpp index fc797852e..a37e9187c 100644 --- a/src/core/meshcop/border_agent.cpp +++ b/src/core/meshcop/border_agent.cpp @@ -475,6 +475,37 @@ exit: #endif // OPENTHREAD_CONFIG_BORDER_AGENT_MESHCOP_SERVICE_ENABLE +#if OPENTHREAD_CONFIG_BORDER_AGENT_COMMISSIONER_EVICTION_API_ENABLE + +Error Manager::EvictActiveCommissioner(void) +{ + Error error = kErrorNone; + uint16_t sessionId; + uint16_t baRloc16; + Tmf::MessageInfo messageInfo(GetInstance()); + OwnedPtr message; + + SuccessOrExit(error = Get().FindBorderAgentRloc(baRloc16)); + SuccessOrExit(error = Get().FindCommissioningSessionId(sessionId)); + + message.Reset(Get().NewPriorityConfirmablePostMessage(kUriLeaderKeepAlive)); + VerifyOrExit(message != nullptr, error = kErrorNoBufs); + + SuccessOrExit(error = Tlv::Append(*message, StateTlv::kReject)); + SuccessOrExit(error = Tlv::Append(*message, sessionId)); + + messageInfo.SetSockAddrToRlocPeerAddrToLeaderAloc(); + messageInfo.SetSockPortToTmf(); + + SuccessOrExit(error = Get().SendMessage(*message, messageInfo)); + message.Release(); + +exit: + return error; +} + +#endif // OPENTHREAD_CONFIG_BORDER_AGENT_COMMISSIONER_EVICTION_API_ENABLE + //---------------------------------------------------------------------------------------------------------------------- // Manager::SessionIterator diff --git a/src/core/meshcop/border_agent.hpp b/src/core/meshcop/border_agent.hpp index 1ac7a50ce..fef489a5e 100644 --- a/src/core/meshcop/border_agent.hpp +++ b/src/core/meshcop/border_agent.hpp @@ -235,6 +235,21 @@ public: Error SetServiceBaseName(const char *aBaseName); #endif +#if OPENTHREAD_CONFIG_BORDER_AGENT_COMMISSIONER_EVICTION_API_ENABLE + /** + * Forcefully evicts the current active Thread Commissioner. + * + * This is intended as an administrator tool to address a misbehaving or stale commissioner session that may be + * connected through a different Border Agent. It provides a mechanism to clear the single Active Commissioner + * role within the Thread network, allowing a new candidate to be selected as the Active commissioner. + * + * @retval kErrorNone Successfully sent the eviction request to the Leader. + * @retval kErrorNotFound There is no active commissioner session to evict. + * @retval kErrorNoBufs Could not allocate a message buffer to send the request. + */ + Error EvictActiveCommissioner(void); +#endif + /** * Gets the set of border agent counters. * diff --git a/tests/toranj/cli/cli.py b/tests/toranj/cli/cli.py index a398f4bcb..1887d954e 100644 --- a/tests/toranj/cli/cli.py +++ b/tests/toranj/cli/cli.py @@ -456,6 +456,9 @@ class Node(object): def get_netdata_contexts(self): return self.get_netdata()['contexts'] + def get_netdata_commissioning(self): + return self.get_netdata()['commissioning'] + def get_netdata_versions(self): leaderdata = Node.parse_list(self.cli('leaderdata')) return (int(leaderdata['Data Version']), int(leaderdata['Stable Data Version'])) diff --git a/tests/toranj/cli/test-039-border-agent-evict-active-commissioner.py b/tests/toranj/cli/test-039-border-agent-evict-active-commissioner.py new file mode 100755 index 000000000..a59d34ef9 --- /dev/null +++ b/tests/toranj/cli/test-039-border-agent-evict-active-commissioner.py @@ -0,0 +1,111 @@ +#!/usr/bin/env python3 +# +# 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. + +from cli import verify +from cli import verify_within +import cli + +# ----------------------------------------------------------------------------------------------------------------------- +# Test description: +# This test covers the behavior of `BorderAgent::EvictActiveCommissioner()` +# + +test_name = __file__[:-3] if __file__.endswith('.py') else __file__ +print('-' * 120) +print('Starting \'{}\''.format(test_name)) + +# ----------------------------------------------------------------------------------------------------------------------- +# Creating `cli.Nodes` instances + +speedup = 25 +cli.Node.set_time_speedup_factor(speedup) + +leader = cli.Node() +commissioner = cli.Node() +agent = cli.Node() + +# ----------------------------------------------------------------------------------------------------------------------- +# Form topology +# + +leader.form('evictcmmr') +commissioner.join(leader) +agent.join(leader) + +verify(leader.get_state() == 'leader') +verify(commissioner.get_state() == 'router') +verify(agent.get_state() == 'router') + +# ----------------------------------------------------------------------------------------------------------------------- +# Test Implementation + +commissioner.cli('commissioner start') + + +def check_commissioner_state_is_active(): + verify(commissioner.cli('commissioner state')[0] == 'active') + + +verify_within(check_commissioner_state_is_active, 10) + +# Verify Commissioning Info in Network Data and that `commissioner` is accepted and active. + +data = leader.get_netdata_commissioning() +rloc16 = int(data[0].strip().split()[1], 16) +verify(rloc16 == int(commissioner.get_rloc16(), 16)) + +# Evict the current active commissioner. + +agent.cli('ba evictcommissioner') + +# Check that the Network Data Commissioning Info is cleared after eviction. + + +def check_netdata_commissioning_info(): + # check there is no active commissioner + data = leader.get_netdata_commissioning() + verify(data[0].strip().split()[1] == '-') + + +verify_within(check_netdata_commissioning_info, 10) + +# Check that the original commissioner's state becomes disabled + + +def check_commissioner_state_is_disabled(): + verify(commissioner.cli('commissioner state')[0] == 'disabled') + + +verify_within(check_commissioner_state_is_disabled, 60 / speedup) + +# ----------------------------------------------------------------------------------------------------------------------- +# Test finished + +cli.Node.finalize_all_nodes() + +print('\'{}\' passed.'.format(test_name)) diff --git a/tests/toranj/openthread-core-toranj-config.h b/tests/toranj/openthread-core-toranj-config.h index be419b82a..9721c6762 100644 --- a/tests/toranj/openthread-core-toranj-config.h +++ b/tests/toranj/openthread-core-toranj-config.h @@ -84,6 +84,8 @@ #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 diff --git a/tests/toranj/start.sh b/tests/toranj/start.sh index ecbe8af6a..46b1d71e1 100755 --- a/tests/toranj/start.sh +++ b/tests/toranj/start.sh @@ -204,6 +204,7 @@ if [ "$TORANJ_CLI" = 1 ]; then run cli/test-036-dhcp-prefix-netdata.py run cli/test-037-mtd-annc-join-older-timestamp.py run cli/test-038-simultaneous-parent-and-child-reset.py + run cli/test-039-border-agent-evict-active-commissioner.py run cli/test-400-srp-client-server.py run cli/test-401-srp-server-address-cache-snoop.py run cli/test-500-two-brs-two-networks.py