diff --git a/tests/nexus/CMakeLists.txt b/tests/nexus/CMakeLists.txt index c22d91c31..a86c837f3 100644 --- a/tests/nexus/CMakeLists.txt +++ b/tests/nexus/CMakeLists.txt @@ -400,6 +400,7 @@ ot_nexus_test(key_rotation_guard_time "core;nexus") ot_nexus_test(log_override "core;nexus") ot_nexus_test(mac_scan "core;nexus") ot_nexus_test(mle_blocking_downgrade "core;nexus") +ot_nexus_test(mle_msg_key_seq_jump "core;nexus") ot_nexus_test(nat64_translator "core;nexus") ot_nexus_test(netdata_publisher "core;nexus") ot_nexus_test(reed_address_solicit_rejected "core;nexus") diff --git a/tests/nexus/test_mle_msg_key_seq_jump.cpp b/tests/nexus/test_mle_msg_key_seq_jump.cpp new file mode 100644 index 000000000..aaa12f3e5 --- /dev/null +++ b/tests/nexus/test_mle_msg_key_seq_jump.cpp @@ -0,0 +1,240 @@ +/* + * 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 "platform/nexus_core.hpp" +#include "platform/nexus_node.hpp" + +namespace ot { +namespace Nexus { + +static constexpr uint32_t kMaxAdvertisementInterval = 32 * 1000; + +void TestMleMsgKeySeqJump(void) +{ + Core nexus; + + Node &leader = nexus.CreateNode(); + Node &child = nexus.CreateNode(); + Node &reed = nexus.CreateNode(); + Node &router = nexus.CreateNode(); + + leader.SetName("LEADER"); + child.SetName("CHILD"); + reed.SetName("REED"); + router.SetName("ROUTER"); + + Node *nodes[] = {&leader, &child, &reed, &router}; + + nexus.AdvanceTime(0); + + SuccessOrQuit(Instance::SetGlobalLogLevel(kLogLevelNote)); + + for (Node *node : nodes) + { + node->Get().SetCurrentKeySequence(0, KeyManager::kForceUpdate); + } + + Log("---------------------------------------------------------------------------------------"); + Log("Form the network"); + + leader.Form(); + nexus.AdvanceTime(15000); + VerifyOrQuit(leader.Get().IsLeader()); + + Log("---------------------------------------------------------------------------------------"); + Log("Nodes join"); + + child.Join(leader, Node::kAsMed); + reed.Join(leader, Node::kAsFed); + router.Join(leader, Node::kAsFtd); + + nexus.AdvanceTime(200000); + + VerifyOrQuit(leader.Get().IsLeader()); + VerifyOrQuit(child.Get().IsChild()); + VerifyOrQuit(reed.Get().IsChild()); + VerifyOrQuit(router.Get().IsRouter()); + + for (Node *node : nodes) + { + VerifyOrQuit(node->Get().GetCurrentKeySequence() == 0); + } + + Log("---------------------------------------------------------------------------------------"); + Log("Manually increase the key seq on child and trigger Child Update Request"); + + child.Get().SetCurrentKeySequence(5, KeyManager::kForceUpdate); + VerifyOrQuit(child.Get().GetCurrentKeySequence() == 5); + + // Trigger Child Update Request by changing mode + { + Mle::DeviceMode mode = child.Get().GetDeviceMode(); + mode.Set(mode.Get() ^ Mle::DeviceMode::kModeFullNetworkData); + SuccessOrQuit(child.Get().SetDeviceMode(mode)); + } + nexus.AdvanceTime(1000); + + VerifyOrQuit(child.Get().GetCurrentKeySequence() == 5); + VerifyOrQuit(leader.Get().GetCurrentKeySequence() == 5); + + Log("---------------------------------------------------------------------------------------"); + Log("Wait for MLE Advertisements for other nodes to adopt the new key seq"); + + nexus.AdvanceTime(2 * kMaxAdvertisementInterval); + for (Node *node : nodes) + { + VerifyOrQuit(node->Get().GetCurrentKeySequence() == 5); + } + + Log("---------------------------------------------------------------------------------------"); + Log("Manually increase the key seq on leader"); + + leader.Get().SetCurrentKeySequence(10, KeyManager::kForceUpdate); + VerifyOrQuit(leader.Get().GetCurrentKeySequence() == 10); + + // Trigger Child Update Request for child (MED) to adopt the new key sequence. + { + Mle::DeviceMode mode = child.Get().GetDeviceMode(); + mode.Set(mode.Get() ^ Mle::DeviceMode::kModeFullNetworkData); + SuccessOrQuit(child.Get().SetDeviceMode(mode)); + } + + // All nodes should adopt the new key sequence from authoritative advertisements or Child Update. + // Wait long enough for them to do so. + nexus.AdvanceTime(3 * kMaxAdvertisementInterval); + + for (Node *node : nodes) + { + VerifyOrQuit(node->Get().GetCurrentKeySequence() == 10); + } + + Log("---------------------------------------------------------------------------------------"); + Log("Stop other nodes, jump leader key seq, and restart them"); + + router.Reset(); + reed.Reset(); + child.Reset(); + + leader.Get().SetCurrentKeySequence(15, KeyManager::kForceUpdate); + VerifyOrQuit(leader.Get().GetCurrentKeySequence() == 15); + + router.Get().Up(); + SuccessOrQuit(router.Get().Start()); + child.Get().Up(); + SuccessOrQuit(child.Get().Start()); + reed.Get().Up(); + SuccessOrQuit(reed.Get().Start()); + nexus.AdvanceTime(10000); + + for (Node *node : nodes) + { + VerifyOrQuit(node->Get().IsAttached()); + VerifyOrQuit(node->Get().GetCurrentKeySequence() == 15); + } + + Log("---------------------------------------------------------------------------------------"); + Log("Stop nodes other than leader, jump child key seq, and restart child"); + + router.Reset(); + reed.Reset(); + child.Reset(); + + child.Get().SetCurrentKeySequence(20, KeyManager::kForceUpdate); + child.Get().Up(); + SuccessOrQuit(child.Get().Start()); + VerifyOrQuit(child.Get().GetCurrentKeySequence() == 20); + nexus.AdvanceTime(5000); + + VerifyOrQuit(child.Get().IsChild()); + VerifyOrQuit(leader.Get().GetCurrentKeySequence() == 20); + + Log("---------------------------------------------------------------------------------------"); + Log("Restart router and reed"); + + router.Get().Up(); + SuccessOrQuit(router.Get().Start()); + reed.Get().Up(); + SuccessOrQuit(reed.Get().Start()); + nexus.AdvanceTime(10000); + + VerifyOrQuit(router.Get().IsAttached()); + VerifyOrQuit(reed.Get().IsAttached()); + + VerifyOrQuit(router.Get().GetCurrentKeySequence() == 20); + VerifyOrQuit(reed.Get().GetCurrentKeySequence() == 20); + + Log("---------------------------------------------------------------------------------------"); + Log("Jump key seq on router and wait for advertisement"); + + router.Get().SetCurrentKeySequence(22, KeyManager::kForceUpdate); + VerifyOrQuit(router.Get().GetCurrentKeySequence() == 22); + + nexus.AdvanceTime(2 * kMaxAdvertisementInterval); + VerifyOrQuit(leader.Get().GetCurrentKeySequence() == 22); + VerifyOrQuit(reed.Get().GetCurrentKeySequence() == 22); + + { + Mle::DeviceMode mode = child.Get().GetDeviceMode(); + mode.Set(mode.Get() ^ Mle::DeviceMode::kModeFullNetworkData); + SuccessOrQuit(child.Get().SetDeviceMode(mode)); + } + nexus.AdvanceTime(10000); + VerifyOrQuit(child.Get().GetCurrentKeySequence() == 22); + + Log("---------------------------------------------------------------------------------------"); + Log("Child factory reset and join with higher key seq"); + + router.Reset(); + reed.Reset(); + + child.Reset(); + child.mSettings.Wipe(); + VerifyOrQuit(child.Get().GetRole() == Mle::kRoleDisabled); + + child.Get().SetCurrentKeySequence(25, KeyManager::kForceUpdate); + child.Join(leader, Node::kAsMed); + VerifyOrQuit(child.Get().GetCurrentKeySequence() == 25); + nexus.AdvanceTime(5000); + + VerifyOrQuit(child.Get().IsChild()); + VerifyOrQuit(leader.Get().GetCurrentKeySequence() == 25); + + nexus.SaveTestInfo("test_mle_msg_key_seq_jump.json"); +} + +} // namespace Nexus +} // namespace ot + +int main(void) +{ + ot::Nexus::TestMleMsgKeySeqJump(); + printf("All tests passed\n"); + return 0; +} diff --git a/tests/scripts/thread-cert/test_mle_msg_key_seq_jump.py b/tests/scripts/thread-cert/test_mle_msg_key_seq_jump.py deleted file mode 100755 index 4113c4285..000000000 --- a/tests/scripts/thread-cert/test_mle_msg_key_seq_jump.py +++ /dev/null @@ -1,266 +0,0 @@ -#!/usr/bin/env python3 -# -# Copyright (c) 2022, 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. -# - -import ipaddress -import unittest - -import command -import config -import thread_cert - -# Test description: -# -# This test verifies behavior of MLE related to handling of received -# larger key sequence based on the MLE message class (authoritative, -# or peer). -# -# -# Topology: -# -# leader --- router -# | \ -# | \ -# child reed -# - -LEADER = 1 -CHILD = 2 -REED = 3 -ROUTER = 4 - - -class MleMsgKeySeqJump(thread_cert.TestCase): - USE_MESSAGE_FACTORY = False - SUPPORT_NCP = False - - TOPOLOGY = { - LEADER: { - 'name': 'LEADER', - 'mode': 'rdn', - }, - CHILD: { - 'name': 'CHILD', - 'is_mtd': True, - 'mode': 'rn', - }, - REED: { - 'name': 'REED', - 'mode': 'rn' - }, - ROUTER: { - 'name': 'ROUTER', - 'mode': 'rdn', - }, - } - - def test(self): - leader = self.nodes[LEADER] - child = self.nodes[CHILD] - reed = self.nodes[REED] - router = self.nodes[ROUTER] - - nodes = [leader, child, reed, router] - - #------------------------------------------------------------------- - # Form the network. - - for node in nodes: - node.set_key_sequence_counter(0) - - leader.start() - self.simulator.go(config.LEADER_STARTUP_DELAY) - self.assertEqual(leader.get_state(), 'leader') - - child.start() - reed.start() - self.simulator.go(5) - self.assertEqual(child.get_state(), 'child') - self.assertEqual(reed.get_state(), 'child') - - router.start() - self.simulator.go(config.ROUTER_STARTUP_DELAY) - self.assertEqual(router.get_state(), 'router') - - #------------------------------------------------------------------- - # Validate the initial key seq counter on all nodes - - for node in nodes: - self.assertEqual(node.get_key_sequence_counter(), 0) - - #------------------------------------------------------------------- - # Manually increase the key seq on child. Then change MLE mode on - # child which triggers a "Child Update Request" to its parent - # (leader). The key jump noticed on parent side would trigger an - # authoritative MLE Child Update exchange (including challenge and - # response TLVs) and causes the parent (leader) to also adopt the - # larger key seq. - - child.set_key_sequence_counter(5) - self.assertEqual(child.get_key_sequence_counter(), 5) - - child.set_mode('r') - self.simulator.go(1) - - self.assertEqual(child.get_key_sequence_counter(), 5) - self.assertEqual(leader.get_key_sequence_counter(), 5) - - #------------------------------------------------------------------- - # Wait long enough for MLE Advertisement to be sent. This would - # trigger reed and router to also notice key seq jump and try to - # re-establish link again (using authoritative exchanges). Validate - # that all nodes are using the new key seq. - - self.simulator.go(52) - for node in nodes: - self.assertEqual(node.get_key_sequence_counter(), 5) - - #------------------------------------------------------------------- - # Manually increase the key seq on leader. Wait for max time between - # advertisements. This would trigger both reed and router - # to notice key seq jump and try to re-establish link (link - # request/accept exchange). Validate that they all adopt the new - # key seq. - - leader.set_key_sequence_counter(10) - self.assertEqual(leader.get_key_sequence_counter(), 10) - - self.simulator.go(52) - - self.assertEqual(router.get_key_sequence_counter(), 10) - self.assertEqual(reed.get_key_sequence_counter(), 10) - - #------------------------------------------------------------------- - # Change MLE mode on child to trigger a "Child Update Request" exchange - # which should then update the key seq on child as well. - - child.set_mode('rn') - self.simulator.go(5) - self.assertEqual(child.get_key_sequence_counter(), 10) - - #------------------------------------------------------------------- - # Stop all other nodes except for leader. Move the leader key seq - # forward and then restart all other node. Validate that router, - # reed and child all re-attach successfully and adopt the higher key - # sequence. - - router.stop() - reed.stop() - child.stop() - - leader.set_key_sequence_counter(15) - self.assertEqual(leader.get_key_sequence_counter(), 15) - - child.start() - reed.start() - router.start() - self.simulator.go(5) - - self.assertEqual(child.get_state(), 'child') - self.assertEqual(reed.get_state(), 'child') - self.assertEqual(router.get_state(), 'router') - - for node in nodes: - self.assertEqual(node.get_key_sequence_counter(), 15) - - #------------------------------------------------------------------- - # Stop all other nodes except for leader. Move the child key seq - # forward and then restart child. Ensure it re-attached successfully - # to leader and that leader adopts the higher key seq counter. - - router.stop() - reed.stop() - child.stop() - - child.set_key_sequence_counter(20) - self.assertEqual(child.get_key_sequence_counter(), 20) - - child.start() - self.simulator.go(5) - - self.assertEqual(child.get_state(), 'child') - self.assertEqual(leader.get_key_sequence_counter(), 20) - - #------------------------------------------------------------------- - # Restart router and reed and ensure they are re-attached and get the - # higher key seq counter. - - router.start() - reed.start() - - self.simulator.go(5) - self.assertEqual(router.get_state(), 'router') - self.assertEqual(reed.get_state(), 'child') - - self.assertEqual(router.get_key_sequence_counter(), 20) - self.assertEqual(reed.get_key_sequence_counter(), 20) - - #------------------------------------------------------------------- - # Move forward the key seq counter by two on router. Wait for max - # time between advertisements. Validate that leader adopts the higher - # counter value. - - router.set_key_sequence_counter(22) - self.assertEqual(router.get_key_sequence_counter(), 22) - - self.simulator.go(52) - self.assertEqual(leader.get_key_sequence_counter(), 22) - self.assertEqual(reed.get_key_sequence_counter(), 22) - - child.set_mode('r') - self.simulator.go(2) - self.assertEqual(child.get_key_sequence_counter(), 22) - - #------------------------------------------------------------------- - # Force a reattachment from the child with a higher key seq counter, - # so that the leader generated a fragmented Child Id Response. Ensure - # the child becomes attached on first attempt while the leader adopts - # the higher counter value. - - router.stop() - reed.stop() - - child.factory_reset() - self.assertEqual(child.get_state(), 'disabled') - child.set_mode('r') - - child.set_active_dataset(channel=leader.get_channel(), - network_key=leader.get_networkkey(), - panid=leader.get_panid()) - child.set_key_sequence_counter(25) - self.assertEqual(child.get_key_sequence_counter(), 25) - - child.start() - self.simulator.go(5) - - self.assertEqual(child.get_state(), 'child') - self.assertEqual(leader.get_key_sequence_counter(), 25) - - -if __name__ == '__main__': - unittest.main()