[mle] block router downgrade if triggered by child ID request (#12725)

This commit updates the `Mle` router role downgrade logic. When a
REED transitions to a router in response to a Child ID Request, it
indicates that the attaching child has no other viable parent options.
To ensure this child remains connected to the mesh, this commit
prevents the newly promoted router from downgrading back to a REED.

A new flag `mBlockDowngrade` is added to `Mle`, and a matching
property `mBlockParentDowngrade` is added to `Child` to track if it is
blocking its parent's downgrade. The downgrade restriction is lifted
under specific conditions: when the device detaches, when a new router
is added to the network (providing a potential alternative parent for
the child), or when all children blocking the downgrade are removed.

A new nexus test `test_mle_blocking_downgrade` is added to validate
the new behavior.
This commit is contained in:
Abtin Keshavarzian
2026-03-25 10:47:36 -07:00
committed by GitHub
parent a5593e7980
commit 9b00c024a8
7 changed files with 305 additions and 9 deletions
+16
View File
@@ -336,6 +336,21 @@ public:
*/
void ResetSecondsSinceLastSupervision(void) { mSecondsSinceSupervision = 0; }
/**
* Indicates whether the child is blocking the parent from downgrading its router role.
*
* @retval TRUE The child is blocking the parent from downgrading.
* @retval FALSE The child is not blocking the parent from downgrading.
*/
bool IsBlockingParentDowngrade(void) const { return mBlockParentDowngrade; }
/**
* Sets whether the child is blocking the parent from downgrading its router role.
*
* @param[in] aBlock TRUE to block parent downgrade, FALSE otherwise.
*/
void SetBlockParentDowngrade(bool aBlock) { mBlockParentDowngrade = aBlock; }
#if OPENTHREAD_CONFIG_TMF_PROXY_MLR_ENABLE
/**
* Returns if the Child has IPv6 address @p aAddress of MLR state `kMlrStateRegistered`.
@@ -376,6 +391,7 @@ private:
ChildIp6AddressSet mMlrRegisteredSet;
#endif
bool mBlockParentDowngrade;
uint8_t mNetworkDataVersion;
union
+16
View File
@@ -81,6 +81,7 @@ Mle::Mle(Instance &aInstance)
#endif
#if OPENTHREAD_FTD
, mRouterEligible(true)
, mBlockDowngrade(false)
, mAddressSolicitPending(false)
, mAddressSolicitRejected(false)
#if OPENTHREAD_CONFIG_REFERENCE_DEVICE_ENABLE
@@ -600,6 +601,7 @@ void Mle::SetStateDetached(void)
Get<MeshForwarder>().SetRxOnWhenIdle(true);
Get<Mac::Mac>().SetBeaconEnabled(false);
#if OPENTHREAD_FTD
mBlockDowngrade = false;
ClearAlternateRloc16();
HandleDetachStart();
#endif
@@ -1043,6 +1045,20 @@ void Mle::HandleNotifierEvents(Events aEvents)
{
HandleSecurityPolicyChanged();
}
if (mBlockDowngrade && aEvents.Contains(kEventThreadChildRemoved))
{
mBlockDowngrade = false;
for (const Child &child : Get<ChildTable>().Iterate(Child::kInStateValid))
{
if (child.IsBlockingParentDowngrade())
{
mBlockDowngrade = true;
break;
}
}
}
#endif
if (aEvents.Contains(kEventSupportedChannelMaskChanged))
+22 -7
View File
@@ -131,6 +131,7 @@ class Mle : public InstanceLocator, private NonCopyable
#if OPENTHREAD_FTD
friend class ot::TimeTicker;
friend class Tmf::Agent;
friend class ot::RouterTable;
#endif
public:
@@ -870,6 +871,7 @@ public:
* @param[in] aPartitionId The preferred Leader Partition Id.
*/
void SetPreferredLeaderPartitionId(uint32_t aPartitionId) { mPreferredLeaderPartitionId = aPartitionId; }
#endif
/**
@@ -976,6 +978,24 @@ public:
*/
void SetRouterDowngradeThreshold(uint8_t aThreshold) { mRouterDowngradeThreshold = aThreshold; }
/**
* Indicates whether or not downgrading from router role to REED is blocked.
*
* If the transition to the router role was triggered by a Child ID Request, it indicates that the child has no
* other parent option. In this case the downgrade is blocked to prevent the parent router becoming REED to ensure
* this child remains connected. This flag is cleared in various situations:
*
* - When device detaches (e.g. partition change).
* - If a new router is added (new possible parent).
* - If all children blocking downgrade are disconnected.
*
* This method is intended for testing purposes only.
*
* @retval TRUE The device is blocked from downgrading.
* @retval FALSE The device is not blocked from downgrading.
*/
bool IsDowngradeBlocked(void) const { return mBlockDowngrade; }
/**
* Returns the MLE_CHILD_ROUTER_LINKS value.
*
@@ -1120,13 +1140,6 @@ public:
*/
void ResetAdvertiseInterval(void);
/**
* Updates the MLE Advertisement Trickle timer max interval (if timer is running).
*
* This is called when there is change in router table.
*/
void UpdateAdvertiseInterval(void);
#if OPENTHREAD_CONFIG_TIME_SYNC_ENABLE
/**
* Generates an MLE Time Synchronization message.
@@ -2433,6 +2446,7 @@ private:
bool NeighborHasComparableConnectivity(const RouteTlv &aRouteTlv, uint8_t aNeighborId) const;
void HandleAdvertiseTrickleTimer(void);
void HandleTimeTick(void);
void HandleRouterTableEvent(RouterTable::Events aEvents);
template <Uri kUri> void HandleTmf(Coap::Msg &aMsg);
@@ -2509,6 +2523,7 @@ private:
#if OPENTHREAD_FTD
bool mRouterEligible : 1;
bool mBlockDowngrade : 1;
bool mAddressSolicitPending : 1;
bool mAddressSolicitRejected : 1;
#if OPENTHREAD_CONFIG_REFERENCE_DEVICE_ENABLE
+23 -1
View File
@@ -478,8 +478,15 @@ uint32_t Mle::DetermineAdvertiseIntervalMax(void) const
return interval;
}
void Mle::UpdateAdvertiseInterval(void)
void Mle::HandleRouterTableEvent(RouterTable::Events aEvents)
{
// Callback from `RouterTable` when there is a change.
if (aEvents & RouterTable::kEventRouterAdded)
{
mBlockDowngrade = false;
}
if (IsRouterOrLeader() && mAdvertiseTrickleTimer.IsRunning())
{
mAdvertiseTrickleTimer.SetIntervalMax(DetermineAdvertiseIntervalMax());
@@ -3420,6 +3427,20 @@ void Mle::HandleAddressSolicitResponse(Coap::Msg *aMsg, Error aResult)
for (Child &child : Get<ChildTable>().Iterate(Child::kInStateChildIdRequest))
{
IgnoreError(SendChildIdResponse(child));
// The transition to the router role was triggered by a Child
// ID Request. This indicates that the child has no other
// parent option. We set the flags to prevent the parent
// router from downgrading back to a REED to ensure this
// child remains connected.
//
// The `mBlockDowngrade` is cleared in various situations:
// - From `SetStateDetached()` (e.g. partition change).
// - If a new router is added (new possible parent).
// - If all children blocking downgrade are disconnected.
child.SetBlockParentDowngrade(true);
mBlockDowngrade = true;
}
exit:
@@ -3724,6 +3745,7 @@ bool Mle::ShouldDowngrade(uint8_t aNeighborId, const RouteTlv &aRouteTlv) const
VerifyOrExit(IsRouter());
VerifyOrExit(mRouterTable.IsAllocated(aNeighborId));
VerifyOrExit(!mBlockDowngrade);
VerifyOrExit(!mRouterRoleTransition.IsPending());
+1 -1
View File
@@ -917,7 +917,7 @@ void RouterTable::HandleTableChanged(void)
Get<HistoryTracker::Local>().RecordRouterTableChange();
#endif
Get<Mle::Mle>().UpdateAdvertiseInterval();
Get<Mle::Mle>().HandleRouterTableEvent(mEvents);
mEvents = 0;
}
+1
View File
@@ -271,6 +271,7 @@ ot_nexus_test(border_agent_tracker "core;nexus")
ot_nexus_test(discover_scan "core;nexus")
ot_nexus_test(dtls "core;nexus")
ot_nexus_test(form_join "core;nexus")
ot_nexus_test(mle_blocking_downgrade "core;nexus")
ot_nexus_test(nat64_translator "core;nexus")
ot_nexus_test(srp_lease "core;nexus")
+226
View File
@@ -0,0 +1,226 @@
/*
* Copyright (c) 2026, 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 <stdarg.h>
#include <stdio.h>
#include <string.h>
#include "platform/nexus_core.hpp"
#include "platform/nexus_node.hpp"
namespace ot {
namespace Nexus {
void TestMleBlockingDowngrade(void)
{
// This test validates the MLE router role downgrade logic when a REED is
// promoted to a router due to a Child ID Request. It verifies that the
// newly promoted router blocks its own downgrade to ensure the child remains
// connected, and that this block is correctly lifted when the child detaches
// or when a new router is introduced to the network (providing an alternative
// parent for the child).
static constexpr uint8_t kNumRouters = 10;
Core nexus;
Node &leader = nexus.CreateNode();
Node &dut = nexus.CreateNode();
Node &child = nexus.CreateNode();
Node &newRouter = nexus.CreateNode();
Node *routers[kNumRouters];
for (Node *&router : routers)
{
router = &nexus.CreateNode();
}
nexus.AdvanceTime(0);
for (Node &node : nexus.GetNodes())
{
node.GetInstance().SetLogLevel(kLogLevelNone);
}
Log("---------------------------------------------------------------------------------------");
Log("Form initial topology");
leader.AllowList(dut);
dut.AllowList(leader);
leader.AllowList(newRouter);
newRouter.AllowList(leader);
child.AllowList(dut);
dut.AllowList(child);
child.AllowList(newRouter);
newRouter.AllowList(child);
for (Node *router : routers)
{
leader.AllowList(*router);
router->AllowList(leader);
dut.AllowList(*router);
router->AllowList(dut);
newRouter.AllowList(*router);
router->AllowList(newRouter);
}
leader.Form();
nexus.AdvanceTime(13 * Time::kOneSecondInMsec);
VerifyOrQuit(leader.Get<Mle::Mle>().IsLeader());
for (Node *router : routers)
{
router->Join(leader);
}
nexus.AdvanceTime(300 * Time::kOneSecondInMsec);
VerifyOrQuit(leader.Get<Mle::Mle>().IsLeader());
for (Node *router : routers)
{
VerifyOrQuit(router->Get<Mle::Mle>().IsRouter());
}
Log("---------------------------------------------------------------------------------------");
Log("Setup DUT - Validate it stays as a REED");
dut.Get<Mle::Mle>().SetRouterUpgradeThreshold(3);
dut.Get<Mle::Mle>().SetRouterDowngradeThreshold(4);
VerifyOrQuit(!dut.Get<Mle::Mle>().IsDowngradeBlocked());
dut.Join(leader);
nexus.AdvanceTime(300 * Time::kOneSecondInMsec);
VerifyOrQuit(dut.Get<Mle::Mle>().IsChild());
VerifyOrQuit(!dut.Get<Mle::Mle>().IsDowngradeBlocked());
Log("---------------------------------------------------------------------------------------");
Log("Attach `child` only connecting through the DUT");
child.Join(dut, Node::kAsFed);
nexus.AdvanceTime(30 * Time::kOneSecondInMsec);
VerifyOrQuit(child.Get<Mle::Mle>().IsChild());
VerifyOrQuit(child.Get<Mle::Mle>().GetParent().GetExtAddress() == dut.Get<Mac::Mac>().GetExtAddress());
Log("---------------------------------------------------------------------------------------");
Log("Validate that DUT is promoted to router and blocking downgrade");
VerifyOrQuit(dut.Get<Mle::Mle>().IsRouter());
VerifyOrQuit(dut.Get<Mle::Mle>().IsDowngradeBlocked());
nexus.AdvanceTime(10 * Time::kOneMinuteInMsec);
VerifyOrQuit(dut.Get<Mle::Mle>().IsRouter());
VerifyOrQuit(dut.Get<Mle::Mle>().IsDowngradeBlocked());
Log("---------------------------------------------------------------------------------------");
Log("Disconnect one of the routers - validate that DUT still blocks downgrade");
routers[0]->Get<Mle::Mle>().Stop();
routers[0]->Get<ThreadNetif>().Down();
nexus.AdvanceTime(10);
VerifyOrQuit(routers[0]->Get<Mle::Mle>().IsDisabled());
nexus.AdvanceTime(10 * Time::kOneMinuteInMsec);
VerifyOrQuit(dut.Get<Mle::Mle>().IsRouter());
VerifyOrQuit(dut.Get<Mle::Mle>().IsDowngradeBlocked());
VerifyOrQuit(child.Get<Mle::Mle>().IsChild());
Log("---------------------------------------------------------------------------------------");
Log("Disconnect the child - validate that DUT can now downgrade");
child.Get<Mle::Mle>().Stop();
child.Get<ThreadNetif>().Down();
nexus.AdvanceTime(10);
VerifyOrQuit(child.Get<Mle::Mle>().IsDisabled());
nexus.AdvanceTime(10 * Time::kOneMinuteInMsec);
VerifyOrQuit(!dut.Get<Mle::Mle>().IsDowngradeBlocked());
VerifyOrQuit(dut.Get<Mle::Mle>().IsChild());
Log("---------------------------------------------------------------------------------------");
Log("Reconnect the child - validate that DUT once again disallow downgrade");
child.Get<ThreadNetif>().Up();
SuccessOrQuit(child.Get<Mle::Mle>().Start());
nexus.AdvanceTime(30 * Time::kOneSecondInMsec);
VerifyOrQuit(child.Get<Mle::Mle>().IsChild());
VerifyOrQuit(dut.Get<Mle::Mle>().IsRouter());
VerifyOrQuit(dut.Get<Mle::Mle>().IsDowngradeBlocked());
nexus.AdvanceTime(10 * Time::kOneMinuteInMsec);
VerifyOrQuit(dut.Get<Mle::Mle>().IsRouter());
VerifyOrQuit(dut.Get<Mle::Mle>().IsDowngradeBlocked());
Log("---------------------------------------------------------------------------------------");
Log("Introduce `newRouter` as a new router - validate DUT now allows downgrade");
newRouter.Join(leader);
nexus.AdvanceTime(15 * Time::kOneMinuteInMsec);
VerifyOrQuit(newRouter.Get<Mle::Mle>().IsRouter());
VerifyOrQuit(!dut.Get<Mle::Mle>().IsDowngradeBlocked());
Log("---------------------------------------------------------------------------------------");
Log("Validate that DUT is downgraded to REED and child connected to the new router");
VerifyOrQuit(dut.Get<Mle::Mle>().IsChild());
VerifyOrQuit(child.Get<Mle::Mle>().IsChild());
VerifyOrQuit(child.Get<Mle::Mle>().GetParent().GetExtAddress() == newRouter.Get<Mac::Mac>().GetExtAddress());
}
} // namespace Nexus
} // namespace ot
int main(void)
{
ot::Nexus::TestMleBlockingDowngrade();
printf("All tests passed\n");
return 0;
}