From fa15976e9bf35973ee01eb1c4b4afc8f39285904 Mon Sep 17 00:00:00 2001 From: Abtin Keshavarzian Date: Thu, 17 Sep 2026 13:47:35 -0700 Subject: [PATCH] [br] do not deprecate old prefix if advertised as preferred by others (#13618) When `OnLinkPrefixManager` deprecates an old local on-link prefix (e.g., following an Extended PAN ID change), it includes the prefix as a PIO with zero preferred lifetime in emitted Router Advertisements until its valid lifetime expires. However, if another router on the infrastructure link is actively advertising the same prefix as a preferred on-link prefix (with a non-zero preferred lifetime), continuing to emit deprecating PIOs creates conflicting advertisements on the link, which can cause hosts to switch between deprecating and preferring their SLAAC addresses. This commit adds new logic to check for this: - Adds `mDeprecateTime` to `OldPrefix` to track when deprecation of an old prefix began (and initializes it to the current time when restoring old prefixes from persistent storage). - Introduces `RxRaTracker::HasSeenPreferredOnLinkPrefixAfter()` to check whether an RA received after a given timestamp advertises the prefix as an on-link and preferred (non-deprecated) prefix. - In `OnLinkPrefixManager::AppendOldPrefixes()`, skips emitting the deprecation PIO if another router has advertised the prefix as preferred after `mDeprecateTime`. This commit also adds the `TestOldOnLinkPrefixAdvertisedOnLink` unit test in `test_routing_manager` to validate that deprecation PIOs are suppressed when an external router advertises the prefix as preferred and resumed if the external router later deprecates it. --- src/core/border_router/routing_manager.cpp | 20 ++- src/core/border_router/routing_manager.hpp | 1 + src/core/border_router/rx_ra_tracker.cpp | 21 +++ src/core/border_router/rx_ra_tracker.hpp | 12 ++ tests/unit/test_routing_manager.cpp | 142 ++++++++++++++++++++- 5 files changed, 188 insertions(+), 8 deletions(-) diff --git a/src/core/border_router/routing_manager.cpp b/src/core/border_router/routing_manager.cpp index 96ccc4284..6929f8e20 100644 --- a/src/core/border_router/routing_manager.cpp +++ b/src/core/border_router/routing_manager.cpp @@ -1272,8 +1272,9 @@ void RoutingManager::OnLinkPrefixManager::Init(void) lifetime = Min(savedPrefix.GetLifetime(), Time::MsecToSec(TimerMilli::kMaxDelay)); - entry->mPrefix = savedPrefix.GetPrefix(); - entry->mExpireTime = now + Time::SecToMsec(lifetime); + entry->mPrefix = savedPrefix.GetPrefix(); + entry->mDeprecateTime = now; + entry->mExpireTime = now + Time::SecToMsec(lifetime); LogInfo("Restored old prefix %s, lifetime:%lu", entry->mPrefix.ToString().AsCString(), ToUlong(lifetime)); @@ -1658,6 +1659,16 @@ Error RoutingManager::OnLinkPrefixManager::AppendOldPrefixes(RouterAdvert::TxMes continue; } + // If another router on the infrastructure link is actively + // advertising this prefix as a preferred on-link prefix after we + // started deprecating it, we skip including it as a deprecating + // PIO to avoid sending conflicting advertisements. + + if (Get().HasSeenPreferredOnLinkPrefixAfter(oldPrefix.mPrefix, oldPrefix.mDeprecateTime)) + { + continue; + } + validLifetime = TimeMilli::MsecToSec(oldPrefix.mExpireTime - now); flags = PrefixInfoOption::kOnLinkFlag | PrefixInfoOption::kAutoConfigFlag; @@ -1755,8 +1766,9 @@ void RoutingManager::OnLinkPrefixManager::DeprecateOldPrefix(const Ip6::Prefix & Get().RemoveBrOnLinkPrefix(removedPrefix); } - entry->mPrefix = aPrefix; - entry->mExpireTime = aExpireTime; + entry->mPrefix = aPrefix; + entry->mDeprecateTime = TimerMilli::GetNow(); + entry->mExpireTime = aExpireTime; mTimer.FireAtIfEarlier(aExpireTime); SavePrefix(aPrefix, aExpireTime); diff --git a/src/core/border_router/routing_manager.hpp b/src/core/border_router/routing_manager.hpp index 1b1988c01..942a82cbd 100644 --- a/src/core/border_router/routing_manager.hpp +++ b/src/core/border_router/routing_manager.hpp @@ -742,6 +742,7 @@ private: bool Matches(const Ip6::Prefix &aPrefix) const { return mPrefix == aPrefix; } Ip6::Prefix mPrefix; + TimeMilli mDeprecateTime; TimeMilli mExpireTime; }; diff --git a/src/core/border_router/rx_ra_tracker.cpp b/src/core/border_router/rx_ra_tracker.cpp index 6a63d7aa5..42cbb59a5 100644 --- a/src/core/border_router/rx_ra_tracker.cpp +++ b/src/core/border_router/rx_ra_tracker.cpp @@ -1183,6 +1183,27 @@ exit: return isOnLink; } +bool RxRaTracker::HasSeenPreferredOnLinkPrefixAfter(const Ip6::Prefix &aPrefix, TimeMilli aTime) const +{ + bool hasSeen = false; + + for (const Router &router : mRouters) + { + for (const OnLinkPrefix &onLinkPrefix : router.mOnLinkPrefixes) + { + if (!onLinkPrefix.IsDeprecated() && onLinkPrefix.Matches(aPrefix) && + (onLinkPrefix.GetLastUpdateTime() >= aTime)) + { + hasSeen = true; + ExitNow(); + } + } + } + +exit: + return hasSeen; +} + bool RxRaTracker::IsPrefixOnLink(const Ip6::Prefix &aPrefix) const { bool isOnLink = false; diff --git a/src/core/border_router/rx_ra_tracker.hpp b/src/core/border_router/rx_ra_tracker.hpp index c641d6548..353a7b31d 100644 --- a/src/core/border_router/rx_ra_tracker.hpp +++ b/src/core/border_router/rx_ra_tracker.hpp @@ -307,6 +307,18 @@ public: */ bool IsAddressReachableThroughExplicitRoute(const Ip6::Address &aAddress) const; + /** + * Indicates whether a given prefix is seen as a preferred (non-deprecated) on-link prefix in any tracked RA + * received after a given time. + * + * @param[in] aPrefix The IPv6 prefix to check. + * @param[in] aTime The time threshold. + * + * @retval TRUE The prefix is seen as a preferred on-link prefix after @p aTime. + * @retval FALSE The prefix is not seen as a preferred on-link prefix after @p aTime. + */ + bool HasSeenPreferredOnLinkPrefixAfter(const Ip6::Prefix &aPrefix, TimeMilli aTime) const; + /** * Indicates whether a given prefix is seen as an on-link prefix in any tracked RA. * diff --git a/tests/unit/test_routing_manager.cpp b/tests/unit/test_routing_manager.cpp index 3664000ad..90b3a1179 100644 --- a/tests/unit/test_routing_manager.cpp +++ b/tests/unit/test_routing_manager.cpp @@ -2780,7 +2780,7 @@ void TestLocalOnLinkPrefixDeprecation(void) void TestUnadvertisedLocalOnLinkPrefix(void) { - static const otExtendedPanId kExtPanId1 = {{0x01, 0x02, 0x03, 0x04, 0x05, 0x6, 0x7, 0x08}}; + static const otExtendedPanId kExtPanId1 = {{0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08}}; Ip6::Prefix localOnLink; Ip6::Prefix oldLocalOnLink; @@ -2931,7 +2931,7 @@ void TestExtPanIdChange(void) { static constexpr uint32_t kMaxRaTxInterval = 196; // In seconds - static const otExtendedPanId kExtPanId1 = {{0x01, 0x02, 0x03, 0x04, 0x05, 0x6, 0x7, 0x08}}; + static const otExtendedPanId kExtPanId1 = {{0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08}}; static const otExtendedPanId kExtPanId2 = {{0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff, 0x99, 0x88}}; static const otExtendedPanId kExtPanId3 = {{0x12, 0x34, 0x56, 0x78, 0x9a, 0xab, 0xcd, 0xef}}; static const otExtendedPanId kExtPanId4 = {{0x44, 0x00, 0x44, 0x00, 0x44, 0x00, 0x44, 0x00}}; @@ -3437,6 +3437,139 @@ void TestExtPanIdChange(void) FinalizeTest(); } +void TestOldOnLinkPrefixAdvertisedOnLink(void) +{ + static const otExtendedPanId kExtPanId1 = {{0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08}}; + + Ip6::Prefix localOnLink; + Ip6::Prefix oldLocalOnLink; + Ip6::Prefix localOmr; + Ip6::Address routerAddressA = AddressFromString("fd00::aaaa"); + otOperationalDataset dataset; + uint16_t heapAllocations; + + Log("--------------------------------------------------------------------------------------------"); + Log("TestOldOnLinkPrefixAdvertisedOnLink"); + + InitTest(); + + //- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + // Start Routing Manager. Check emitted RS and RA messages. + + heapAllocations = sHeapAllocatedPtrs.GetLength(); + SuccessOrQuit(sInstance->Get().SetEnabled(true)); + + SuccessOrQuit(sInstance->Get().GetOnLinkPrefix(localOnLink)); + SuccessOrQuit(sInstance->Get().GetOmrPrefix(localOmr)); + + Log("Local on-link prefix is %s", localOnLink.ToString().AsCString()); + Log("Local OMR prefix is %s", localOmr.ToString().AsCString()); + + sRsEmitted = false; + sRaValidated = false; + sExpectedPio = kPioAdvertisingLocalOnLink; + sExpectedRios.Clear(); + sExpectedRios.Add(localOmr); + + AdvanceTime(30000); + + VerifyOrQuit(sRsEmitted); + VerifyOrQuit(sRaValidated); + VerifyOrQuit(sExpectedRios.SawAll()); + + //- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + // Change the extended PAN ID to deprecate the current local on-link prefix. + + Log("Changing ext PAN ID"); + + oldLocalOnLink = localOnLink; + + SuccessOrQuit(otDatasetGetActive(sInstance, &dataset)); + VerifyOrQuit(dataset.mComponents.mIsExtendedPanIdPresent); + + dataset.mExtendedPanId = kExtPanId1; + dataset.mActiveTimestamp.mSeconds++; + SuccessOrQuit(otDatasetSetActive(sInstance, &dataset)); + + AdvanceTime(500); + SuccessOrQuit(sInstance->Get().GetOnLinkPrefix(localOnLink)); + VerifyOrQuit(localOnLink != oldLocalOnLink); + Log("Local on-link prefix changed to %s from %s", localOnLink.ToString().AsCString(), + oldLocalOnLink.ToString().AsCString()); + + //- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + // Validate that the old local on-link prefix is initially included as a + // deprecating PIO in the emitted RA. + + sRaValidated = false; + sExpectedPio = kPioAdvertisingLocalOnLink; + + AdvanceTime(30000); + + VerifyOrQuit(sRaValidated); + VerifyOrQuit(sDeprecatingPrefixes.GetLength() == 1); + VerifyOrQuit(sDeprecatingPrefixes[0].mPrefix == oldLocalOnLink); + + //- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + // Send an RA from router A advertising `oldLocalOnLink` as a preferred on-link prefix. + // Ensure that it is no longer included as a deprecating PIO in emitted RAs. + + Log("Router A advertises old on-link prefix as preferred"); + + SendRouterAdvert(routerAddressA, {Pio(oldLocalOnLink, kValidLitime, kPreferredLifetime)}); + + sRaValidated = false; + sExpectedPio = kPioAdvertisingLocalOnLink; + + AdvanceTime(30000); + + VerifyOrQuit(sRaValidated); + VerifyOrQuit(sDeprecatingPrefixes.IsEmpty()); + + //- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + // Send an RA from router A deprecating `oldLocalOnLink` (zero preferred lifetime). + // Ensure that this BR resumes including `oldLocalOnLink` as a deprecating PIO. + + Log("Router A deprecates old on-link prefix"); + + SendRouterAdvert(routerAddressA, {Pio(oldLocalOnLink, kValidLitime, 0)}); + + sRaValidated = false; + sExpectedPio = kPioAdvertisingLocalOnLink; + + AdvanceTime(30000); + + VerifyOrQuit(sRaValidated); + VerifyOrQuit(sDeprecatingPrefixes.GetLength() == 1); + VerifyOrQuit(sDeprecatingPrefixes[0].mPrefix == oldLocalOnLink); + + //- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + // Router A advertises `oldLocalOnLink` as preferred again. + // Ensure that the deprecating PIO is suppressed once more. + + Log("Router A advertises old on-link prefix as preferred again"); + + SendRouterAdvert(routerAddressA, {Pio(oldLocalOnLink, kValidLitime, kPreferredLifetime)}); + + sRaValidated = false; + sExpectedPio = kPioAdvertisingLocalOnLink; + + AdvanceTime(30000); + + VerifyOrQuit(sRaValidated); + VerifyOrQuit(sDeprecatingPrefixes.IsEmpty()); + + //- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + SuccessOrQuit(sInstance->Get().SetEnabled(false)); + AdvanceTime(3000); + + VerifyOrQuit(heapAllocations == sHeapAllocatedPtrs.GetLength()); + + Log("End of TestOldOnLinkPrefixAdvertisedOnLink"); + FinalizeTest(); +} + void TestPrefixStaleTime(void) { Ip6::Prefix localOnLink; @@ -3996,7 +4129,7 @@ void TestLearnRaHeader(void) void TestConflictingPrefix(void) { - static const otExtendedPanId kExtPanId1 = {{0x01, 0x02, 0x03, 0x04, 0x05, 0x6, 0x7, 0x08}}; + static const otExtendedPanId kExtPanId1 = {{0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08}}; Ip6::Prefix localOnLink; Ip6::Prefix oldLocalOnLink; @@ -4219,7 +4352,7 @@ void TestConflictingPrefix(void) #if OPENTHREAD_CONFIG_PLATFORM_FLASH_API_ENABLE void TestSavedOnLinkPrefixes(void) { - static const otExtendedPanId kExtPanId1 = {{0x01, 0x02, 0x03, 0x04, 0x05, 0x6, 0x7, 0x08}}; + static const otExtendedPanId kExtPanId1 = {{0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08}}; Ip6::Prefix localOnLink; Ip6::Prefix oldLocalOnLink; @@ -5611,6 +5744,7 @@ int main(void) ot::TestLocalOnLinkPrefixDeprecation(); ot::TestUnadvertisedLocalOnLinkPrefix(); ot::TestExtPanIdChange(); + ot::TestOldOnLinkPrefixAdvertisedOnLink(); ot::TestConflictingPrefix(); ot::TestPrefixStaleTime(); ot::TestRouterNsProbe();