[mle] allow detached MTD to attach via stale announce (#11928)

This commit updates the logic in MLE `HandleAnnounce()` for processing
a received Announce message with an older (stale) timestamp.

Previously, only an Announce with a newer timestamp would be
considered for processing and announce attach (trying to attach using
the channel and/or PANID from the received announce). Also, any
device receiving a stale Announce with an older timestamp would send
its own Announce back to inform the sender.

This change updates the behavior regarding the processing of stale
Announce messages:
- A router-eligible FTD still sends an Announce back to help inform
  the other device.
- A detached MTD will now process the Announce (wait for a short delay
  before trying to attach to the older Dataset). This is useful when
  an MTD child device has a newer Dataset but the routers it can hear
  are still on a previous, older Dataset.
- An attached MTD now ignores the stale Announce. Since an MTD cannot
  become a router to help the device with the older Dataset join the
  new Dataset, sending an Announce back from the MTD would be
  pointless.

This commit also introduces a test to verify this exact scenario.
This commit is contained in:
Abtin Keshavarzian
2025-09-15 11:34:34 -07:00
committed by GitHub
parent f60b00695e
commit 242e10fb86
3 changed files with 186 additions and 13 deletions
+59 -13
View File
@@ -5775,7 +5775,16 @@ void Mle::AnnounceHandler::Stop(void)
void Mle::AnnounceHandler::HandleAnnounce(RxInfo &aRxInfo)
{
Error error = kErrorNone;
enum Action : uint8_t
{
kIgnore,
kSendAnnouceBack,
kAnnounceAttachAfterDelay,
kSignalAnnounceSender,
};
Error error = kErrorNone;
Action action = kIgnore;
ChannelTlvValue channelTlvValue;
MeshCoP::Timestamp timestamp;
MeshCoP::Timestamp pendingActiveTimestamp;
@@ -5799,21 +5808,59 @@ void Mle::AnnounceHandler::HandleAnnounce(RxInfo &aRxInfo)
timestampCompare = MeshCoP::Timestamp::Compare(timestamp, Get<MeshCoP::ActiveDatasetManager>().GetTimestamp());
channelAndPanIdMatch = (channel == Get<Mac::Mac>().GetPanChannel()) && (panId == Get<Mac::Mac>().GetPanId());
if (isFromOrphan || (timestampCompare < 0))
// Determine the action to perform.
if (isFromOrphan)
{
if (isFromOrphan)
VerifyOrExit(!channelAndPanIdMatch);
action = kSendAnnouceBack;
}
else if (timestampCompare < 0)
{
// On an FTD which can become a router, we send an Announce
// back to help the sender learn and migrate to the newer
// Dataset. On a detached MTD, we process the Announce (wait
// for a short delay before trying to attach to the older
// Dataset). This is useful when an MTD child device has a
// newer Dataset but the routers it can hear are still on a
// previous, older Dataset. On an attached MTD, we ignore the
// stale Announce, since we cannot become a router to help the
// older device join the new Dataset, so sending an Announce
// back would be pointless.
#if OPENTHREAD_FTD
if (Get<Mle>().IsFullThreadDevice() && Get<Mle>().IsRouterEligible())
{
VerifyOrExit(!channelAndPanIdMatch);
action = kSendAnnouceBack;
}
Get<Mle>().SendAnnounce(channel);
#if OPENTHREAD_CONFIG_MLE_SEND_UNICAST_ANNOUNCE_RESPONSE
Get<Mle>().SendAnnounce(channel, aRxInfo.mMessageInfo.GetPeerAddr());
else
#endif
{
action = Get<Mle>().IsDetached() ? kAnnounceAttachAfterDelay : kIgnore;
}
}
else if (timestampCompare > 0)
{
action = kAnnounceAttachAfterDelay;
}
else // timestampCompare is zero
{
action = kSignalAnnounceSender;
}
switch (action)
{
case kIgnore:
break;
case kSendAnnouceBack:
Get<Mle>().SendAnnounce(channel);
#if OPENTHREAD_CONFIG_MLE_SEND_UNICAST_ANNOUNCE_RESPONSE
Get<Mle>().SendAnnounce(channel, aRxInfo.mMessageInfo.GetPeerAddr());
#endif
break;
case kAnnounceAttachAfterDelay:
// No action is required if device is detached, and current
// channel and pan-id match the values from the received MLE
// Announce message.
@@ -5853,17 +5900,16 @@ void Mle::AnnounceHandler::HandleAnnounce(RxInfo &aRxInfo)
mTimer.Start(kAnnounceProcessTimeout);
LogNote("Delay processing Announce - channel %d, panid 0x%02x", channel, panId);
}
else
{
// Timestamps are equal.
break;
case kSignalAnnounceSender:
#if OPENTHREAD_CONFIG_ANNOUNCE_SENDER_ENABLE
// Notify `AnnounceSender` of the received Announce
// message so it can update its state to determine
// whether to send Announce or not.
Get<AnnounceSender>().UpdateOnReceivedAnnounce();
#endif
break;
}
exit:
+126
View File
@@ -0,0 +1,126 @@
#!/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 validates that a MTD can attach to a router/parent that
# has a previous Dataset with an older active timestamp (when it
# received Announce message from the parent/router).
#
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)
parent = cli.Node()
child = cli.Node()
# -----------------------------------------------------------------------------------------------------------------------
# Form topology
parent.form('mtd-old-tmstmp')
child.join(parent, cli.JOIN_TYPE_END_DEVICE)
verify(parent.get_state() == 'leader')
verify(child.get_state() == 'child')
# -----------------------------------------------------------------------------------------------------------------------
# Test Implementation
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
# Disable MLE on the child.
child.thread_stop()
verify(child.get_state() == 'disabled')
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
# Update the Active Dataset on the child with a new channel and a
# higher active timestamp.
child.cli('dataset init active')
channel = int(child.get_channel())
channel = 11 if channel != 11 else 12
child.cli('dataset channel', channel)
timestamp = int(child.cli('dataset activetimestamp')[0])
timestamp = timestamp + 100
child.cli('dataset activetimestamp', timestamp)
child.cli('dataset commit active')
time.sleep(0.1)
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
# Restart MLE on the child again now with the new Dataset.
child.thread_start()
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
# Verify that the child can successfully attach to the parent, despite
# the parent being on an older Dataset.
def check_child_state():
verify(child.get_state() == 'child')
verify_within(check_child_state, 10)
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
# Verify that the parent also adopts the new Dataset from the child.
def check_parent_adopts_the_new_dataset():
verify(child.get_state() == 'child')
verify(parent.get_state() == 'leader')
parent.cli('dataset init active')
verify(int(parent.cli('dataset activetimestamp')[0]) == timestamp)
verify(int(parent.get_channel()) == channel)
verify_within(check_parent_adopts_the_new_dataset, 10)
# -----------------------------------------------------------------------------------------------------------------------
# Test finished
cli.Node.finalize_all_nodes()
print('\'{}\' passed.'.format(test_name))
+1
View File
@@ -202,6 +202,7 @@ if [ "$TORANJ_CLI" = 1 ]; then
run cli/test-034-fed-parent-search.py
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-400-srp-client-server.py
run cli/test-401-srp-server-address-cache-snoop.py
run cli/test-500-two-brs-two-networks.py