From 3913e0de614d0d86cf40e9b16cdf1ac6185626c6 Mon Sep 17 00:00:00 2001 From: Abtin Keshavarzian Date: Wed, 25 Sep 2024 10:28:53 -0700 Subject: [PATCH] [mle] enhance MTD child IPv6 address registration (#10743) This commit improves how MTD children register their IPv6 addresses with their parent. The `Slaac` class now tracks the Lowpan Context ID (from Network Data) for each SLAAC address. If the Context ID associated with an existing SLAAC address changes (due to Network Data updates), the `Slaac` module notifies the `Mle` to schedule a "Child Update Request" transmission (if the device is an MTD child). This ensures that the MTD child re-registers its addresses, resolving any previous registration failures caused by incorrect or outdated context ID compression. This commit also adds `test-035-context-id-change-addr-reg.py` to validate the newly added behavior. --- src/core/thread/mle.cpp | 33 +-- src/core/thread/mle.hpp | 9 + src/core/utils/slaac_address.cpp | 30 +++ src/core/utils/slaac_address.hpp | 6 + tests/toranj/cli/cli.py | 9 + .../test-035-context-id-change-addr-reg.py | 209 ++++++++++++++++++ tests/toranj/start.sh | 1 + 7 files changed, 284 insertions(+), 13 deletions(-) create mode 100755 tests/toranj/cli/test-035-context-id-change-addr-reg.py diff --git a/src/core/thread/mle.cpp b/src/core/thread/mle.cpp index 63e4034a846..74bc32362a4 100644 --- a/src/core/thread/mle.cpp +++ b/src/core/thread/mle.cpp @@ -138,8 +138,11 @@ Error Mle::Enable(void) void Mle::ScheduleChildUpdateRequest(void) { - mChildUpdateRequestState = kChildUpdateRequestPending; - ScheduleMessageTransmissionTimer(); + if (mChildUpdateRequestState != kChildUpdateRequestPending) + { + mChildUpdateRequestState = kChildUpdateRequestPending; + ScheduleMessageTransmissionTimer(); + } } Error Mle::Disable(void) @@ -1055,13 +1058,21 @@ void Mle::InitNeighbor(Neighbor &aNeighbor, const RxInfo &aRxInfo) aNeighbor.SetLastHeard(TimerMilli::GetNow()); } +void Mle::ScheduleChildUpdateRequestIfMtdChild(void) +{ + if (IsChild() && !IsFullThreadDevice()) + { + ScheduleChildUpdateRequest(); + } +} + void Mle::HandleNotifierEvents(Events aEvents) { VerifyOrExit(!IsDisabled()); if (aEvents.Contains(kEventThreadRoleChanged)) { - if (IsChild() && !IsFullThreadDevice() && mAddressRegistrationMode == kAppendMeshLocalOnly) + if (mAddressRegistrationMode == kAppendMeshLocalOnly) { // If only mesh-local address was registered in the "Child // ID Request" message, after device is attached, trigger a @@ -1069,7 +1080,7 @@ void Mle::HandleNotifierEvents(Events aEvents) // addresses. mAddressRegistrationMode = kAppendAllAddresses; - ScheduleChildUpdateRequest(); + ScheduleChildUpdateRequestIfMtdChild(); } } @@ -1083,10 +1094,7 @@ void Mle::HandleNotifierEvents(Events aEvents) Get().Signal(kEventThreadMeshLocalAddrChanged); } - if (IsChild() && !IsFullThreadDevice()) - { - ScheduleChildUpdateRequest(); - } + ScheduleChildUpdateRequestIfMtdChild(); } if (aEvents.ContainsAny(kEventIp6MulticastSubscribed | kEventIp6MulticastUnsubscribed)) @@ -1097,15 +1105,14 @@ void Mle::HandleNotifierEvents(Events aEvents) // parent of 1.2 or higher version as it could depend on its // parent to perform Multicast Listener Report. - if (IsChild() && !IsFullThreadDevice() && - (!IsRxOnWhenIdle() + if (!IsRxOnWhenIdle() #if (OPENTHREAD_CONFIG_THREAD_VERSION >= OT_THREAD_VERSION_1_2) - || !GetParent().IsThreadVersion1p1() + || !GetParent().IsThreadVersion1p1() #endif - )) + ) { - ScheduleChildUpdateRequest(); + ScheduleChildUpdateRequestIfMtdChild(); } } diff --git a/src/core/thread/mle.hpp b/src/core/thread/mle.hpp index 673bb7785e9..20ca9cde9f9 100644 --- a/src/core/thread/mle.hpp +++ b/src/core/thread/mle.hpp @@ -667,6 +667,15 @@ class Mle : public InstanceLocator, private NonCopyable return (&aAddress == &mLinkLocalAllThreadNodes) || (&aAddress == &mRealmLocalAllThreadNodes); } + /** + * Schedules a "Child Update Request" transmission if the device is an MTD child. + * + * For example, the `Slaac` class, which manages SLAAC addresses, calls this method to notify `Mle` that an + * existing SLAAC address's Context ID has changed. This can occur due to Network Data updates where the same + * on-mesh prefix receives a new Context ID. + */ + void ScheduleChildUpdateRequestIfMtdChild(void); + #if OPENTHREAD_CONFIG_DYNAMIC_STORE_FRAME_AHEAD_COUNTER_ENABLE /** * Sets the store frame counter ahead. diff --git a/src/core/utils/slaac_address.cpp b/src/core/utils/slaac_address.cpp index 5b17b19dfab..3164d859318 100644 --- a/src/core/utils/slaac_address.cpp +++ b/src/core/utils/slaac_address.cpp @@ -196,6 +196,15 @@ void Slaac::RemoveOrDeprecateAddresses(void) { RemoveAddress(slaacAddr); } + + if (UpdateContextIdFor(slaacAddr)) + { + // If the Context ID of an existing address changes, + // notify MLE so an MTD child can re-register its + // addresses with the parent. + + Get().ScheduleChildUpdateRequestIfMtdChild(); + } } else if (!slaacAddr.IsDeprecating()) { @@ -341,6 +350,9 @@ void Slaac::AddAddressFor(const NetworkData::OnMeshPrefixConfig &aConfig) IgnoreError(GenerateIid(*newAddress, dadCounter)); + newAddress->SetContextId(SlaacAddress::kInvalidContextId); + UpdateContextIdFor(*newAddress); + LogAddress(kAdding, *newAddress); Get().AddUnicastAddress(*newAddress); @@ -349,6 +361,24 @@ void Slaac::AddAddressFor(const NetworkData::OnMeshPrefixConfig &aConfig) return; } +bool Slaac::UpdateContextIdFor(SlaacAddress &aSlaacAddress) +{ + bool didChange = false; + Lowpan::Context context; + + if (Get().GetContext(aSlaacAddress.GetAddress(), context) != kErrorNone) + { + context.mContextId = SlaacAddress::kInvalidContextId; + } + + VerifyOrExit(context.mContextId != aSlaacAddress.GetContextId()); + aSlaacAddress.SetContextId(context.mContextId); + didChange = true; + +exit: + return didChange; +} + void Slaac::HandleTimer(void) { NextFireTime nextTime; diff --git a/src/core/utils/slaac_address.hpp b/src/core/utils/slaac_address.hpp index 48774cdd9b4..87a99b75ac5 100644 --- a/src/core/utils/slaac_address.hpp +++ b/src/core/utils/slaac_address.hpp @@ -168,8 +168,12 @@ class Slaac : public InstanceLocator, private NonCopyable class SlaacAddress : public Ip6::Netif::UnicastAddress { public: + static constexpr uint8_t kInvalidContextId = 0; + bool IsInUse(void) const { return mValid; } void MarkAsNotInUse(void) { mValid = false; } + uint8_t GetContextId(void) const { return mContextId; } + void SetContextId(uint8_t aContextId) { mContextId = aContextId; } uint8_t GetDomainId(void) const { return mDomainId; } void SetDomainId(uint8_t aDomainId) { mDomainId = aDomainId; } bool IsDeprecating(void) const { return (mExpirationTime.GetValue() != kNotDeprecated); }; @@ -188,6 +192,7 @@ class Slaac : public InstanceLocator, private NonCopyable private: static constexpr uint32_t kNotDeprecated = 0; // Special `mExpirationTime` value to indicate not deprecated. + uint8_t mContextId; uint8_t mDomainId; TimeMilli mExpirationTime; }; @@ -200,6 +205,7 @@ class Slaac : public InstanceLocator, private NonCopyable void DeprecateAddress(SlaacAddress &aAddress); void RemoveAddress(SlaacAddress &aAddress); void AddAddressFor(const NetworkData::OnMeshPrefixConfig &aConfig); + bool UpdateContextIdFor(SlaacAddress &aSlaacAddress); void HandleTimer(void); void GetIidSecretKey(IidSecretKey &aKey) const; void HandleNotifierEvents(Events aEvents); diff --git a/tests/toranj/cli/cli.py b/tests/toranj/cli/cli.py index fef3be27edd..ab310b74608 100644 --- a/tests/toranj/cli/cli.py +++ b/tests/toranj/cli/cli.py @@ -351,6 +351,12 @@ def get_pollperiod(self): def set_pollperiod(self, period): self._cli_no_output('pollperiod', period) + def get_child_timeout(self): + return self._cli_single_output('childtimeout') + + def set_child_timeout(self, timeout): + self._cli_no_output('childtimeout', timeout) + def get_partition_id(self): return self._cli_single_output('partitionid') @@ -368,6 +374,9 @@ def get_parent_info(self): def get_child_table(self): return Node.parse_table(self.cli('child table')) + def get_child_ip(self): + return self.cli('childip') + def get_neighbor_table(self): return Node.parse_table(self.cli('neighbor table')) diff --git a/tests/toranj/cli/test-035-context-id-change-addr-reg.py b/tests/toranj/cli/test-035-context-id-change-addr-reg.py new file mode 100755 index 00000000000..9e725143137 --- /dev/null +++ b/tests/toranj/cli/test-035-context-id-change-addr-reg.py @@ -0,0 +1,209 @@ +#!/usr/bin/env python3 +# +# Copyright (c) 2024, 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 the behavior of MTD children regarding the +# registration of their IPv6 addresses with their parent. +# Specifically, it covers the scenario where SLAAC-based addresses +# remain unchanged, but their assigned LoWPAN Context ID for the +# corresponding on-mesh prefix in the Network Data changes. In this +# case, the MTD child should schedule an MLE Child Update Request +# exchange with its parent to re-register the addresses. This ensures +# that any earlier registration failures due to incorrect context ID +# compression are resolved. +# + +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() +sed = cli.Node() + +# ----------------------------------------------------------------------------------------------------------------------- +# Form topology + +leader.form('cid-chng') + +verify(leader.get_state() == 'leader') + +# ----------------------------------------------------------------------------------------------------------------------- +# Test Implementation + +# Set the "context ID reuse delay" to a short interval of 3 seconds. + +leader.set_context_reuse_delay(3) +verify(int(leader.get_context_reuse_delay()) == 3) + +# Add two on-link prefixes on `leader`. + +leader.add_prefix('fd00:1::/64', 'poas') +leader.add_prefix('fd00:2::/64', 'poas') +leader.register_netdata() + +# Check that context ID 2 is assigned to prefix `fd00:2::/64`. + +time.sleep(0.5 / speedup) + +netdata = leader.get_netdata() +contexts = netdata['contexts'] +verify(len(contexts) == 2) +verify(any([context.startswith('fd00:1:0:0::/64 1 c') for context in contexts])) +verify(any([context.startswith('fd00:2:0:0::/64 2 c') for context in contexts])) + +# Remove the first prefix. + +leader.remove_prefix('fd00:1::/64') +leader.register_netdata() + +# Wait longer than Context ID reuse delay to ensure that context ID +# associated with the removed prefix is aged and removed. + +# Wait longer than the Context ID reuse delay to ensure the removed +# prefix's associated context ID is aged out and removed. + +time.sleep(3.5 / speedup) + +netdata = leader.get_netdata() +contexts = netdata['contexts'] +verify(len(contexts) == 1) +verify(any([context.startswith('fd00:2:0:0::/64 2 c') for context in contexts])) + +# Have `sed` attach as a child of `leader`. + +sed.set_child_timeout(10) +sed.set_pollperiod(1000) + +sed.join(leader, cli.JOIN_TYPE_SLEEPY_END_DEVICE) +verify(sed.get_state() == 'child') + +verify(int(sed.get_child_timeout()) == 10) + +# Check the Network Data on `sed`. + +netdata = sed.get_netdata() +contexts = netdata['contexts'] +verify(len(contexts) == 1) +verify(any([context.startswith('fd00:2:0:0::/64 2 c') for context in contexts])) + +# Find the `sed` address associated with on-mesh prefix `fd00:2::`. + +sed_ip_addrs = sed.get_ip_addrs() + +for ip_addr in sed_ip_addrs: + if ip_addr.startswith('fd00:2:0:0:'): + sed_addr = ip_addr + break +else: + verify(False) + +# Check the parent's child table, and that `sed` child has registered its +# IPv6 addresses with the parent. + +verify(len(leader.get_child_table()) == 1) +ip_addrs = leader.get_child_ip() +verify(len(ip_addrs) == 2) + +# Stop MLE operation on `sed` + +sed.thread_stop() +verify(sed.get_state() == 'disabled') + +# Remove the `fd00::2` prefix and wait for the child entry +# on parent to expire. Child timeout is set to 10 seconds. + +leader.remove_prefix('fd00:2::/64') +leader.register_netdata() + +time.sleep(10.5 / speedup) + +# Validate that the Context ID associated with `fd00::2` is expired +# and removed. + +netdata = leader.get_netdata() +contexts = netdata['contexts'] +verify(len(contexts) == 0) + +# Re-add the `fd00::2` prefix and check that it gets a different +# Context ID. + +leader.add_prefix('fd00:2::/64', 'poas') +leader.register_netdata() + +time.sleep(0.5 / speedup) + +netdata = leader.get_netdata() +contexts = netdata['contexts'] +verify(len(contexts) == 1) +verify(any([context.startswith('fd00:2:0:0::/64 1 c') for context in contexts])) + +# Make sure that child is timed out and removed on parent. + +verify(leader.get_child_table() == []) + +# Re-enable MLE operation on `sed` for it to attach back. + +sed.thread_start() + +time.sleep(5.0 / speedup) + +verify(sed.get_state() == 'child') + +# Check the `sed` IPv6 addresses and that `sed` still has the same +# SLAAC address based on the `fd00:2::` prefix + +sed_ip_addrs = sed.get_ip_addrs() +verify(any([ip_addr == sed_addr for ip_addr in sed_ip_addrs])) + +# Validate that all `sed` IPv6 addresses are successfully +# registered on parent. + +verify(len(leader.get_child_table()) == 1) +ip_addrs = leader.get_child_ip() +verify(len(ip_addrs) == 2) +verify(any([ip_addr.endswith(sed_addr) for ip_addr in ip_addrs])) + +# ----------------------------------------------------------------------------------------------------------------------- +# 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 482774c854c..5451c51bffc 100755 --- a/tests/toranj/start.sh +++ b/tests/toranj/start.sh @@ -197,6 +197,7 @@ if [ "$TORANJ_CLI" = 1 ]; then run cli/test-030-anycast-forwarding.py run cli/test-031-service-aloc-route-lookup.py run cli/test-032-leader-take-over.py + run cli/test-035-context-id-change-addr-reg.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