diff --git a/src/core/thread/mle.cpp b/src/core/thread/mle.cpp index 5b8c5660b..71ee8d0e2 100644 --- a/src/core/thread/mle.cpp +++ b/src/core/thread/mle.cpp @@ -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().GetTimestamp()); channelAndPanIdMatch = (channel == Get().GetPanChannel()) && (panId == Get().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().IsFullThreadDevice() && Get().IsRouterEligible()) { - VerifyOrExit(!channelAndPanIdMatch); + action = kSendAnnouceBack; } - - Get().SendAnnounce(channel); - -#if OPENTHREAD_CONFIG_MLE_SEND_UNICAST_ANNOUNCE_RESPONSE - Get().SendAnnounce(channel, aRxInfo.mMessageInfo.GetPeerAddr()); + else #endif + { + action = Get().IsDetached() ? kAnnounceAttachAfterDelay : kIgnore; + } } else if (timestampCompare > 0) { + action = kAnnounceAttachAfterDelay; + } + else // timestampCompare is zero + { + action = kSignalAnnounceSender; + } + + switch (action) + { + case kIgnore: + break; + + case kSendAnnouceBack: + Get().SendAnnounce(channel); +#if OPENTHREAD_CONFIG_MLE_SEND_UNICAST_ANNOUNCE_RESPONSE + Get().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().UpdateOnReceivedAnnounce(); #endif + break; } exit: diff --git a/tests/toranj/cli/test-037-mtd-annc-join-older-timestamp.py b/tests/toranj/cli/test-037-mtd-annc-join-older-timestamp.py new file mode 100755 index 000000000..504ad6b8a --- /dev/null +++ b/tests/toranj/cli/test-037-mtd-annc-join-older-timestamp.py @@ -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)) diff --git a/tests/toranj/start.sh b/tests/toranj/start.sh index 792bba4f9..f7aee1b36 100755 --- a/tests/toranj/start.sh +++ b/tests/toranj/start.sh @@ -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