diff --git a/src/core/thread/child.hpp b/src/core/thread/child.hpp index abcd27d0d..07eeaca90 100644 --- a/src/core/thread/child.hpp +++ b/src/core/thread/child.hpp @@ -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 diff --git a/src/core/thread/mle.cpp b/src/core/thread/mle.cpp index 91098052a..fdf1947c6 100644 --- a/src/core/thread/mle.cpp +++ b/src/core/thread/mle.cpp @@ -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().SetRxOnWhenIdle(true); Get().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().Iterate(Child::kInStateValid)) + { + if (child.IsBlockingParentDowngrade()) + { + mBlockDowngrade = true; + break; + } + } + } #endif if (aEvents.Contains(kEventSupportedChannelMaskChanged)) diff --git a/src/core/thread/mle.hpp b/src/core/thread/mle.hpp index 7eccb6fea..d46372156 100644 --- a/src/core/thread/mle.hpp +++ b/src/core/thread/mle.hpp @@ -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 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 diff --git a/src/core/thread/mle_ftd.cpp b/src/core/thread/mle_ftd.cpp index a64dbe04b..7a48bfc6e 100644 --- a/src/core/thread/mle_ftd.cpp +++ b/src/core/thread/mle_ftd.cpp @@ -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().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()); diff --git a/src/core/thread/router_table.cpp b/src/core/thread/router_table.cpp index d7085255e..b8a0bf92e 100644 --- a/src/core/thread/router_table.cpp +++ b/src/core/thread/router_table.cpp @@ -917,7 +917,7 @@ void RouterTable::HandleTableChanged(void) Get().RecordRouterTableChange(); #endif - Get().UpdateAdvertiseInterval(); + Get().HandleRouterTableEvent(mEvents); mEvents = 0; } diff --git a/tests/nexus/CMakeLists.txt b/tests/nexus/CMakeLists.txt index 8b2b273d1..5f8fbdfe9 100644 --- a/tests/nexus/CMakeLists.txt +++ b/tests/nexus/CMakeLists.txt @@ -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") diff --git a/tests/nexus/test_mle_blocking_downgrade.cpp b/tests/nexus/test_mle_blocking_downgrade.cpp new file mode 100644 index 000000000..ac314f238 --- /dev/null +++ b/tests/nexus/test_mle_blocking_downgrade.cpp @@ -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 +#include +#include + +#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().IsLeader()); + + for (Node *router : routers) + { + router->Join(leader); + } + + nexus.AdvanceTime(300 * Time::kOneSecondInMsec); + + VerifyOrQuit(leader.Get().IsLeader()); + + for (Node *router : routers) + { + VerifyOrQuit(router->Get().IsRouter()); + } + + Log("---------------------------------------------------------------------------------------"); + Log("Setup DUT - Validate it stays as a REED"); + + dut.Get().SetRouterUpgradeThreshold(3); + dut.Get().SetRouterDowngradeThreshold(4); + + VerifyOrQuit(!dut.Get().IsDowngradeBlocked()); + + dut.Join(leader); + + nexus.AdvanceTime(300 * Time::kOneSecondInMsec); + + VerifyOrQuit(dut.Get().IsChild()); + VerifyOrQuit(!dut.Get().IsDowngradeBlocked()); + + Log("---------------------------------------------------------------------------------------"); + Log("Attach `child` only connecting through the DUT"); + + child.Join(dut, Node::kAsFed); + + nexus.AdvanceTime(30 * Time::kOneSecondInMsec); + + VerifyOrQuit(child.Get().IsChild()); + VerifyOrQuit(child.Get().GetParent().GetExtAddress() == dut.Get().GetExtAddress()); + + Log("---------------------------------------------------------------------------------------"); + Log("Validate that DUT is promoted to router and blocking downgrade"); + + VerifyOrQuit(dut.Get().IsRouter()); + VerifyOrQuit(dut.Get().IsDowngradeBlocked()); + + nexus.AdvanceTime(10 * Time::kOneMinuteInMsec); + + VerifyOrQuit(dut.Get().IsRouter()); + VerifyOrQuit(dut.Get().IsDowngradeBlocked()); + + Log("---------------------------------------------------------------------------------------"); + Log("Disconnect one of the routers - validate that DUT still blocks downgrade"); + + routers[0]->Get().Stop(); + routers[0]->Get().Down(); + + nexus.AdvanceTime(10); + + VerifyOrQuit(routers[0]->Get().IsDisabled()); + + nexus.AdvanceTime(10 * Time::kOneMinuteInMsec); + + VerifyOrQuit(dut.Get().IsRouter()); + VerifyOrQuit(dut.Get().IsDowngradeBlocked()); + VerifyOrQuit(child.Get().IsChild()); + + Log("---------------------------------------------------------------------------------------"); + Log("Disconnect the child - validate that DUT can now downgrade"); + + child.Get().Stop(); + child.Get().Down(); + + nexus.AdvanceTime(10); + + VerifyOrQuit(child.Get().IsDisabled()); + + nexus.AdvanceTime(10 * Time::kOneMinuteInMsec); + + VerifyOrQuit(!dut.Get().IsDowngradeBlocked()); + VerifyOrQuit(dut.Get().IsChild()); + + Log("---------------------------------------------------------------------------------------"); + Log("Reconnect the child - validate that DUT once again disallow downgrade"); + + child.Get().Up(); + SuccessOrQuit(child.Get().Start()); + + nexus.AdvanceTime(30 * Time::kOneSecondInMsec); + + VerifyOrQuit(child.Get().IsChild()); + + VerifyOrQuit(dut.Get().IsRouter()); + VerifyOrQuit(dut.Get().IsDowngradeBlocked()); + + nexus.AdvanceTime(10 * Time::kOneMinuteInMsec); + + VerifyOrQuit(dut.Get().IsRouter()); + VerifyOrQuit(dut.Get().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().IsRouter()); + VerifyOrQuit(!dut.Get().IsDowngradeBlocked()); + + Log("---------------------------------------------------------------------------------------"); + Log("Validate that DUT is downgraded to REED and child connected to the new router"); + + VerifyOrQuit(dut.Get().IsChild()); + + VerifyOrQuit(child.Get().IsChild()); + VerifyOrQuit(child.Get().GetParent().GetExtAddress() == newRouter.Get().GetExtAddress()); +} + +} // namespace Nexus +} // namespace ot + +int main(void) +{ + ot::Nexus::TestMleBlockingDowngrade(); + printf("All tests passed\n"); + return 0; +}