From 283edc0bd5ec5d8a835c708f3c309b073d292782 Mon Sep 17 00:00:00 2001 From: Abtin Keshavarzian Date: Tue, 22 Apr 2025 13:44:12 -0700 Subject: [PATCH] [routing-manager] introduce `MultiAilDetector` (#11400) This commit introduces the `MultiAilDetector` feature within the `RoutingManager`. This feature detects whether Border Routers(BRs) on the Thread mesh might be connected to different Adjacent Infrastructure Links (AILs). The feature can be enabled using the configuration option `OPENTHREAD_CONFIG_BORDER_ROUTING_MULTI_AIL_DETECTION_ENABLE`. The detection mechanism operates as follows: The Routing Manager monitors the number of peer BRs listed in the Thread Network Data and compares this with the number of peer BRs discovered by processing received Router Advertisements (RAs) on its local AIL. If the count derived from Network Data consistently exceeds the count derived from RAs for a detection period of 5 minutes, the detector concludes that BRs are likely connected to different AILs. This triggers a detection state change, and a registered callback is invoked. To clear this state, a shorter window of 1 minute is used. Public APIs and corresponding CLI commands have been added to allow checking the current detection state and registering a callback for state change notifications. This commit also includes test coverage for the newly added feature. --- include/openthread/border_routing.h | 62 ++++++++ include/openthread/instance.h | 2 +- src/cli/README_BR.md | 42 ++++++ src/cli/cli_br.cpp | 67 +++++++++ src/cli/cli_br.hpp | 5 + src/core/api/border_routing_api.cpp | 16 ++ src/core/border_router/routing_manager.cpp | 114 ++++++++++++++ src/core/border_router/routing_manager.hpp | 95 ++++++++++++ src/core/config/border_routing.h | 17 +++ tests/toranj/cli/cli.py | 3 + .../cli/test-505-multi-ail-detection.py | 142 ++++++++++++++++++ tests/toranj/start.sh | 1 + 12 files changed, 565 insertions(+), 1 deletion(-) create mode 100755 tests/toranj/cli/test-505-multi-ail-detection.py diff --git a/include/openthread/border_routing.h b/include/openthread/border_routing.h index 29f22ddf8..5ed54bf8f 100644 --- a/include/openthread/border_routing.h +++ b/include/openthread/border_routing.h @@ -596,6 +596,68 @@ otError otBorderRoutingGetNextPeerBrEntry(otInstance * */ uint16_t otBorderRoutingCountPeerBrs(otInstance *aInstance, uint32_t *aMinAge); +/** + * A callback function pointer called when the multi-AIL detection state changes. + * + * This callback function is invoked by the OpenThread stack whenever the Routing Manager determines a change in + * whether Border Routers on the Thread mesh might be connected to different Adjacent Infrastructure Links (AILs). + * + * See `otBorderRoutingIsMultiAilDetected()` for more details. + * + * @param[in] aDetected `TRUE` if multiple AILs are now detected, `FALSE` otherwise. + * @param[in] aContext A pointer to arbitrary context information provided when the callback was registered + * using `otBorderRoutingSetMultiAilCallback()`. + */ +typedef void (*otBorderRoutingMultiAilCallback)(bool aDetected, void *aContext); + +/** + * Gets the current detected state regarding multiple Adjacent Infrastructure Links (AILs). + * + * Requires `OPENTHREAD_CONFIG_BORDER_ROUTING_MULTI_AIL_DETECTION_ENABLE`. + * + * It returns whether the Routing Manager currently believes that Border Routers (BRs) on the Thread mesh may be + * connected to different AILs. + * + * The detection mechanism operates as follows: The Routing Manager monitors the number of peer BRs listed in the + * Thread Network Data (see `otBorderRoutingCountPeerBrs()`) and compares this count with the number of peer BRs + * discovered by processing received Router Advertisement (RA) messages on its connected AIL. If the count derived from + * Network Data consistently exceeds the count derived from RAs for a detection duration of 10 minutes, it concludes + * that BRs are likely connected to different AILs. To clear state a shorter window of 1 minute is used. + * + * The detection window of 10 minutes helps to avoid false positives due to transient changes. The Routing Manager uses + * 200 seconds for reachability checks of peer BRs (sending Neighbor Solicitation). Stale Network Data entries are + * also expected to age out within a few minutes. So a 10-minute detection time accommodates both cases. + * + * While generally effective, this detection mechanism may get less reliable in scenarios with a large number of + * BRs, particularly exceeding ten. This is related to the "Network Data Publisher" mechanism, where BRs might refrain + * from publishing their external route information in the Network Data to conserve its limited size, potentially + * skewing the Network Data BR count. + * + * @param[in] aInstance A pointer to the OpenThread instance. + * + * @retval TRUE Has detected that BRs are likely connected to multiple AILs. + * @retval FALSE Has not detected (or no longer detects) that BRs are connected to multiple AILs. + */ +bool otBorderRoutingIsMultiAilDetected(otInstance *aInstance); + +/** + * Sets a callback function to be notified of changes in the multi-AIL detection state. + * + * Requires `OPENTHREAD_CONFIG_BORDER_ROUTING_MULTI_AIL_DETECTION_ENABLE`. + * + * Subsequent calls to this function will overwrite the previous callback setting. Using `NULL` for @p aCallback will + * disable the callback. + * + * @param[in] aInstance A pointer to the OpenThread instance. + * @param[in] aCallback A pointer to the function (`otBorderRoutingMultiAilCallback`) to be called + * upon state changes, or `NULL` to unregister a previously set callback. + * @param[in] aContext A pointer to application-specific context that will be passed back + * in the `aCallback` function. This can be `NULL` if no context is needed. + */ +void otBorderRoutingSetMultiAilCallback(otInstance *aInstance, + otBorderRoutingMultiAilCallback aCallback, + void *aContext); + /** * Iterates over the Recursive DNS Server (RDNSS) address entries. * diff --git a/include/openthread/instance.h b/include/openthread/instance.h index 6549a0719..a8cd3b5fd 100644 --- a/include/openthread/instance.h +++ b/include/openthread/instance.h @@ -52,7 +52,7 @@ extern "C" { * * @note This number versions both OpenThread platform and user APIs. */ -#define OPENTHREAD_API_VERSION (498) +#define OPENTHREAD_API_VERSION (499) /** * @addtogroup api-instance diff --git a/src/cli/README_BR.md b/src/cli/README_BR.md index dcc9b1088..be98847db 100644 --- a/src/cli/README_BR.md +++ b/src/cli/README_BR.md @@ -33,6 +33,7 @@ Print BR command help menu. counters disable enable +multiail omrconfig omrprefix onlinkprefix @@ -118,6 +119,47 @@ RS TxFailed: 0 Done ``` +### multiail + +Usage : `br multiail` + +Requires `OPENTHREAD_CONFIG_BORDER_ROUTING_MULTI_AIL_DETECTION_ENABLE`. + +Get the current detected state regarding multiple Adjacent Infrastructure Links (AILs) indicating whether the Routing Manager currently believes that Border Routers (BRs) on the Thread mesh may be connected to different AILs. + +The detection mechanism operates as follows: The Routing Manager monitors the number of peer BRs listed in the Thread Network Data (see `br peers`) and compares this count with the number of peer BRs discovered by processing received Router Advertisement (RA) messages on its connected AIL. If the count derived from Network Data consistently exceeds the count derived from RAs for a detection duration of 10 minutes, it concludes that BRs are likely connected to different AILs. To clear the state a shorter window of 1 minute is used. + +The detection window of 10 minutes helps to avoid false positives due to transient changes. The Routing Manager uses 200 seconds for reachability checks of peer BRs (sending Neighbor Solicitation). Stale Network Data entries are also expected to age out within a few minutes. So a 10-minute detection time accommodates both cases. + +While generally effective, this detection mechanism may get less reliable in scenarios with a large number of BRs, particularly exceeding ten. This is related to the "Network Data Publisher" mechanism, where BRs might refrain from publishing their external route information in the Network Data to conserve its limited size, potentially skewing the Network Data BR count. + +```bash +> br multiail +not detected +Done + +> br multiail +detected +Done +``` + +Usage: `br multiail callback enable|disable` + +Enable or disable callback to be notified of changes in the multi-AIL detection state. + +```bash +> br multiail callback enable +Done + +BR multi AIL callback: detected + +> br multiail +detected +Done + +BR multi AIL callback: cleared +``` + ### omrconfig Usage: `br omrconfig` diff --git a/src/cli/cli_br.cpp b/src/cli/cli_br.cpp index 5f0bfa494..156f68b14 100644 --- a/src/cli/cli_br.cpp +++ b/src/cli/cli_br.cpp @@ -143,6 +143,70 @@ exit: return error; } +#if OPENTHREAD_CONFIG_BORDER_ROUTING_MULTI_AIL_DETECTION_ENABLE + +template <> otError Br::Process(Arg aArgs[]) +{ + otError error = OT_ERROR_NONE; + + /** + * @cli br multiail + * @code + * br multiail + * not detected + * @endcode + * @par api_copy + * #otBorderRoutingIsMultiAilDetected + */ + if (aArgs[0].IsEmpty()) + { + OutputLine("%sdetected", otBorderRoutingIsMultiAilDetected(GetInstancePtr()) ? "" : "not "); + } + /** + * @cli br multiail callback + * @code + * br multiail callback enable + * Done + * @endcode + * @cparam br multiail callback @ca{enable|disable} + * @par api_copy + * #otBorderRoutingSetMultiAilCallback + */ + else if (aArgs[0] == "callback") + { + bool enable; + otBorderRoutingMultiAilCallback callback = nullptr; + + SuccessOrExit(error = ParseEnableOrDisable(aArgs[1], enable)); + + if (enable) + { + callback = &HandleMultiAilDetected; + } + + otBorderRoutingSetMultiAilCallback(GetInstancePtr(), callback, this); + } + else + { + error = OT_ERROR_INVALID_ARGS; + } + +exit: + return error; +} + +void Br::HandleMultiAilDetected(bool aDetected, void *aContext) +{ + static_cast
(aContext)->HandleMultiAilDetected(aDetected); +} + +void Br::HandleMultiAilDetected(bool aDetected) +{ + OutputLine("BR multi AIL callback: %s", aDetected ? "detected" : "cleared"); +} + +#endif // OPENTHREAD_CONFIG_BORDER_ROUTING_MULTI_AIL_DETECTION_ENABLE + otError Br::ParsePrefixTypeArgs(Arg aArgs[], PrefixType &aFlags) { otError error = OT_ERROR_NONE; @@ -1030,6 +1094,9 @@ otError Br::Process(Arg aArgs[]) CmdEntry("disable"), CmdEntry("enable"), CmdEntry("init"), +#if OPENTHREAD_CONFIG_BORDER_ROUTING_MULTI_AIL_DETECTION_ENABLE + CmdEntry("multiail"), +#endif #if OPENTHREAD_CONFIG_NAT64_BORDER_ROUTING_ENABLE CmdEntry("nat64prefix"), #endif diff --git a/src/cli/cli_br.hpp b/src/cli/cli_br.hpp index 26d56d8b0..d5c13793f 100644 --- a/src/cli/cli_br.hpp +++ b/src/cli/cli_br.hpp @@ -93,6 +93,11 @@ private: otError ParsePrefixTypeArgs(Arg aArgs[], PrefixType &aFlags); void OutputRouterInfo(const otBorderRoutingRouterEntry &aEntry, RouterOutputMode aMode); + +#if OPENTHREAD_CONFIG_BORDER_ROUTING_MULTI_AIL_DETECTION_ENABLE + static void HandleMultiAilDetected(bool aDetected, void *aContext); + void HandleMultiAilDetected(bool aDetected); +#endif }; } // namespace Cli diff --git a/src/core/api/border_routing_api.cpp b/src/core/api/border_routing_api.cpp index 3f4ad0e9c..183db0693 100644 --- a/src/core/api/border_routing_api.cpp +++ b/src/core/api/border_routing_api.cpp @@ -257,6 +257,22 @@ uint16_t otBorderRoutingCountPeerBrs(otInstance *aInstance, uint32_t *aMinAge) #endif +#if OPENTHREAD_CONFIG_BORDER_ROUTING_MULTI_AIL_DETECTION_ENABLE + +bool otBorderRoutingIsMultiAilDetected(otInstance *aInstance) +{ + return AsCoreType(aInstance).Get().IsMultiAilDetected(); +} + +void otBorderRoutingSetMultiAilCallback(otInstance *aInstance, + otBorderRoutingMultiAilCallback aCallback, + void *aContext) +{ + AsCoreType(aInstance).Get().SetMultiAilCallback(aCallback, aContext); +} + +#endif + #if OPENTHREAD_CONFIG_BORDER_ROUTING_DHCP6_PD_ENABLE void otBorderRoutingDhcp6PdSetEnabled(otInstance *aInstance, bool aEnabled) diff --git a/src/core/border_router/routing_manager.cpp b/src/core/border_router/routing_manager.cpp index 43789c6b0..508cdc17d 100644 --- a/src/core/border_router/routing_manager.cpp +++ b/src/core/border_router/routing_manager.cpp @@ -59,6 +59,9 @@ RoutingManager::RoutingManager(Instance &aInstance) , mOnLinkPrefixManager(aInstance) #if OPENTHREAD_CONFIG_BORDER_ROUTING_TRACK_PEER_BR_INFO_ENABLE , mNetDataPeerBrTracker(aInstance) +#endif +#if OPENTHREAD_CONFIG_BORDER_ROUTING_MULTI_AIL_DETECTION_ENABLE + , mMultiAilDetector(aInstance) #endif , mRxRaTracker(aInstance) , mRoutePublisher(aInstance) @@ -308,6 +311,9 @@ void RoutingManager::Start(void) #endif #if OPENTHREAD_CONFIG_NAT64_BORDER_ROUTING_ENABLE mNat64PrefixManager.Start(); +#endif +#if OPENTHREAD_CONFIG_BORDER_ROUTING_MULTI_AIL_DETECTION_ENABLE + mMultiAilDetector.Start(); #endif } } @@ -324,6 +330,9 @@ void RoutingManager::Stop(void) #if OPENTHREAD_CONFIG_NAT64_BORDER_ROUTING_ENABLE mNat64PrefixManager.Stop(); #endif +#if OPENTHREAD_CONFIG_BORDER_ROUTING_MULTI_AIL_DETECTION_ENABLE + mMultiAilDetector.Stop(); +#endif SendRouterAdvertisement(kInvalidateAllPrevPrefixes); @@ -762,6 +771,9 @@ void RoutingManager::HandleRaPrefixTableChanged(void) mOnLinkPrefixManager.HandleRaPrefixTableChanged(); mRoutePublisher.Evaluate(); +#if OPENTHREAD_CONFIG_BORDER_ROUTING_MULTI_AIL_DETECTION_ENABLE + mMultiAilDetector.Evaluate(); +#endif exit: return; @@ -1077,6 +1089,7 @@ void RoutingManager::RdnssAddress::CopyInfoTo(RdnssAddrEntry &aEntry, TimeMilli // NetDataPeerBrTracker #if OPENTHREAD_CONFIG_BORDER_ROUTING_TRACK_PEER_BR_INFO_ENABLE + RoutingManager::NetDataPeerBrTracker::NetDataPeerBrTracker(Instance &aInstance) : InstanceLocator(aInstance) { @@ -1152,12 +1165,97 @@ void RoutingManager::NetDataPeerBrTracker::HandleNotifierEvents(Events aEvents) mPeerBrs.Push(*newEntry); } +#if OPENTHREAD_CONFIG_BORDER_ROUTING_MULTI_AIL_DETECTION_ENABLE + Get().mMultiAilDetector.Evaluate(); +#endif + exit: return; } #endif // OPENTHREAD_CONFIG_BORDER_ROUTING_TRACK_PEER_BR_INFO_ENABLE +//--------------------------------------------------------------------------------------------------------------------- +// MultiAilDetector + +#if OPENTHREAD_CONFIG_BORDER_ROUTING_MULTI_AIL_DETECTION_ENABLE + +RoutingManager::MultiAilDetector::MultiAilDetector(Instance &aInstance) + : InstanceLocator(aInstance) + , mDetected(false) + , mNetDataPeerBrCount(0) + , mRxRaTrackerPeerBrCount(0) + , mTimer(aInstance) +{ +} + +void RoutingManager::MultiAilDetector::Stop(void) +{ + mTimer.Stop(); + mDetected = false; + mNetDataPeerBrCount = 0; + mRxRaTrackerPeerBrCount = 0; +} + +void RoutingManager::MultiAilDetector::Evaluate(void) +{ + uint16_t count; + uint32_t minAge; + bool detected; + + VerifyOrExit(Get().IsRunning()); + + count = Get().mNetDataPeerBrTracker.CountPeerBrs(minAge); + + if (count != mNetDataPeerBrCount) + { + LogInfo("Peer BR count from netdata: %u -> %u", mNetDataPeerBrCount, count); + mNetDataPeerBrCount = count; + } + + count = Get().mRxRaTracker.CountPeerBrs(); + + if (count != mRxRaTrackerPeerBrCount) + { + LogInfo("Peer BR count from RaTracker: %u -> %u", mRxRaTrackerPeerBrCount, count); + mRxRaTrackerPeerBrCount = count; + } + + detected = (mNetDataPeerBrCount > mRxRaTrackerPeerBrCount); + + if (detected == mDetected) + { + mTimer.Stop(); + } + else if (!mTimer.IsRunning()) + { + mTimer.Start(detected ? kDetectTime : kClearTime); + } + +exit: + return; +} + +void RoutingManager::MultiAilDetector::HandleTimer(void) +{ + if (!mDetected) + { + LogNote("BRs on multi AIL detected - BRs are likely connected to different infra-links"); + LogInfo("More peer BRs in netdata vs from rx RAs for past %lu seconds", ToUlong(Time::MsecToSec(kDetectTime))); + LogInfo("NetData Peer BR count: %u, RaTracker Peer BR count: %u", mNetDataPeerBrCount, mRxRaTrackerPeerBrCount); + mDetected = true; + } + else + { + LogNote("BRs on multi AIL detection cleared"); + mDetected = false; + } + + mCallback.InvokeIfSet(mDetected); +} + +#endif // OPENTHREAD_CONFIG_BORDER_ROUTING_MULTI_AIL_DETECTION_ENABLE + //--------------------------------------------------------------------------------------------------------------------- // RxRaTracker @@ -2077,6 +2175,22 @@ exit: return error; } +#if OPENTHREAD_CONFIG_BORDER_ROUTING_TRACK_PEER_BR_INFO_ENABLE +uint16_t RoutingManager::RxRaTracker::CountPeerBrs(void) const +{ + uint16_t count = 0; + + for (const Router &router : mRouters) + { + if (!router.mIsLocalDevice && router.IsPeerBr()) + { + count++; + } + } + + return count; +} +#endif //--------------------------------------------------------------------------------------------------------------------- // RxRaTracker::Iterator diff --git a/src/core/border_router/routing_manager.hpp b/src/core/border_router/routing_manager.hpp index 698c6e1cb..e5c458a69 100644 --- a/src/core/border_router/routing_manager.hpp +++ b/src/core/border_router/routing_manager.hpp @@ -50,6 +50,11 @@ #error "TRACK_PEER_BR_INFO_ENABLE feature requires OPENTHREAD_CONFIG_BORDER_ROUTING_USE_HEAP_ENABLE" #endif +#if OPENTHREAD_CONFIG_BORDER_ROUTING_MULTI_AIL_DETECTION_ENABLE && \ + !OPENTHREAD_CONFIG_BORDER_ROUTING_TRACK_PEER_BR_INFO_ENABLE +#error "MULTI_AIL_DETECTION_ENABLE feature requires OPENTHREAD_CONFIG_BORDER_ROUTING_TRACK_PEER_BR_INFO_ENABLE" +#endif + #include #include #include @@ -110,6 +115,7 @@ public: typedef otBorderRoutingPeerBorderRouterEntry PeerBrEntry; ///< Peer Border Router Entry. typedef otPdProcessedRaInfo PdProcessedRaInfo; ///< Data of PdProcessedRaInfo. typedef otBorderRoutingRequestDhcp6PdCallback PdCallback; ///< DHCPv6 PD callback. + typedef otBorderRoutingMultiAilCallback MultiAilCallback; ///< Multi AIL detection callback. /** * This constant specifies the maximum number of route prefixes that may be published by `RoutingManager` @@ -572,6 +578,37 @@ public: #endif // OPENTHREAD_CONFIG_BORDER_ROUTING_TRACK_PEER_BR_INFO_ENABLE +#if OPENTHREAD_CONFIG_BORDER_ROUTING_MULTI_AIL_DETECTION_ENABLE + + /** + * Gets the current detected state regarding multiple Adjacent Infrastructure Links (AILs). + * + * It returns whether the Routing Manager currently believes that Border Routers (BRs) on the Thread mesh may be + * connected to different AILs. + * + * See `otBorderRoutingIsMultiAilDetected()` for more details about detection process. + * + * @retval TRUE Has detected that BRs are likely connected to multiple AILs. + * @retval FALSE Has not detected (or no longer detects) that BRs are connected to multiple AILs. + */ + bool IsMultiAilDetected(void) const { return mMultiAilDetector.IsDetected(); } + + /** + * Sets a callback function to be notified of changes in the multi-AIL detection state. + * + * Subsequent calls to this function will overwrite the previous callback setting. Using `NULL` for @p aCallback + * will disable the callback. + * + * @param[in] aCallback The callback function + * @param[in] aContext A pointer to application-specific context used with @p aCallback. + */ + void SetMultiAilCallback(MultiAilCallback aCallback, void *aContext) + { + mMultiAilDetector.SetCallback(aCallback, aContext); + } + +#endif // OPENTHREAD_CONFIG_BORDER_ROUTING_MULTI_AIL_DETECTION_ENABLE + #if OPENTHREAD_CONFIG_SRP_SERVER_ENABLE /** * Determines whether to enable/disable SRP server when the auto-enable mode is changed on SRP server. @@ -897,6 +934,56 @@ private: //- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +#if OPENTHREAD_CONFIG_BORDER_ROUTING_MULTI_AIL_DETECTION_ENABLE + + void HandleMultiAilDetectorTimer(void) { mMultiAilDetector.HandleTimer(); } + + class MultiAilDetector : public InstanceLocator + { + // Detects whether BRs may be connected to different AILs by + // tracking the number of peer BRs from netdata versus from + // `RxRaTracker`. If the netdata count exceeds the RA-tracked + // count for more than `kDetectTime` (10 minutes), it notifies + // this using the provided callback. To clear the state, + // `kClearTime` (1 minute) is used. + // + // This longer detection window of 10 minutes helps to avoid + // false positives due to transient changes. `RxRaTracker` uses + // 200 seconds for reachability checks of peer BRs. Stale + // Network Data entries are also expected to age out within a + // few minutes. So 10-minute detection time accommodates both. + + friend class RxRaTracker; + + public: + explicit MultiAilDetector(Instance &aInstance); + + void SetCallback(MultiAilCallback aCallback, void *aContext) { mCallback.Set(aCallback, aContext); } + bool IsDetected(void) const { return mDetected; } + + void Start(void) { Evaluate(); } + void Stop(void); + void Evaluate(void); + void HandleTimer(void); + + private: + static constexpr uint32_t kDetectTime = 10 * Time::kOneMinuteInMsec; + static constexpr uint32_t kClearTime = 1 * Time::kOneMinuteInMsec; + + using DetectCallback = Callback; + using DetectTimer = TimerMilliIn; + + bool mDetected; + uint16_t mNetDataPeerBrCount; + uint16_t mRxRaTrackerPeerBrCount; + DetectTimer mTimer; + DetectCallback mCallback; + }; + +#endif // OPENTHREAD_CONFIG_BORDER_ROUTING_MULTI_AIL_DETECTION_ENABLE + + //- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + void HandleRxRaTrackerSignalTask(void) { mRxRaTracker.HandleSignalTask(); } void HandleRxRaTrackerRdnssAddrTask(void) { mRxRaTracker.HandleRdnssAddrTask(); } void HandleRxRaTrackerExpirationTimer(void) { mRxRaTracker.HandleExpirationTimer(); } @@ -952,6 +1039,10 @@ private: Error GetNextRouter(PrefixTableIterator &aIterator, RouterEntry &aEntry) const; Error GetNextRdnssAddr(PrefixTableIterator &aIterator, RdnssAddrEntry &aEntry) const; +#if OPENTHREAD_CONFIG_BORDER_ROUTING_TRACK_PEER_BR_INFO_ENABLE + uint16_t CountPeerBrs(void) const; +#endif + // Callbacks notifying of changes void RemoveOrDeprecateOldEntries(TimeMilli aTimeThreshold); void HandleLocalOnLinkPrefixChanged(void); @@ -1706,6 +1797,10 @@ private: NetDataPeerBrTracker mNetDataPeerBrTracker; #endif +#if OPENTHREAD_CONFIG_BORDER_ROUTING_MULTI_AIL_DETECTION_ENABLE + MultiAilDetector mMultiAilDetector; +#endif + RxRaTracker mRxRaTracker; RoutePublisher mRoutePublisher; diff --git a/src/core/config/border_routing.h b/src/core/config/border_routing.h index 19508a16f..a8fb56dc4 100644 --- a/src/core/config/border_routing.h +++ b/src/core/config/border_routing.h @@ -83,6 +83,23 @@ #define OPENTHREAD_CONFIG_BORDER_ROUTING_TRACK_PEER_BR_INFO_ENABLE OPENTHREAD_CONFIG_BORDER_ROUTING_USE_HEAP_ENABLE #endif +/** + * @def OPENTHREAD_CONFIG_BORDER_ROUTING_MULTI_AIL_DETECTION_ENABLE + * + * Define to 1 to enable Routing Manager multiple Adjacent Infrastructure Links (AILs) detection feature. + * + * The detection mechanism operates as follows: The Routing Manager monitors the number of peer BRs listed in the + * Thread Network Data (see `otBorderRoutingCountPeerBrs()`) and compares this count with the number of peer BRs + * discovered by processing received Router Advertisement (RA) messages on its connected AIL. If the count derived from + * Network Data consistently exceeds the count derived from RAs for a detection duration of 10 minutes, it concludes + * that BRs are likely connected to different AILs. To clear state a shorter window of 1 minute is used. + * + * See `otBorderRoutingIsMultiAilDetected()` for more details. + */ +#ifndef OPENTHREAD_CONFIG_BORDER_ROUTING_MULTI_AIL_DETECTION_ENABLE +#define OPENTHREAD_CONFIG_BORDER_ROUTING_MULTI_AIL_DETECTION_ENABLE OPENTHREAD_CONFIG_BORDER_ROUTING_USE_HEAP_ENABLE +#endif + /** * @def OPENTHREAD_CONFIG_BORDER_ROUTING_REACHABILITY_CHECK_ICMP6_ERROR_ENABLE * diff --git a/tests/toranj/cli/cli.py b/tests/toranj/cli/cli.py index 4bd3ecaa8..7be26e2c7 100644 --- a/tests/toranj/cli/cli.py +++ b/tests/toranj/cli/cli.py @@ -851,6 +851,9 @@ class Node(object): def br_count_peers(self): return self._cli_single_output('br peers count') + def br_get_multiail(self): + return self._cli_single_output('br multiail') + #- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - # trel diff --git a/tests/toranj/cli/test-505-multi-ail-detection.py b/tests/toranj/cli/test-505-multi-ail-detection.py new file mode 100755 index 000000000..13323dcee --- /dev/null +++ b/tests/toranj/cli/test-505-multi-ail-detection.py @@ -0,0 +1,142 @@ +#!/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: +# +# Check multi AIL detection +# +# -+- _______ +# | / \ +# br1 --- br2 --- br3 +# + +test_name = __file__[:-3] if __file__.endswith('.py') else __file__ +print('-' * 120) +print('Starting \'{}\''.format(test_name)) + +# ----------------------------------------------------------------------------------------------------------------------- +# Creating `cli.Nodes` instances + +speedup = 60 +cli.Node.set_time_speedup_factor(speedup) + +one_minute = 60 + +br1 = cli.Node() +br2 = cli.Node() +br3 = cli.Node() + +IF_INDEX_1 = 1 +IF_INDEX_2 = 2 + +# ----------------------------------------------------------------------------------------------------------------------- +# Form topology + +br1.form("multiail") +br2.join(br1) +br3.join(br2) + +verify(br1.get_state() == 'leader') +verify(br2.get_state() == 'router') +verify(br3.get_state() == 'router') + +# ----------------------------------------------------------------------------------------------------------------------- +# Test implementation + +# Start first border router `br1` + +br1.srp_server_set_addr_mode('unicast') +br1.srp_server_auto_enable() + +br1.br_init(IF_INDEX_1, 1) +br1.br_enable() + +time.sleep(one_minute / speedup) +verify(br1.br_get_state() == 'running') +verify(br1.br_get_multiail() == 'not detected') + +# Start `br2` and `br3` together on a different AIL + +br2.br_init(IF_INDEX_2, 1) +br2.br_enable() + +br3.br_init(IF_INDEX_2, 1) +br3.br_enable() + +time.sleep(one_minute / speedup) +verify(br2.br_get_state() == 'running') +verify(br3.br_get_state() == 'running') + +verify(br2.br_get_multiail() == 'not detected') +verify(br3.br_get_multiail() == 'not detected') + +# wait for longer than 10 minutes and check that multi AIL is detected + +time.sleep(10.01 * one_minute / speedup) + +verify(br1.br_get_multiail() == 'detected') +verify(br2.br_get_multiail() == 'detected') +verify(br3.br_get_multiail() == 'detected') + +# Disable `br1` + +br1.br_disable() +verify(br1.br_get_state() == 'disabled') + +time.sleep(9 * one_minute / speedup) + +verify(br2.br_get_multiail() == 'not detected') +verify(br3.br_get_multiail() == 'not detected') + +# Enable `br` again, wait for a short time (before detection) and disable it + +br1.br_enable() + +time.sleep(2 * one_minute / speedup) +verify(br1.br_get_state() == 'running') + +br1.br_disable() +verify(br1.br_get_state() == 'disabled') + +time.sleep(10 * one_minute / speedup) + +verify(br2.br_get_multiail() == 'not detected') +verify(br3.br_get_multiail() == 'not detected') + +# ----------------------------------------------------------------------------------------------------------------------- +# 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 5a50b3f0a..75d8062ae 100755 --- a/tests/toranj/start.sh +++ b/tests/toranj/start.sh @@ -208,6 +208,7 @@ if [ "$TORANJ_CLI" = 1 ]; then run cli/test-502-multi-br-leader-failure-recovery.py run cli/test-503-peer-tbr-discovery.py run cli/test-504-br-icmp-unreach-err.py + run cli/test-505-multi-ail-detection.py run cli/test-601-channel-manager-channel-change.py # Skip the "channel-select" test on a TREL only radio link, since it # requires energy scan which is not supported in this case.