From 932e00e12d4abc92428a7cc44c497c7b6a2594d0 Mon Sep 17 00:00:00 2001 From: Jonathan Hui Date: Sat, 18 Apr 2026 19:55:13 -0700 Subject: [PATCH] [nexus] migrate router_downgrade_on_sec_policy_change to nexus (#12926) This commit migrates the certification test for router downgrade on security policy change from a Python script to the Nexus test framework. The original test was test_router_downgrade_on_sec_policy_change.py in tests/scripts/thread-cert. The new Nexus test test_router_downgrade_on_sec_policy_change.cpp replicates the original test scenario: - Forms a network with a Leader and a Router. - Verifies that both nodes are in the expected router/leader roles. - Changes the security policy to disable 'R' bit (routers) and sets the version threshold to 7. - Verifies that the Leader and Router do not immediately downgrade, respecting the mandatory 10-second delay. - Verifies that restoring the original security policy before the timeout cancels the pending downgrade. - Verifies that re-applying the security policy change leads to both nodes eventually downgrading to the detached state once the version threshold and router disable flags are propagated and the timer expires. Migrating to Nexus provides faster execution using virtual time and a more integrated environment for debugging core Thread logic. --- tests/nexus/CMakeLists.txt | 1 + ..._router_downgrade_on_sec_policy_change.cpp | 200 ++++++++++++++++++ ...t_router_downgrade_on_sec_policy_change.py | 149 ------------- 3 files changed, 201 insertions(+), 149 deletions(-) create mode 100644 tests/nexus/test_router_downgrade_on_sec_policy_change.cpp delete mode 100755 tests/scripts/thread-cert/test_router_downgrade_on_sec_policy_change.py diff --git a/tests/nexus/CMakeLists.txt b/tests/nexus/CMakeLists.txt index 737a7d1ad..990e2b37c 100644 --- a/tests/nexus/CMakeLists.txt +++ b/tests/nexus/CMakeLists.txt @@ -393,6 +393,7 @@ ot_nexus_test(dtls "core;nexus") ot_nexus_test(form_join "core;nexus") ot_nexus_test(dataset_updater "core;nexus") ot_nexus_test(mle_blocking_downgrade "core;nexus") +ot_nexus_test(router_downgrade_on_sec_policy_change "core;nexus") ot_nexus_test(nat64_translator "core;nexus") ot_nexus_test(srp_lease "core;nexus") diff --git a/tests/nexus/test_router_downgrade_on_sec_policy_change.cpp b/tests/nexus/test_router_downgrade_on_sec_policy_change.cpp new file mode 100644 index 000000000..b28a6214e --- /dev/null +++ b/tests/nexus/test_router_downgrade_on_sec_policy_change.cpp @@ -0,0 +1,200 @@ +/* + * 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" +#include "thread/key_manager.hpp" + +namespace ot { +namespace Nexus { + +void TestRouterDowngradeOnSecPolicyChange(void) +{ + // This test verifies leader and router downgrade delay on security policy version threshold change. + // + // Topology: + // + // LEADER + // | + // | + // ROUTER + // + + Core nexus; + Node &leader = nexus.CreateNode(); + Node &router = nexus.CreateNode(); + + leader.Get().SetRouterSelectionJitter(10); + router.Get().SetRouterSelectionJitter(10); + + nexus.AdvanceTime(0); + + SuccessOrQuit(Instance::SetGlobalLogLevel(kLogLevelNone)); + + Log("---------------------------------------------------------------------------------------"); + Log("Form initial topology"); + + AllowLinkBetween(leader, router); + + leader.SetName("LEADER"); + router.SetName("ROUTER"); + + leader.Form(); + nexus.AdvanceTime(13 * Time::kOneSecondInMsec); + VerifyOrQuit(leader.Get().IsLeader()); + + router.Join(leader); + nexus.AdvanceTime(300 * Time::kOneSecondInMsec); + VerifyOrQuit(router.Get().IsRouter()); + + VerifyOrQuit(leader.Get().IsRouterRoleAllowed()); + VerifyOrQuit(router.Get().IsRouterRoleAllowed()); + + Log("---------------------------------------------------------------------------------------"); + Log("Change security policy, disable `R` bit and set version threshold to max value (7)"); + + { + MeshCoP::Dataset::Info datasetInfo; + SecurityPolicy policy; + MeshCoP::Timestamp timestamp; + + SuccessOrQuit(leader.Get().Read(datasetInfo)); + + policy.SetToDefault(); + policy.mRoutersEnabled = false; + policy.mVersionThresholdForRouting = 7; + datasetInfo.Set(policy); + + timestamp.Clear(); + timestamp.SetSeconds(30); + datasetInfo.Set(timestamp); + + leader.Get().SaveLocal(datasetInfo); + } + + // Wait for the dataset to propagate to the router. + nexus.AdvanceTime(500); + + VerifyOrQuit(leader.Get().IsLeader()); + VerifyOrQuit(router.Get().IsRouter()); + + Log("Leader should take at least 10 seconds before downgrading"); + + nexus.AdvanceTime(5 * Time::kOneSecondInMsec); + VerifyOrQuit(leader.Get().IsLeader()); + VerifyOrQuit(!leader.Get().IsRouterRoleAllowed()); + VerifyOrQuit(!router.Get().IsRouterRoleAllowed()); + + Log("---------------------------------------------------------------------------------------"); + Log("Change back security policy. This should cancel the ongoing downgrade delay"); + + { + MeshCoP::Dataset::Info datasetInfo; + SecurityPolicy policy; + MeshCoP::Timestamp timestamp; + + SuccessOrQuit(leader.Get().Read(datasetInfo)); + + policy.SetToDefault(); + policy.mRoutersEnabled = true; + policy.mVersionThresholdForRouting = 0; + datasetInfo.Set(policy); + + timestamp.Clear(); + timestamp.SetSeconds(60); + datasetInfo.Set(timestamp); + + leader.Get().SaveLocal(datasetInfo); + } + + nexus.AdvanceTime(1 * Time::kOneSecondInMsec); + VerifyOrQuit(leader.Get().IsLeader()); + VerifyOrQuit(leader.Get().IsRouterRoleAllowed()); + + Log("Make sure both devices stay as leader and router"); + + nexus.AdvanceTime(600 * Time::kOneSecondInMsec); + VerifyOrQuit(leader.Get().IsLeader()); + VerifyOrQuit(router.Get().IsRouter()); + VerifyOrQuit(leader.Get().IsRouterRoleAllowed()); + VerifyOrQuit(router.Get().IsRouterRoleAllowed()); + + Log("---------------------------------------------------------------------------------------"); + Log("Change security policy again, disable `R` bit and set version threshold to max value (7)"); + + { + MeshCoP::Dataset::Info datasetInfo; + SecurityPolicy policy; + MeshCoP::Timestamp timestamp; + + SuccessOrQuit(leader.Get().Read(datasetInfo)); + + policy.SetToDefault(); + policy.mRoutersEnabled = false; + policy.mVersionThresholdForRouting = 7; + datasetInfo.Set(policy); + + timestamp.Clear(); + timestamp.SetSeconds(90); + datasetInfo.Set(timestamp); + + leader.Get().SaveLocal(datasetInfo); + } + + nexus.AdvanceTime(500); + VerifyOrQuit(leader.Get().IsLeader()); + + Log("Leader should take at least 10 seconds before downgrading"); + + nexus.AdvanceTime(5 * Time::kOneSecondInMsec); + VerifyOrQuit(leader.Get().IsLeader()); + VerifyOrQuit(!leader.Get().IsRouterRoleAllowed()); + + Log("Make sure both leader and router are downgraded and are now `detached`."); + + nexus.AdvanceTime(150 * Time::kOneSecondInMsec); + VerifyOrQuit(leader.Get().IsDetached()); + VerifyOrQuit(router.Get().IsDetached()); + + VerifyOrQuit(!leader.Get().IsRouterRoleAllowed()); + VerifyOrQuit(!router.Get().IsRouterRoleAllowed()); +} + +} // namespace Nexus +} // namespace ot + +int main(void) +{ + ot::Nexus::TestRouterDowngradeOnSecPolicyChange(); + printf("All tests passed\n"); + return 0; +} diff --git a/tests/scripts/thread-cert/test_router_downgrade_on_sec_policy_change.py b/tests/scripts/thread-cert/test_router_downgrade_on_sec_policy_change.py deleted file mode 100755 index 5241ad17d..000000000 --- a/tests/scripts/thread-cert/test_router_downgrade_on_sec_policy_change.py +++ /dev/null @@ -1,149 +0,0 @@ -#!/usr/bin/env python3 -# -# Copyright (c) 2023, 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 leader and router downgrade delay on security policy version threshold change. -# -# Topology: -# -# LEADER -# | -# | -# ROUTER -# - -LEADER = 1 -ROUTER = 2 - - -class DowngradeOnSecPolicyChange(thread_cert.TestCase): - USE_MESSAGE_FACTORY = False - SUPPORT_NCP = False - - TOPOLOGY = { - LEADER: { - 'name': 'LEADER', - 'mode': 'rdn', - }, - ROUTER: { - 'name': 'ROUTER', - 'mode': 'rdn', - }, - } - - def test(self): - leader = self.nodes[LEADER] - router = self.nodes[ROUTER] - - # Start the leader & router devices. - - leader.start() - self.simulator.go(config.LEADER_STARTUP_DELAY) - self.assertEqual(leader.get_state(), 'leader') - - router.start() - self.simulator.go(config.ROUTER_STARTUP_DELAY) - self.assertEqual(router.get_state(), 'router') - - self.assertTrue(leader.get_router_eligible()) - self.assertTrue(router.get_router_eligible()) - - # - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - # Change security policy, disable `R` bit and set version threshold to max value (7). - - leader.send_mgmt_active_set( - active_timestamp=30, - security_policy=[3600, 'onc', 7], - ) - - self.assertEqual(leader.get_state(), 'leader') - self.assertEqual(router.get_state(), 'router') - - # Leader should take at least 10 seconds before downgrading. - - self.simulator.go(8) - self.assertEqual(leader.get_state(), 'leader') - self.assertFalse(leader.get_router_eligible()) - self.assertFalse(router.get_router_eligible()) - - # - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - # Change back security policy. This should cancel the ongoing downgrade delay. - - leader.send_mgmt_active_set( - active_timestamp=60, - security_policy=[3600, 'ornc', 0], - ) - - self.simulator.go(1) - self.assertEqual(leader.get_state(), 'leader') - self.assertTrue(leader.get_router_eligible()) - - # Make sure both device stay as leader and router. - - self.simulator.go(600) - self.assertEqual(leader.get_state(), 'leader') - self.assertEqual(router.get_state(), 'router') - self.assertTrue(leader.get_router_eligible()) - self.assertTrue(router.get_router_eligible()) - - # - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - # Change security policy again, disable `R` bit and set version threshold to max value (7). - - leader.send_mgmt_active_set( - active_timestamp=90, - security_policy=[3600, 'onc', 7], - ) - - self.assertEqual(leader.get_state(), 'leader') - - # Leader should take at least 10 seconds before downgrading. - - self.simulator.go(8) - self.assertEqual(leader.get_state(), 'leader') - self.assertFalse(leader.get_router_eligible()) - - # Make sure both leader and router are downgraded and are now `detached`. - - self.simulator.go(80) - self.assertEqual(leader.get_state(), 'detached') - self.assertEqual(router.get_state(), 'detached') - - self.assertFalse(leader.get_router_eligible()) - self.assertFalse(router.get_router_eligible()) - - -if __name__ == '__main__': - unittest.main()