[mle] allow processing of "Child Update Request" while restoring child role (#12007)

This change modifies `HandleChildUpdateRequest()` to allow a detached
child that is restoring its previous role to process a "Child Update
Request" from its former parent.

When in this state, the device will respond to the request but will
not save any of the content (TLVs) from the message, as the child has
not yet established trust with any device (including its former
parent) and therefore cannot authenticate the freshness of the
received request.

This change handles the scenario where a child and its parent may be
reset simultaneously. It allows the parent to first restore its link
with the child through a "Child Update" exchange, which can then be
followed by the child sending its own "Child Update Request" to
re-establish the link. Without this change, a communication impasse
could occur where the parent rejects the child's request (as the
child is not yet valid), and the child ignores the parent's request
(as it is not yet attached). This change prevents devices from
resorting to a full re-attachment, thereby improving network
resilience and recovery time.

A new test is added to emulate this scenario and verify that the child
restores its role correctly without performing a full attach.
This commit is contained in:
Abtin Keshavarzian
2025-10-14 16:31:53 +02:00
committed by GitHub
parent 1f20dbc0af
commit ef058d5bbb
3 changed files with 155 additions and 19 deletions
+31 -19
View File
@@ -2181,31 +2181,40 @@ exit:
void Mle::HandleChildUpdateRequest(RxInfo &aRxInfo)
{
VerifyOrExit(IsAttached());
#if OPENTHREAD_FTD
if (IsRouterOrLeader())
{
HandleChildUpdateRequestOnParent(aRxInfo);
ExitNow();
}
else
#endif
HandleChildUpdateRequestOnChild(aRxInfo);
exit:
return;
{
HandleChildUpdateRequestOnChild(aRxInfo);
}
}
void Mle::HandleChildUpdateRequestOnChild(RxInfo &aRxInfo)
{
Error error = kErrorNone;
Error error = kErrorNone;
bool canTrustMessage = aRxInfo.IsNeighborStateValid();
uint16_t sourceAddress;
RxChallenge challenge;
TlvList requestedTlvList;
TlvList tlvList;
uint8_t linkMarginOut;
if (!IsAttached())
{
// If detached and trying to restore our role as a child, we
// allow processing of a received "Child Update Request"
// and send a response. But since we have not yet established
// trust with any device (including our former parent),
// we will not save any of the content (TLVs) from the
// message (`canTrustMessage` will be `false`).
VerifyOrExit(mPrevRoleRestorer.IsRestoringChildRole());
}
SuccessOrExit(error = Tlv::Find<SourceAddressTlv>(aRxInfo.mMessage, sourceAddress));
Log(kMessageReceive, kTypeChildUpdateRequestAsChild, aRxInfo.mMessageInfo.GetPeerAddr(), sourceAddress);
@@ -2245,17 +2254,20 @@ void Mle::HandleChildUpdateRequestOnChild(RxInfo &aRxInfo)
ExitNow();
}
SuccessOrExit(error = HandleLeaderData(aRxInfo));
switch (Tlv::Find<LinkMarginTlv>(aRxInfo.mMessage, linkMarginOut))
if (canTrustMessage)
{
case kErrorNone:
mParent.SetLinkQualityOut(LinkQualityForLinkMargin(linkMarginOut));
break;
case kErrorNotFound:
break;
default:
ExitNow(error = kErrorParse);
SuccessOrExit(error = HandleLeaderData(aRxInfo));
switch (Tlv::Find<LinkMarginTlv>(aRxInfo.mMessage, linkMarginOut))
{
case kErrorNone:
mParent.SetLinkQualityOut(LinkQualityForLinkMargin(linkMarginOut));
break;
case kErrorNotFound:
break;
default:
ExitNow(error = kErrorParse);
}
}
#if OPENTHREAD_CONFIG_MAC_CSL_RECEIVER_ENABLE
@@ -0,0 +1,123 @@
#!/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
import time
# -----------------------------------------------------------------------------------------------------------------------
# Test description:
# This test covers reset of parent, router, leader and restoring children after reset
#
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()
router = cli.Node()
child = cli.Node()
# -----------------------------------------------------------------------------------------------------------------------
# Form topology
#
# leader --- router
# |
# child
#
leader.allowlist_node(router)
router.allowlist_node(leader)
child.allowlist_node(router)
router.allowlist_node(child)
leader.form('reset')
router.join(leader)
child.join(leader, cli.JOIN_TYPE_END_DEVICE)
verify(leader.get_state() == 'leader')
verify(router.get_state() == 'router')
verify(child.get_state() == 'child')
# Make sure that `child` is attached to `router` as its parent.
router_rloc = int(router.get_rloc16(), 16)
verify(int(child.get_parent_info()['Rloc'], 16) == router_rloc)
# -----------------------------------------------------------------------------------------------------------------------
# Test Implementation
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
del router
router = cli.Node(index=2)
router.interface_up()
router.thread_start()
del child
child = cli.Node(index=3)
child.set_router_eligible('disable')
child.interface_up()
child.thread_start()
# Ensure both child and router restore their previous role.
def check_router_and_child_states():
verify(router.get_state() == 'router')
verify(child.get_state() == 'child')
verify_within(check_router_and_child_states, 10)
verify(int(router.get_rloc16(), 16) == router_rloc)
verify(int(child.get_parent_info()['Rloc'], 16) == router_rloc)
# Validate that the child was restored using a "Child Update" exchange and not
# through a "Parent Request/Child ID Request" sequence. We validate this by
# checking that the "Attach Attempts" in the MLE counters remains zero.
mle_counter = child.get_mle_counter()
verify(int(cli.Node.parse_list(mle_counter)['Role Detached']) == 1)
verify(int(cli.Node.parse_list(mle_counter)['Attach Attempts']) == 0)
# -----------------------------------------------------------------------------------------------------------------------
# Test finished
cli.Node.finalize_all_nodes()
print('\'{}\' passed.'.format(test_name))
+1
View File
@@ -203,6 +203,7 @@ if [ "$TORANJ_CLI" = 1 ]; then
run cli/test-035-context-id-change-addr-reg.py
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-400-srp-client-server.py
run cli/test-401-srp-server-address-cache-snoop.py
run cli/test-500-two-brs-two-networks.py