From 848918df326c8914fdd1665ef615ef68aefdf9c7 Mon Sep 17 00:00:00 2001 From: Abtin Keshavarzian Date: Wed, 4 Feb 2026 12:51:57 -0800 Subject: [PATCH] [joiner] introduce `Seeker` to handle discovery and candidate tracking (#12325) This change introduces a new `Seeker` class to encapsulate the logic for discovering and prioritizing Joiner Router candidates. The `Joiner` class is updated to use the `Seeker` to perform the discovery scan. The `Joiner` provides a callback `EvaluateScanResult` to the `Seeker` to filter and evaluate scan results based on Steering Data, preserving the existing behavior. This change separates the discovery mechanism from the `Joiner` state machine. This simplifies the `Joiner` implementation and facilitates future enhancements to the joining process. This commit makes to changes to how the Joiner Router candidates are tracked or prioritized. --- src/core/BUILD.gn | 2 + src/core/CMakeLists.txt | 1 + src/core/instance/instance.cpp | 1 + src/core/instance/instance.hpp | 4 + src/core/meshcop/joiner.cpp | 219 ++++++++++------------------- src/core/meshcop/joiner.hpp | 60 +++----- src/core/meshcop/seeker.cpp | 249 +++++++++++++++++++++++++++++++++ src/core/meshcop/seeker.hpp | 196 ++++++++++++++++++++++++++ 8 files changed, 548 insertions(+), 184 deletions(-) create mode 100644 src/core/meshcop/seeker.cpp create mode 100644 src/core/meshcop/seeker.hpp diff --git a/src/core/BUILD.gn b/src/core/BUILD.gn index d79953aaa..36cd2c487 100644 --- a/src/core/BUILD.gn +++ b/src/core/BUILD.gn @@ -586,6 +586,8 @@ openthread_core_files = [ "meshcop/panid_query_client.hpp", "meshcop/secure_transport.cpp", "meshcop/secure_transport.hpp", + "meshcop/seeker.cpp", + "meshcop/seeker.hpp", "meshcop/steering_data.cpp", "meshcop/steering_data.hpp", "meshcop/tcat_agent.cpp", diff --git a/src/core/CMakeLists.txt b/src/core/CMakeLists.txt index b5bf3f326..c53798fc8 100644 --- a/src/core/CMakeLists.txt +++ b/src/core/CMakeLists.txt @@ -182,6 +182,7 @@ set(COMMON_SOURCES meshcop/network_name.cpp meshcop/panid_query_client.cpp meshcop/secure_transport.cpp + meshcop/seeker.cpp meshcop/steering_data.cpp meshcop/tcat_agent.cpp meshcop/timestamp.cpp diff --git a/src/core/instance/instance.cpp b/src/core/instance/instance.cpp index 663d6ee3b..bd5678864 100644 --- a/src/core/instance/instance.cpp +++ b/src/core/instance/instance.cpp @@ -188,6 +188,7 @@ Instance::Instance(void) , mTmfSecureAgent(*this) #endif #if OPENTHREAD_CONFIG_JOINER_ENABLE + , mSeeker(*this) , mJoiner(*this) #endif #if OPENTHREAD_CONFIG_JAM_DETECTION_ENABLE diff --git a/src/core/instance/instance.hpp b/src/core/instance/instance.hpp index 322d5a3b9..ffc96e94e 100644 --- a/src/core/instance/instance.hpp +++ b/src/core/instance/instance.hpp @@ -99,6 +99,7 @@ #include "meshcop/joiner_router.hpp" #include "meshcop/meshcop_leader.hpp" #include "meshcop/network_identity.hpp" +#include "meshcop/seeker.hpp" #include "net/dhcp6_client.hpp" #include "net/dhcp6_server.hpp" #include "net/dhcp6_types.hpp" @@ -618,6 +619,7 @@ private: #endif #if OPENTHREAD_CONFIG_JOINER_ENABLE + MeshCoP::Seeker mSeeker; MeshCoP::Joiner mJoiner; #endif @@ -958,6 +960,8 @@ template <> inline Dnssd &Instance::Get(void) { return mDnssd; } #endif #if OPENTHREAD_CONFIG_JOINER_ENABLE +template <> inline MeshCoP::Seeker &Instance::Get(void) { return mSeeker; } + template <> inline MeshCoP::Joiner &Instance::Get(void) { return mJoiner; } #endif diff --git a/src/core/meshcop/joiner.cpp b/src/core/meshcop/joiner.cpp index 3f21e4eb7..9d48e546c 100644 --- a/src/core/meshcop/joiner.cpp +++ b/src/core/meshcop/joiner.cpp @@ -45,13 +45,11 @@ RegisterLogModule("Joiner"); Joiner::Joiner(Instance &aInstance) : InstanceLocator(aInstance) , mState(kStateIdle) - , mJoinerRouterIndex(0) , mFinalizeMessage(nullptr) , mTimer(aInstance) { SetIdFromIeeeEui64(); mDiscerner.Clear(); - ClearAllBytes(mJoinerRouters); } void Joiner::SetIdFromIeeeEui64(void) @@ -113,10 +111,9 @@ Error Joiner::Start(const char *aPskd, otJoinerCallback aCallback, void *aContext) { - Error error; - JoinerPskd joinerPskd; - Mac::ExtAddress randomAddress; - SteeringData::HashBitIndexes filterIndexes; + Error error; + bool shouldCleanup = false; + JoinerPskd joinerPskd; LogInfo("Joiner starting"); @@ -124,51 +121,36 @@ Error Joiner::Start(const char *aPskd, SuccessOrExit(error = Tlv::ValidateStringValue(aVendorName)); SuccessOrExit(error = Tlv::ValidateStringValue(aVendorModel)); SuccessOrExit(error = Tlv::ValidateStringValue(aVendorSwVersion)); - - VerifyOrExit(mState == kStateIdle, error = kErrorBusy); - VerifyOrExit(Get().IsUp() && Get().GetRole() == Mle::kRoleDisabled, - error = kErrorInvalidState); - SuccessOrExit(error = joinerPskd.SetFrom(aPskd)); - randomAddress.GenerateRandom(); - Get().SetExtAddress(randomAddress); - Get().UpdateLinkLocalAddress(); + VerifyOrExit(mState == kStateIdle, error = kErrorBusy); + VerifyOrExit(!Get().IsRunning(), error = kErrorBusy); SuccessOrExit(error = Get().Open(Ip6::NetifIdentifier::kNetifThreadInternal)); - SuccessOrExit(error = Get().Bind(kJoinerUdpPort)); + + // After this, if any of the steps fails, we need to cleanup + // (free allocated message, stop seeker, close agent, etc). + shouldCleanup = true; + + SuccessOrExit(error = Get().Bind(Seeker::kUdpPort)); Get().SetConnectCallback(HandleSecureCoapClientConnect, this); Get().SetPsk(joinerPskd); - for (JoinerRouter &router : mJoinerRouters) - { - router.mPriority = 0; // Priority zero means entry is not in-use. - } - SuccessOrExit(error = PrepareJoinerFinalizeMessage(aProvisioningUrl, aVendorName, aVendorModel, aVendorSwVersion, aVendorData)); - if (!mDiscerner.IsEmpty()) - { - SteeringData::CalculateHashBitIndexes(mDiscerner, filterIndexes); - } - else - { - SetIdFromIeeeEui64(); - SteeringData::CalculateHashBitIndexes(mId, filterIndexes); - } - - SuccessOrExit(error = Get().Discover(Mac::ChannelMask(0), Get().GetPanId(), - /* aJoiner */ true, /* aEnableFiltering */ true, - &filterIndexes, HandleDiscoverResult, this)); - mCompletionCallback.Set(aCallback, aContext); SetState(kStateDiscover); + error = Get().Start(EvaluateScanResult, this); + exit: - if (error != kErrorNone) + if ((error != kErrorNone) && shouldCleanup) { FreeJoinerFinalizeMessage(); + Get().Close(); + Get().Stop(); + SetState(kStateIdle); } LogWarnOnError(error, "start joiner"); @@ -196,13 +178,13 @@ void Joiner::Finish(Error aError) case kStateEntrust: case kStateJoined: Get().Disconnect(); - IgnoreError(Get().RemoveUnsecurePort(kJoinerUdpPort)); mTimer.Stop(); OT_FALL_THROUGH; case kStateDiscover: Get().Close(); + Get().Stop(); break; } @@ -215,114 +197,74 @@ exit: return; } -uint8_t Joiner::CalculatePriority(int8_t aRssi, bool aSteeringDataAllowsAny) +Seeker::Verdict Joiner::EvaluateScanResult(void *aContext, const Seeker::ScanResult *aResult) { - int16_t priority; + return static_cast(aContext)->EvaluateScanResult(aResult); +} - if (aRssi == Radio::kInvalidRssi) +Seeker::Verdict Joiner::EvaluateScanResult(const Seeker::ScanResult *aResult) +{ + Seeker::Verdict verdict = Seeker::kIgnore; + const SteeringData *steeringData; + + if (aResult == nullptr) { - aRssi = -127; + HandleScanCompleted(); + ExitNow(); } - priority = Clamp(aRssi, -127, -1); + steeringData = AsCoreTypePtr(&aResult->mSteeringData); - // Assign higher priority to networks with an exact match of Joiner - // ID in the Steering Data (128 < priority < 256) compared to ones - // that allow all Joiners (0 < priority < 128). Sub-prioritize - // based on signal strength. Priority 0 is reserved for unused - // entry. + // We prefer networks with an exact match of Joiner ID or + // Discerner in the Steering Data compared to ones that allow all + // Joiners. - priority += aSteeringDataAllowsAny ? 128 : 256; - - return static_cast(priority); -} - -void Joiner::HandleDiscoverResult(Mle::DiscoverScanner::ScanResult *aResult, void *aContext) -{ - static_cast(aContext)->HandleDiscoverResult(aResult); -} - -void Joiner::HandleDiscoverResult(Mle::DiscoverScanner::ScanResult *aResult) -{ - VerifyOrExit(mState == kStateDiscover); - - if (aResult != nullptr) + if (steeringData->PermitsAllJoiners()) { - SaveDiscoveredJoinerRouter(*aResult); + verdict = Seeker::kAccept; + ExitNow(); + } + + if (!mDiscerner.IsEmpty()) + { + VerifyOrExit(steeringData->Contains(mDiscerner)); } else { - Get().SetExtAddress(mId); - Get().UpdateLinkLocalAddress(); - - mJoinerRouterIndex = 0; - TryNextJoinerRouter(kErrorNone); + VerifyOrExit(steeringData->Contains(mId)); } + verdict = Seeker::kAcceptPreferred; + +exit: + return verdict; +} + +void Joiner::HandleScanCompleted(void) +{ + VerifyOrExit(mState == kStateDiscover); + + Get().SetExtAddress(mId); + Get().UpdateLinkLocalAddress(); + + TryNextCandidate(kErrorNone); + exit: return; } -void Joiner::SaveDiscoveredJoinerRouter(const Mle::DiscoverScanner::ScanResult &aResult) +void Joiner::TryNextCandidate(Error aPrevError) { - uint8_t priority; - bool doesAllowAny; - JoinerRouter *end; - JoinerRouter *entry; + Error error; - VerifyOrExit(aResult.mJoinerUdpPort > 0); - - doesAllowAny = AsCoreType(&aResult.mSteeringData).PermitsAllJoiners(); - - LogInfo("Joiner discover network: %s, pan:0x%04x, port:%d, chan:%d, rssi:%d, allow-any:%s", - AsCoreType(&aResult.mExtAddress).ToString().AsCString(), aResult.mPanId, aResult.mJoinerUdpPort, - aResult.mChannel, aResult.mRssi, ToYesNo(doesAllowAny)); - - priority = CalculatePriority(aResult.mRssi, doesAllowAny); - - // We keep the list sorted based on priority. Find the place to - // add the new result. - - end = GetArrayEnd(mJoinerRouters); - - for (entry = &mJoinerRouters[0]; entry < end; entry++) + do { - if (priority > entry->mPriority) + error = ConnectToNextCandidate(); + + if (error == kErrorNone) { - break; + ExitNow(); } - } - - VerifyOrExit(entry < end); - - // Shift elements in array to make room for the new one. - memmove(entry + 1, entry, - static_cast(reinterpret_cast(end - 1) - reinterpret_cast(entry))); - - entry->mExtAddr = AsCoreType(&aResult.mExtAddress); - entry->mPanId = aResult.mPanId; - entry->mJoinerUdpPort = aResult.mJoinerUdpPort; - entry->mChannel = aResult.mChannel; - entry->mPriority = priority; - -exit: - return; -} - -void Joiner::TryNextJoinerRouter(Error aPrevError) -{ - for (; mJoinerRouterIndex < GetArrayLength(mJoinerRouters); mJoinerRouterIndex++) - { - JoinerRouter &router = mJoinerRouters[mJoinerRouterIndex]; - Error error; - - if (router.mPriority == 0) - { - break; - } - - error = Connect(router); - VerifyOrExit(error != kErrorNone, mJoinerRouterIndex++); // Save the error from `Connect` only if there is no previous // error from earlier attempts. This ensures that if there has @@ -331,16 +273,9 @@ void Joiner::TryNextJoinerRouter(Error aPrevError) // emitted from `Finish()` call corresponds to the error from // that attempt. - if (aPrevError == kErrorNone) - { - aPrevError = error; - } - } + aPrevError = (aPrevError == kErrorNone) ? error : aPrevError; - if (aPrevError == kErrorNone) - { - aPrevError = kErrorNotFound; - } + } while (error != kErrorNotFound); Finish(aPrevError); @@ -348,26 +283,16 @@ exit: return; } -Error Joiner::Connect(JoinerRouter &aRouter) +Error Joiner::ConnectToNextCandidate(void) { - Error error = kErrorNotFound; - Ip6::SockAddr sockAddr(aRouter.mJoinerUdpPort); - - LogInfo("Joiner connecting to %s, pan:0x%04x, chan:%d", aRouter.mExtAddr.ToString().AsCString(), aRouter.mPanId, - aRouter.mChannel); - - Get().SetPanId(aRouter.mPanId); - SuccessOrExit(error = Get().SetPanChannel(aRouter.mChannel)); - SuccessOrExit(error = Get().AddUnsecurePort(kJoinerUdpPort)); - - sockAddr.GetAddress().SetToLinkLocalAddress(aRouter.mExtAddr); + Error error; + Ip6::SockAddr sockAddr; + SuccessOrExit(error = Get().SetUpNextConnection(sockAddr)); SuccessOrExit(error = Get().Connect(sockAddr)); - SetState(kStateConnect); exit: - LogWarnOnError(error, "start secure joiner connection"); return error; } @@ -388,7 +313,7 @@ void Joiner::HandleSecureCoapClientConnect(Dtls::Session::ConnectEvent aEvent) } else { - TryNextJoinerRouter(kErrorSecurity); + TryNextCandidate(kErrorSecurity); } exit: @@ -491,7 +416,7 @@ void Joiner::HandleJoinerFinalizeResponse(Coap::Msg *aMsg, Error aResult) exit: Get().Disconnect(); - IgnoreError(Get().RemoveUnsecurePort(kJoinerUdpPort)); + Get().Stop(); } template <> void Joiner::HandleTmf(Coap::Msg &aMsg) diff --git a/src/core/meshcop/joiner.hpp b/src/core/meshcop/joiner.hpp index 771b4946c..11535182a 100644 --- a/src/core/meshcop/joiner.hpp +++ b/src/core/meshcop/joiner.hpp @@ -52,7 +52,7 @@ #include "meshcop/meshcop.hpp" #include "meshcop/meshcop_tlvs.hpp" #include "meshcop/secure_transport.hpp" -#include "thread/discover_scanner.hpp" +#include "meshcop/seeker.hpp" #include "thread/tmf.hpp" namespace ot { @@ -135,7 +135,7 @@ public: const Mac::ExtAddress &GetId(void) const { return mId; } /** - * Gets the Jointer Discerner. + * Gets the Joiner Discerner. * * @returns A pointer to the current Joiner Discerner or `nullptr` if none is set. */ @@ -178,40 +178,28 @@ public: static const char *StateToString(State aState); private: - static constexpr uint16_t kMaxJoinerRouterCandidates = OPENTHREAD_CONFIG_JOINER_MAX_CANDIDATES; - static constexpr uint16_t kJoinerUdpPort = OPENTHREAD_CONFIG_JOINER_UDP_PORT; - static constexpr uint32_t kConfigExtAddressDelay = 100; // in msec. - static constexpr uint32_t kResponseTimeout = 4000; // in msec + static constexpr uint32_t kConfigExtAddressDelay = 100; // in msec. + static constexpr uint32_t kResponseTimeout = 4000; // in msec - struct JoinerRouter - { - Mac::ExtAddress mExtAddr; - Mac::PanId mPanId; - uint16_t mJoinerUdpPort; - uint8_t mChannel; - uint8_t mPriority; - }; - - void SetState(State aState); - void SetIdFromIeeeEui64(void); - void SaveDiscoveredJoinerRouter(const Mle::DiscoverScanner::ScanResult &aResult); - void TryNextJoinerRouter(Error aPrevError); - Error Connect(JoinerRouter &aRouter); - void Finish(Error aError); - void HandleTimer(void); - uint8_t CalculatePriority(int8_t aRssi, bool aSteeringDataAllowsAny); - Error PrepareJoinerFinalizeMessage(const char *aProvisioningUrl, - const char *aVendorName, - const char *aVendorModel, - const char *aVendorSwVersion, - const char *aVendorData); - void FreeJoinerFinalizeMessage(void); - void SendJoinerFinalize(void); - void SendJoinerEntrustResponse(const Coap::Msg &aMsg); - static void HandleDiscoverResult(Mle::DiscoverScanner::ScanResult *aResult, void *aContext); - void HandleDiscoverResult(Mle::DiscoverScanner::ScanResult *aResult); - static void HandleSecureCoapClientConnect(Dtls::Session::ConnectEvent aEvent, void *aContext); - void HandleSecureCoapClientConnect(Dtls::Session::ConnectEvent aEvent); + void SetState(State aState); + void SetIdFromIeeeEui64(void); + void TryNextCandidate(Error aPrevError); + Error ConnectToNextCandidate(void); + void Finish(Error aError); + void HandleTimer(void); + Error PrepareJoinerFinalizeMessage(const char *aProvisioningUrl, + const char *aVendorName, + const char *aVendorModel, + const char *aVendorSwVersion, + const char *aVendorData); + void FreeJoinerFinalizeMessage(void); + void SendJoinerFinalize(void); + void SendJoinerEntrustResponse(const Coap::Msg &aMsg); + static Seeker::Verdict EvaluateScanResult(void *aContext, const Seeker::ScanResult *aResult); + Seeker::Verdict EvaluateScanResult(const Seeker::ScanResult *aResult); + void HandleScanCompleted(void); + static void HandleSecureCoapClientConnect(Dtls::Session::ConnectEvent aEvent, void *aContext); + void HandleSecureCoapClientConnect(Dtls::Session::ConnectEvent aEvent); DeclareTmfResponseHandlerIn(Joiner, HandleJoinerFinalizeResponse); @@ -223,8 +211,6 @@ private: JoinerDiscerner mDiscerner; State mState; Callback mCompletionCallback; - JoinerRouter mJoinerRouters[kMaxJoinerRouterCandidates]; - uint16_t mJoinerRouterIndex; Coap::Message *mFinalizeMessage; JoinerTimer mTimer; }; diff --git a/src/core/meshcop/seeker.cpp b/src/core/meshcop/seeker.cpp new file mode 100644 index 000000000..333d1facc --- /dev/null +++ b/src/core/meshcop/seeker.cpp @@ -0,0 +1,249 @@ +/* + * 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. + */ + +/** + * @file + * This file implements the Seeker functionality. + */ + +#include "seeker.hpp" + +#if OPENTHREAD_CONFIG_JOINER_ENABLE + +#include "instance/instance.hpp" + +namespace ot { +namespace MeshCoP { + +RegisterLogModule("Seeker"); + +Seeker::Seeker(Instance &aInstance) + : InstanceLocator(aInstance) + , mState(kStateStopped) + , mCandidateIndex(0) +{ +} + +Error Seeker::Start(ScanEvaluator aScanEvaluator, void *aContext) +{ + Error error = kErrorNone; + Mac::ExtAddress randomAddress; + + VerifyOrExit(aScanEvaluator != nullptr, error = kErrorInvalidArgs); + + VerifyOrExit(GetState() == kStateStopped, error = kErrorBusy); + VerifyOrExit(Get().IsUp() && Get().IsDisabled(), error = kErrorInvalidState); + + randomAddress.GenerateRandom(); + Get().SetExtAddress(randomAddress); + Get().UpdateLinkLocalAddress(); + + mScanEvaluator.Set(aScanEvaluator, aContext); + + ClearAllBytes(mCandidates); + mCandidateIndex = 0; + + SuccessOrExit(error = + Get().Discover(Mac::ChannelMask(0), Get().GetPanId(), + /* aJoiner */ true, /* aEnableFiltering */ false, + /* aFilterIndexes */ nullptr, HandleDiscoverResult, this)); + SetState(kStateDiscovering); + +exit: + return error; +} + +void Seeker::Stop(void) +{ + switch (GetState()) + { + case kStateStopped: + case kStateDiscovering: + case kStateDiscoverDone: + break; + case kStateConnecting: + IgnoreError(Get().RemoveUnsecurePort(kUdpPort)); + break; + } + + SetState(kStateStopped); +} + +void Seeker::HandleDiscoverResult(ScanResult *aResult, void *aContext) +{ + static_cast(aContext)->HandleDiscoverResult(aResult); +} + +void Seeker::HandleDiscoverResult(ScanResult *aResult) +{ + bool preferred = false; + + VerifyOrExit(GetState() == kStateDiscovering); + + if (aResult == nullptr) + { + SetState(kStateDiscoverDone); + IgnoreReturnValue(mScanEvaluator.Invoke(aResult)); + ExitNow(); + } + + VerifyOrExit(aResult->mJoinerUdpPort > 0); + VerifyOrExit(AsCoreType(&aResult->mSteeringData).IsValid()); + + switch (mScanEvaluator.Invoke(aResult)) + { + case kAccept: + break; + case kAcceptPreferred: + preferred = true; + break; + + case kIgnore: + default: + ExitNow(); + } + + SaveCandidate(*aResult, preferred); + +exit: + return; +} + +void Seeker::SaveCandidate(const ScanResult &aResult, bool aPreferred) +{ + uint8_t priority; + Candidate *end; + Candidate *entry; + + LogInfo("Discovered: %s, pan:0x%04x, port:%u, chan:%u, rssi:%d, preferred:%s", + AsCoreType(&aResult.mExtAddress).ToString().AsCString(), aResult.mPanId, aResult.mJoinerUdpPort, + aResult.mChannel, aResult.mRssi, ToYesNo(aPreferred)); + + priority = CalculatePriority(aResult.mRssi, aPreferred); + + // We keep the list sorted based on priority. Find the place to + // add the new result. + + end = GetArrayEnd(mCandidates); + + for (entry = &mCandidates[0]; entry < end; entry++) + { + if (priority > entry->mPriority) + { + break; + } + } + + VerifyOrExit(entry < end); + + // Shift elements in array to make room for the new one. + memmove(entry + 1, entry, + static_cast(reinterpret_cast(end - 1) - reinterpret_cast(entry))); + + entry->mExtAddr = AsCoreType(&aResult.mExtAddress); + entry->mPanId = aResult.mPanId; + entry->mJoinerUdpPort = aResult.mJoinerUdpPort; + entry->mChannel = aResult.mChannel; + entry->mPriority = priority; + +exit: + return; +} + +uint8_t Seeker::CalculatePriority(int8_t aRssi, bool aPreferred) +{ + int16_t priority; + + if (aRssi == Radio::kInvalidRssi) + { + aRssi = -127; + } + + priority = Clamp(aRssi, -127, -1); + + // We assign a higher priority value to networks marked as + // preferred (128 < priority < 256) compared to normal + // (0 < priority < 128). Sub-prioritize based on signal + // strength. Priority 0 is reserved for unused entry. + + priority += aPreferred ? 256 : 128; + + return static_cast(priority); +} + +Error Seeker::SetUpNextConnection(Ip6::SockAddr &aSockAddr) +{ + Error error = kErrorNone; + const Candidate *candidate; + + switch (GetState()) + { + case kStateDiscoverDone: + case kStateConnecting: + break; + + case kStateStopped: + case kStateDiscovering: + ExitNow(error = kErrorInvalidState); + } + + candidate = &mCandidates[mCandidateIndex]; + + if (!candidate->IsValid()) + { + Stop(); + ExitNow(error = kErrorNotFound); + } + + mCandidateIndex++; + + LogInfo("Setting up conn to %s, pan:0x%04x, chan:%u", candidate->mExtAddr.ToString().AsCString(), candidate->mPanId, + candidate->mChannel); + + Get().SetPanId(candidate->mPanId); + SuccessOrExit(error = Get().SetPanChannel(candidate->mChannel)); + + if (!Get().IsUnsecurePort(kUdpPort)) + { + SuccessOrExit(error = Get().AddUnsecurePort(kUdpPort)); + } + + SetState(kStateConnecting); + + aSockAddr.Clear(); + aSockAddr.SetPort(candidate->mJoinerUdpPort); + aSockAddr.GetAddress().SetToLinkLocalAddress(candidate->mExtAddr); + +exit: + return error; +} + +} // namespace MeshCoP +} // namespace ot + +#endif // OPENTHREAD_CONFIG_JOINER_ENABLE diff --git a/src/core/meshcop/seeker.hpp b/src/core/meshcop/seeker.hpp new file mode 100644 index 000000000..a0b5aabf4 --- /dev/null +++ b/src/core/meshcop/seeker.hpp @@ -0,0 +1,196 @@ +/* + * 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. + */ + +/** + * @file + * This file includes definitions for the Seeker module. + * + * The Seeker is responsible for discovering nearby Joiner Router candidates, prioritizing them, and + * iterating through the list to select the next best candidate for connection. It acts as a + * sub-module of the `Joiner` class. + */ + +#ifndef OT_CORE_MESHCOP_SEEKER_HPP_ +#define OT_CORE_MESHCOP_SEEKER_HPP_ + +#include "openthread-core-config.h" + +#if OPENTHREAD_CONFIG_JOINER_ENABLE + +#include "common/callback.hpp" +#include "common/error.hpp" +#include "common/locator.hpp" +#include "common/non_copyable.hpp" +#include "mac/mac_types.hpp" +#include "net/socket.hpp" +#include "thread/discover_scanner.hpp" + +namespace ot { +namespace MeshCoP { + +/** + * Represents a MeshCoP Seeker. + */ +class Seeker : public InstanceLocator, private NonCopyable +{ +public: + static constexpr uint16_t kUdpPort = OPENTHREAD_CONFIG_JOINER_UDP_PORT; ///< The default Joiner UDP port. + + typedef Mle::DiscoverScanner::ScanResult ScanResult; ///< Discover Scan result. + + /** + * Represents a verdict returned from `ScanEvaluator` when evaluating a Discover Scan result. + */ + enum Verdict : uint8_t + { + kAccept, ///< The scan result is acceptable. + kAcceptPreferred, ///< The scan result is acceptable and preferred. + kIgnore, ///< The scan result should be ignored. + }; + + /** + * Represents the callback function type used to evaluate a scan result or report the end of a scan. + * + * @param[in] aContext A pointer to the callback context. + * @param[in] aResult A pointer to the scan result to evaluate, or `nullptr` to indicate scan completion. + * + * @returns The verdict for the scan result (`kAccept`, `kAcceptPreferred`, or `kIgnore`). + * If @p aResult is `nullptr` (scan complete), the return value is ignored. + */ + typedef Verdict (*ScanEvaluator)(void *aContext, const ScanResult *aResult); + + /** + * Initializes the `Seeker` + * + * @param[in] aInstance The OpenThread instance. + */ + explicit Seeker(Instance &aInstance); + + /** + * Starts the Seeker operation. + * + * The Seeker generates and sets a random MAC address for anonymity, then initiates an MLE Discover Scan to find + * Joiner Router candidates. + * + * Found candidates are reported to the @p aScanEvaluator callback. Based on the returned `Verdict`, the Seeker + * maintains a prioritized list of candidates for future connection attempts. + * + * @param[in] aScanEvaluator The callback function to evaluate scan results. + * @param[in] aContext An arbitrary context pointer to use with @p aScanEvaluator. + * + * @retval kErrorNone Successfully started the Seeker. + * @retval kErrorBusy The Seeker is already active (scanning or connecting). + * @retval kErrorInvalidState The IPv6 interface is not enabled, or MLE is enabled. + */ + Error Start(ScanEvaluator aScanEvaluator, void *aContext); + + /** + * Stops the Seeker operation. + * + * This method stops any ongoing discovery or connection process, unregisters the unsecure Joiner UDP port, and + * clears internal state. If the Seeker is already stopped, this method has no effect. + * + * If the join process succeeds after a call to `SetupNextConnection()`, the caller MUST call this method to stop + * the Seeker and, importantly, unregister the Joiner UDP port. + * + * Note: If `SetupNextConnection()` returns `kErrorNotFound` (indicating the candidate list is exhausted), the + * Seeker stops automatically. + */ + void Stop(void); + + /** + * Indicates whether or not the Seeker is running. + * + * @retval TRUE The seeker is active and running. + * @retval FALSE The seeker is stopped. + */ + bool IsRunning(void) const { return GetState() != kStateStopped; } + + /** + * Selects the next best candidate and prepares the connection. + * + * This method must be called after the discovery scan has completed (indicated by the `ScanEvaluator` callback + * receiving `nullptr`). Calling it before scan completion will result in `kErrorInvalidState`. + * + * This method iterates through the discovered Joiner Router candidates in order of priority. For the selected + * candidate, it configures the radio channel and PAN ID, and populates @p aSockAddr with the candidate's address. + * It also registers the Joiner UDP port `kUdpPort` as an unsecure port to allow UDP + * connection to the candidate. + * + * If the list is exhausted, this method returns `kErrorNotFound` and automatically calls `Stop()`, which removes + * the unsecure port and clears internal state. + * + * @param[out] aSockAddr A reference to a socket address to output the candidate's address. + * + * @retval kErrorNone Successfully set up the connection to the next candidate. + * @retval kErrorNotFound No more candidates are available (list exhausted). + * @retval kErrorInvalidState The Seeker is not in a valid state (e.g. scan not yet completed). + */ + Error SetUpNextConnection(Ip6::SockAddr &aSockAddr); + +private: + static constexpr uint16_t kMaxCandidates = OPENTHREAD_CONFIG_JOINER_MAX_CANDIDATES; + + enum State : uint8_t + { + kStateStopped, + kStateDiscovering, + kStateDiscoverDone, + kStateConnecting, + }; + + struct Candidate + { + bool IsValid(void) const { return mPriority != 0; } + + Mac::ExtAddress mExtAddr; + Mac::PanId mPanId; + uint16_t mJoinerUdpPort; + uint8_t mChannel; + uint8_t mPriority; + }; + + State GetState(void) const { return mState; } + void SetState(State aState) { mState = aState; } + static void HandleDiscoverResult(ScanResult *aResult, void *aContext); + void HandleDiscoverResult(ScanResult *aResult); + void SaveCandidate(const ScanResult &aResult, bool aPreferred); + static uint8_t CalculatePriority(int8_t aRssi, bool aPreferred); + + State mState; + Callback mScanEvaluator; + Candidate mCandidates[kMaxCandidates]; + uint16_t mCandidateIndex; +}; + +} // namespace MeshCoP +} // namespace ot + +#endif // OPENTHREAD_CONFIG_JOINER_ENABLE + +#endif // OT_CORE_MESHCOP_SEEKER_HPP_