[border-agent] add commissioner eviction API (#12174)

This change adds `EvictActiveCommissioner()` to the Border Agent,
which sends a `LeaderKeepAlive` TMF message with a `StateTlv` of
`kReject` to the Leader, causing the current active commissioner
to be evicted.

The feature is exposed through:
- A new public C API `otBorderAgentEvictActiveCommissioner()`.
- A new CLI command `ba evictcommissioner`.

The entire feature is guarded by a new configuration flag,
`OPENTHREAD_CONFIG_BORDER_AGENT_COMMISSIONER_EVICTION_API_ENABLE`,
which is disabled by default.

This provides an administrator-level tool to remove a stale or
misbehaving commissioner, which is particularly useful when the
commissioner is connected through a different border agent and cannot be
managed locally.

A new test is also added to verify the eviction behavior.
This commit is contained in:
Abtin Keshavarzian
2025-12-02 21:26:10 -08:00
committed by GitHub
parent 7a76650d8b
commit 97598f88e8
12 changed files with 229 additions and 1 deletions
+17
View File
@@ -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 */
+1 -1
View File
@@ -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
+13
View File
@@ -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.
+16
View File
@@ -588,6 +588,22 @@ template <> otError Interpreter::Process<Cmd("ba")>(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")
{
+7
View File
@@ -119,6 +119,13 @@ const otBorderAgentCounters *otBorderAgentGetCounters(otInstance *aInstance)
return &AsCoreType(aInstance).Get<MeshCoP::BorderAgent::Manager>().GetCounters();
}
#if OPENTHREAD_CONFIG_BORDER_AGENT_COMMISSIONER_EVICTION_API_ENABLE
otError otBorderAgentEvictActiveCommissioner(otInstance *aInstance)
{
return AsCoreType(aInstance).Get<MeshCoP::BorderAgent::Manager>().EvictActiveCommissioner();
}
#endif
#if OPENTHREAD_CONFIG_BORDER_AGENT_EPHEMERAL_KEY_ENABLE
otBorderAgentEphemeralKeyState otBorderAgentEphemeralKeyGetState(otInstance *aInstance)
+12
View File
@@ -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
*
+31
View File
@@ -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<Coap::Message> message;
SuccessOrExit(error = Get<NetworkData::Leader>().FindBorderAgentRloc(baRloc16));
SuccessOrExit(error = Get<NetworkData::Leader>().FindCommissioningSessionId(sessionId));
message.Reset(Get<Tmf::Agent>().NewPriorityConfirmablePostMessage(kUriLeaderKeepAlive));
VerifyOrExit(message != nullptr, error = kErrorNoBufs);
SuccessOrExit(error = Tlv::Append<StateTlv>(*message, StateTlv::kReject));
SuccessOrExit(error = Tlv::Append<CommissionerSessionIdTlv>(*message, sessionId));
messageInfo.SetSockAddrToRlocPeerAddrToLeaderAloc();
messageInfo.SetSockPortToTmf();
SuccessOrExit(error = Get<Tmf::Agent>().SendMessage(*message, messageInfo));
message.Release();
exit:
return error;
}
#endif // OPENTHREAD_CONFIG_BORDER_AGENT_COMMISSIONER_EVICTION_API_ENABLE
//----------------------------------------------------------------------------------------------------------------------
// Manager::SessionIterator
+15
View File
@@ -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.
*
+3
View File
@@ -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']))
@@ -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))
@@ -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
+1
View File
@@ -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