diff --git a/doc/ot_api_doc.h b/doc/ot_api_doc.h index 54252193b..8d134db3f 100644 --- a/doc/ot_api_doc.h +++ b/doc/ot_api_doc.h @@ -104,6 +104,7 @@ * * @defgroup api-backbone-router Backbone Router * @defgroup api-border-agent Border Agent + * @defgroup api-border-agent-tracker Border Agent Tracker * @defgroup api-border-router Border Router * @defgroup api-border-routing Border Routing Manager * @defgroup api-commissioner Commissioner diff --git a/etc/cmake/options.cmake b/etc/cmake/options.cmake index 48dbcce19..77be9cb34 100644 --- a/etc/cmake/options.cmake +++ b/etc/cmake/options.cmake @@ -177,6 +177,7 @@ ot_option(OT_BORDER_AGENT OPENTHREAD_CONFIG_BORDER_AGENT_ENABLE "border agent") ot_option(OT_BORDER_AGENT_EPSKC OPENTHREAD_CONFIG_BORDER_AGENT_EPHEMERAL_KEY_ENABLE "border agent ephemeral PSKc") ot_option(OT_BORDER_AGENT_ID OPENTHREAD_CONFIG_BORDER_AGENT_ID_ENABLE "create and save border agent ID") ot_option(OT_BORDER_AGENT_MESHCOP_SERVICE OPENTHREAD_CONFIG_BORDER_AGENT_MESHCOP_SERVICE_ENABLE "border agent meshcop service") +ot_option(OT_BORDER_AGENT_TRACKER OPENTHREAD_CONFIG_BORDER_AGENT_TRACKER_ENABLE "border agent tracker") ot_option(OT_BORDER_ROUTER OPENTHREAD_CONFIG_BORDER_ROUTER_ENABLE "border router") ot_option(OT_BORDER_ROUTING OPENTHREAD_CONFIG_BORDER_ROUTING_ENABLE "border routing") ot_option(OT_BORDER_ROUTING_DHCP6_PD OPENTHREAD_CONFIG_BORDER_ROUTING_DHCP6_PD_ENABLE "dhcpv6 pd support in border routing") diff --git a/include/openthread/BUILD.gn b/include/openthread/BUILD.gn index 973606fde..e8c96acce 100644 --- a/include/openthread/BUILD.gn +++ b/include/openthread/BUILD.gn @@ -43,6 +43,7 @@ source_set("openthread") { "backbone_router_ftd.h", "ble_secure.h", "border_agent.h", + "border_agent_tracker.h", "border_router.h", "border_routing.h", "channel_manager.h", diff --git a/include/openthread/border_agent_tracker.h b/include/openthread/border_agent_tracker.h new file mode 100644 index 000000000..1fe03a846 --- /dev/null +++ b/include/openthread/border_agent_tracker.h @@ -0,0 +1,162 @@ +/* + * 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. + */ + +/** + * @file + * @brief + * This file defines the OpenThread Border Agent Tracker APIs. + */ + +#ifndef OPENTHREAD_BORDER_AGENT_TRACKER_H_ +#define OPENTHREAD_BORDER_AGENT_TRACKER_H_ + +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * @addtogroup api-border-agent-tracker + * + * @brief + * This module includes APIs for the Border Agent Tracker. + * + * The Border Agent Tracker discovers and tracks Border Agents on the infrastructure link by browsing for the + * `_meshcop._udp` mDNS service. + * + * @{ + * + */ + +/** + * Represents an iterator to iterate through the discovered Border Agents. + * + * The fields in this struct are for OpenThread internal use only and MUST NOT be accessed or modified by the caller. + * + * An iterator MUST be initialized using `otBorderAgentTrackerInitIterator()` before it is used. + */ +typedef struct otBorderAgentTrackerIterator +{ + const void *mPtr; + uint64_t mData; +} otBorderAgentTrackerIterator; + +/** + * Represents information about a discovered Border Agent. + * + * To ensure consistent `mMsecSinceDiscovered` and `mMsecSinceLastChange` time calculations, the iterator's + * initialization time is stored within the iterator when `otBorderAgentTrackerInitIterator()` is called. The time + * values in this struct are calculated relative to the iterator's initialization time. + */ +typedef struct otBorderAgentTrackerAgentInfo +{ + const char *mServiceName; ///< The service name. + const char *mHostName; ///< The host name. May be NULL if not known yet. + uint16_t mPort; ///< The port number. Can be zero if not known yet. + const uint8_t *mTxtData; ///< The TXT data. May be NULL if not known yet. + uint16_t mTxtDataLength; ///< The TXT data length. + const otIp6Address *mAddresses; ///< Array of IPv6 addresses of the host. May be NULL if not known yet. + uint16_t mNumAddresses; ///< Number of addresses in the `mAddresses` array. + uint64_t mMsecSinceDiscovered; ///< Milliseconds since the service was discovered. + uint64_t mMsecSinceLastChange; ///< Milliseconds since the last change (port, TXT, or addresses). +} otBorderAgentTrackerAgentInfo; + +/** + * Enables or disables the Border Agent Tracker. + * + * Requires `OPENTHREAD_CONFIG_BORDER_AGENT_TRACKER_ENABLE`. + * + * When enabled, the tracker browses for the `_meshcop._udp` mDNS service to discover and track Border Agents on + * the infra-if network. + * + * @param[in] aInstance A pointer to an OpenThread instance. + * @param[in] aEnable TRUE to enable the Border Agent Tracker, FALSE to disable it. + */ +void otBorderAgentTrackerSetEnabled(otInstance *aInstance, bool aEnable); + +/** + * Indicates whether the Border Agent Tracker is running. + * + * Requires `OPENTHREAD_CONFIG_BORDER_AGENT_TRACKER_ENABLE`. + * + * The tracker can be enabled by the user (via `otBorderAgentTrackerSetEnabled()`) or by the OpenThread stack + * itself. The tracker is considered running if it is enabled by either entity AND the underlying DNS-SD (mDNS) + * is ready. This means that `otBorderAgentTrackerIsRunning()` may not return `TRUE` immediately after a call + * to `otBorderAgentTrackerSetEnabled(true)`. + * + * @param[in] aInstance A pointer to an OpenThread instance. + * + * @retval TRUE If the tracker is running. + * @retval FALSE If the tracker is not running. + */ +bool otBorderAgentTrackerIsRunning(otInstance *aInstance); + +/** + * Initializes a Border Agent Tracker iterator. + * + * Requires `OPENTHREAD_CONFIG_BORDER_AGENT_TRACKER_ENABLE`. + * + * An iterator MUST be initialized before being used. + * + * @param[in] aInstance A pointer to an OpenThread instance. + * @param[in] aIterator A pointer to the iterator to initialize. + */ +void otBorderAgentTrackerInitIterator(otInstance *aInstance, otBorderAgentTrackerIterator *aIterator); + +/** + * Gets the information for the next discovered Border Agent. + * + * Requires `OPENTHREAD_CONFIG_BORDER_AGENT_TRACKER_ENABLE`. + * + * The iterator initialization time is used to determine the `mMsecSinceDiscovered` and `mMsecSinceLastChange` in the + * `otBorderAgentTrackerAgentInfo`. + * + * @param[in] aInstance A pointer to an OpenThread instance. + * @param[in,out] aIterator A pointer to the iterator. An iterator MUST be initialized using + * `otBorderAgentTrackerInitIterator()` before it is used. + * @param[out] aAgentInfo A pointer to an `otBorderAgentTrackerAgentInfo` struct to populate. + * + * @retval OT_ERROR_NONE Successfully retrieved the information for the next agent. + * @retval OT_ERROR_NOT_FOUND No more agents were found. + */ +otError otBorderAgentTrackerGetNextAgent(otInstance *aInstance, + otBorderAgentTrackerIterator *aIterator, + otBorderAgentTrackerAgentInfo *aAgentInfo); + +/** + * @} + * + */ + +#ifdef __cplusplus +} // extern "C" +#endif + +#endif // OPENTHREAD_BORDER_AGENT_TRACKER_H_ diff --git a/include/openthread/instance.h b/include/openthread/instance.h index a5809ff89..18b7e2f81 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 (539) +#define OPENTHREAD_API_VERSION (540) /** * @addtogroup api-instance diff --git a/src/cli/README.md b/src/cli/README.md index cb12de08e..fe89ff8ef 100644 --- a/src/cli/README.md +++ b/src/cli/README.md @@ -23,6 +23,7 @@ Done - [attachtime](#attachtime) - [ba](#ba) +- [batracker](#batracker-enable) - [bbr](#bbr) - [br](README_BR.md) - [bufferinfo](#bufferinfo) @@ -609,6 +610,81 @@ mgmtPendingGet: 0 Done ``` +### batracker enable + +Enables Border Agent Tracker. + +Requires `OPENTHREAD_CONFIG_BORDER_AGENT_TRACKER_ENABLE`. + +When enabled, the tracker browses for the `_meshcop._udp` mDNS service to discover and track Border Agents on the infra-if network. + +```bash +> batracker enable +Done +``` + +### batracker disable + +Disables Border Agent Tracker. + +Requires `OPENTHREAD_CONFIG_BORDER_AGENT_TRACKER_ENABLE`. + +``` +> batracker disable +Done +``` + +### batracker state + +Requires `OPENTHREAD_CONFIG_BORDER_AGENT_TRACKER_ENABLE`. + +Shows the state of Border Agent Tracker, `running` or `inactive`. + +The tracker can be enabled by the user (e.g., via `batracker enable`) or by the OpenThread stack itself. The tracker is considered running if it is enabled by either entity and the underlying DNS-SD (mDNS) is ready. + +```bash +> batracker state +running +Done +``` + +### batracker agents + +Requires `OPENTHREAD_CONFIG_BORDER_AGENT_TRACKER_ENABLE`. + +Outputs the list of discovered Border Agents. Information per Agent: + +- Service name +- Port number +- Host name +- TXT data (key/value pairs per line) +- Host addresses +- Milliseconds since agent was first discovered +- Milliseconds since the last change to agent info (port, addresses, TXT data) + +```bash +> batracker agents +ServiceName: OTBR-by-Google-be345eefb12f7f9c + Port: 49152 + Host: otbe345eefb12f7f9c + TxtData: + id=4b21d3f4a431725048380698f3073a4b + rv=31 + nn=4f70656e546872656164 + xp=dead00beef00cafe + tv=312e342e30 + xa=be345eefb12f7f9c + sb=00000820 + dn=44656661756c74446f6d61696e + Address(es): + fe80:0:0:0:108f:3188:ff96:8e9f + fd7c:af54:fada:564d:7:fd6e:744c:e300 + fd7c:af54:fada:564d:d9:899d:1217:9e2 + MilliSecondsSinceDiscovered: 5237 + MilliSecondsSinceLastChange: 5237 +Done +``` + ### bufferinfo Show the current message buffer information. diff --git a/src/cli/cli.cpp b/src/cli/cli.cpp index 654789a75..c01994b4c 100644 --- a/src/cli/cli.cpp +++ b/src/cli/cli.cpp @@ -41,6 +41,7 @@ #include #include +#include #include #include #include @@ -807,6 +808,139 @@ void Interpreter::HandleBorderAgentEphemeralKeyStateChange(void) #endif // OPENTHREAD_CONFIG_BORDER_AGENT_ENABLE +#if OPENTHREAD_CONFIG_BORDER_AGENT_TRACKER_ENABLE + +template <> otError Interpreter::Process(Arg aArgs[]) +{ + otError error = OT_ERROR_NONE; + + /** + * @cli batracker (enable, disable) + * @code + * batracker enable + * Done + * @endcode + * @code + * batracker disable + * Done + * @endcode + * @cparam batracker @ca{enable|disable} + * @par api_copy + * #otBorderAgentTrackerSetEnabled + */ + if (ProcessEnableDisable(aArgs, otBorderAgentTrackerSetEnabled) == OT_ERROR_NONE) + { + } + /** + * @cli batracker state + * @code + * batracker state + * running + * Done + * @endcode + * @par + * Shows the state of Border Agent Tracker, `running` or `inactive`. + * + * The tracker can be enabled by the user (e.g., via `batracker enable`) or by the OpenThread stack itself. The + * tracker is considered running if it is enabled by either entity and the underlying DNS-SD (mDNS) is ready. + */ + else if (aArgs[0] == "state") + { + OutputLine("%s", otBorderAgentTrackerIsRunning(GetInstancePtr()) ? "running" : "inactive"); + } + /** + * @cli batracker agents + * @code + * batracker agents + * ServiceName: OTBR-by-Google-be345eefb12f7f9c + * Port: 49152 + * Host: otbe345eefb12f7f9c + * TxtData: + * id=4b21d3f4a431725048380698f3073a4b + * rv=31 + * nn=4f70656e546872656164 + * xp=dead00beef00cafe + * tv=312e342e30 + * xa=be345eefb12f7f9c + * sb=00000820 + * dn=44656661756c74446f6d61696e + * Address(es): + * fe80:0:0:0:108f:3188:ff96:8e9f + * fd7c:af54:fada:564d:7:fd6e:744c:e300 + * fd7c:af54:fada:564d:d9:899d:1217:9e2 + * MilliSecondsSinceDiscovered: 5237 + * MilliSecondsSinceLastChange: 5237 + * Done + * @endcode + * @par + * Outputs the list of discovered border agents. Information per agent: + * - Service name + * - Port number + * - Host name + * - TXT data (key/value pairs per line) + * - Host addresses + * - Milliseconds since agent was first discovered + * - Milliseconds since the last change to agent info (port, addresses, TXT data) + */ + else if (aArgs[0] == "agents") + { + otBorderAgentTrackerIterator iterator; + otBorderAgentTrackerAgentInfo agent; + + otBorderAgentTrackerInitIterator(GetInstancePtr(), &iterator); + + while (otBorderAgentTrackerGetNextAgent(GetInstancePtr(), &iterator, &agent) == OT_ERROR_NONE) + { + OutputLine("ServiceName: %s", agent.mServiceName); + OutputLine(kIndentSize, "Port: %u", agent.mPort); + OutputLine(kIndentSize, "Host: %s", agent.mHostName != nullptr ? agent.mHostName : "(null)"); + + OutputFormat(kIndentSize, "TxtData:"); + + if (agent.mTxtData != nullptr) + { + OutputNewLine(); + OutputDnsTxtData(kIndentSize * 2, agent.mTxtData, agent.mTxtDataLength); + } + else + { + OutputLine(" (null)"); + } + + OutputFormat(kIndentSize, "Address(es):"); + + if (agent.mAddresses != nullptr) + { + OutputNewLine(); + + for (uint16_t i = 0; i < agent.mNumAddresses; i++) + { + OutputSpaces(kIndentSize * 2); + OutputIp6AddressLine(agent.mAddresses[i]); + } + } + else + { + OutputLine(" (null)"); + } + + OutputFormat(kIndentSize, "MilliSecondsSinceDiscovered: "); + OutputUint64Line(agent.mMsecSinceDiscovered); + + OutputFormat(kIndentSize, "MilliSecondsSinceLastChange: "); + OutputUint64Line(agent.mMsecSinceLastChange); + } + } + else + { + error = OT_ERROR_INVALID_ARGS; + } + + return error; +} + +#endif // OPENTHREAD_CONFIG_BORDER_AGENT_TRACKER_ENABLE + #if OPENTHREAD_CONFIG_BORDER_ROUTING_ENABLE template <> otError Interpreter::Process(Arg aArgs[]) { return mBr.Process(aArgs); } #endif @@ -8433,6 +8567,9 @@ otError Interpreter::ProcessCommand(Arg aArgs[]) #if OPENTHREAD_CONFIG_BORDER_AGENT_ENABLE CmdEntry("ba"), #endif +#if OPENTHREAD_CONFIG_BORDER_AGENT_TRACKER_ENABLE + CmdEntry("batracker"), +#endif #if (OPENTHREAD_CONFIG_THREAD_VERSION >= OT_THREAD_VERSION_1_2) CmdEntry("bbr"), #endif diff --git a/src/cli/cli_utils.cpp b/src/cli/cli_utils.cpp index c2ee2c2dc..bed0198bd 100644 --- a/src/cli/cli_utils.cpp +++ b/src/cli/cli_utils.cpp @@ -218,6 +218,19 @@ void Utils::OutputSockAddrLine(const otSockAddr &aSockAddr) } void Utils::OutputDnsTxtData(const uint8_t *aTxtData, uint16_t aTxtDataLength) +{ + OutputDnsTxtData(/* aKeyValuePerLine */ false, 0, aTxtData, aTxtDataLength); +} + +void Utils::OutputDnsTxtData(uint8_t aIndentSize, const uint8_t *aTxtData, uint16_t aTxtDataLength) +{ + OutputDnsTxtData(/* aKeyValuePerLine */ true, aIndentSize, aTxtData, aTxtDataLength); +} + +void Utils::OutputDnsTxtData(bool aKeyValuePerLine, + uint8_t aIndentSize, + const uint8_t *aTxtData, + uint16_t aTxtDataLength) { otDnsTxtEntry entry; otDnsTxtEntryIterator iterator; @@ -225,11 +238,18 @@ void Utils::OutputDnsTxtData(const uint8_t *aTxtData, uint16_t aTxtDataLength) otDnsInitTxtEntryIterator(&iterator, aTxtData, aTxtDataLength); - OutputFormat("["); + if (!aKeyValuePerLine) + { + OutputFormat("["); + } while (otDnsGetNextTxtEntry(&iterator, &entry) == OT_ERROR_NONE) { - if (!isFirst) + if (aKeyValuePerLine) + { + OutputSpaces(aIndentSize); + } + else if (!isFirst) { OutputFormat(", "); } @@ -257,9 +277,17 @@ void Utils::OutputDnsTxtData(const uint8_t *aTxtData, uint16_t aTxtDataLength) } isFirst = false; + + if (aKeyValuePerLine) + { + OutputNewLine(); + } } - OutputFormat("]"); + if (!aKeyValuePerLine) + { + OutputFormat("]"); + } } const char *Utils::PercentageToString(uint16_t aValue, PercentageStringBuffer &aBuffer) diff --git a/src/cli/cli_utils.hpp b/src/cli/cli_utils.hpp index 7a3c90db5..7496de8a5 100644 --- a/src/cli/cli_utils.hpp +++ b/src/cli/cli_utils.hpp @@ -403,11 +403,24 @@ public: /** * Outputs DNS TXT data to the CLI console. * + * All key-value pairs are output on a single line in the format: "[key1=value1, key2=value2, ...]". + * * @param[in] aTxtData A pointer to a buffer containing the DNS TXT data. * @param[in] aTxtDataLength The length of @p aTxtData (in bytes). */ void OutputDnsTxtData(const uint8_t *aTxtData, uint16_t aTxtDataLength); + /** + * Outputs DNS TXT data to the CLI console, one key per line and applying an indentation. + * + * Each key-value pair is output on its own line, preceded by the specified indentation. + * + * @param[in] aIndentSize The number of space characters to prepend as indentation to each output line. + * @param[in] aTxtData A pointer to a buffer containing the DNS TXT data. + * @param[in] aTxtDataLength The length of @p aTxtData (in bytes). + */ + void OutputDnsTxtData(uint8_t aIndentSize, const uint8_t *aTxtData, uint16_t aTxtDataLength); + /** * Represents a buffer which is used when converting an encoded rate value to percentage string. */ @@ -719,6 +732,9 @@ private: void OutputTableHeader(uint8_t aNumColumns, const char *const aTitles[], const uint8_t aWidths[]); void OutputTableSeparator(uint8_t aNumColumns, const uint8_t aWidths[]); +#if OPENTHREAD_FTD || OPENTHREAD_MTD + void OutputDnsTxtData(bool aKeyValuePerLine, uint8_t aIndentSize, const uint8_t *aTxtData, uint16_t aTxtDataLength); +#endif otInstance *mInstance; OutputImplementer &mImplementer; diff --git a/src/core/BUILD.gn b/src/core/BUILD.gn index bb8ec3ef4..5e99aaa37 100644 --- a/src/core/BUILD.gn +++ b/src/core/BUILD.gn @@ -309,6 +309,7 @@ openthread_core_files = [ "api/backbone_router_ftd_api.cpp", "api/ble_secure_api.cpp", "api/border_agent_api.cpp", + "api/border_agent_tracker_api.cpp", "api/border_router_api.cpp", "api/border_routing_api.cpp", "api/channel_manager_api.cpp", @@ -523,6 +524,8 @@ openthread_core_files = [ "meshcop/announce_begin_client.hpp", "meshcop/border_agent.cpp", "meshcop/border_agent.hpp", + "meshcop/border_agent_tracker.cpp", + "meshcop/border_agent_tracker.hpp", "meshcop/commissioner.cpp", "meshcop/commissioner.hpp", "meshcop/dataset.cpp", diff --git a/src/core/CMakeLists.txt b/src/core/CMakeLists.txt index a5b75ad26..257f17de0 100644 --- a/src/core/CMakeLists.txt +++ b/src/core/CMakeLists.txt @@ -35,6 +35,7 @@ set(COMMON_SOURCES api/backbone_router_ftd_api.cpp api/ble_secure_api.cpp api/border_agent_api.cpp + api/border_agent_tracker_api.cpp api/border_router_api.cpp api/border_routing_api.cpp api/channel_manager_api.cpp @@ -156,6 +157,7 @@ set(COMMON_SOURCES mac/wakeup_tx_scheduler.cpp meshcop/announce_begin_client.cpp meshcop/border_agent.cpp + meshcop/border_agent_tracker.cpp meshcop/commissioner.cpp meshcop/dataset.cpp meshcop/dataset_manager.cpp diff --git a/src/core/api/border_agent_tracker_api.cpp b/src/core/api/border_agent_tracker_api.cpp new file mode 100644 index 000000000..6f90acc1b --- /dev/null +++ b/src/core/api/border_agent_tracker_api.cpp @@ -0,0 +1,66 @@ +/* + * 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. + */ + +#include "openthread-core-config.h" + +#if OPENTHREAD_CONFIG_BORDER_AGENT_TRACKER_ENABLE + +#include + +#include "common/as_core_type.hpp" +#include "instance/instance.hpp" + +using namespace ot; + +void otBorderAgentTrackerSetEnabled(otInstance *aInstance, bool aEnable) +{ + AsCoreType(aInstance).Get().SetEnabled(aEnable, + MeshCoP::BorderAgentTracker::kRequesterUser); +} + +bool otBorderAgentTrackerIsRunning(otInstance *aInstance) +{ + return AsCoreType(aInstance).Get().IsRunning(); +} + +void otBorderAgentTrackerInitIterator(otInstance *aInstance, otBorderAgentTrackerIterator *aIterator) +{ + AsCoreType(aIterator).Init(AsCoreType(aInstance)); +} + +otError otBorderAgentTrackerGetNextAgent(otInstance *aInstance, + otBorderAgentTrackerIterator *aIterator, + otBorderAgentTrackerAgentInfo *aAgentInfo) +{ + OT_UNUSED_VARIABLE(aInstance); + AssertPointerIsNotNull(aAgentInfo); + + return AsCoreType(aIterator).GetNextAgentInfo(*aAgentInfo); +} + +#endif // OPENTHREAD_CONFIG_BORDER_AGENT_TRACKER_ENABLE diff --git a/src/core/common/heap_string.hpp b/src/core/common/heap_string.hpp index d840e9c58..30ecda210 100644 --- a/src/core/common/heap_string.hpp +++ b/src/core/common/heap_string.hpp @@ -172,6 +172,16 @@ public: */ bool operator==(const String &aString) const { return (*this == aString.AsCString()); } + /** + * Overloads operator `!=` to evaluate whether or not two `String` are not equal. + * + * @param[in] aString The other string to compare with. + * + * @retval TRUE If the two strings are not equal. + * @retval FALSE If the two strings are equal. + */ + bool operator!=(const String &aString) const { return (*this != aString.AsCString()); } + String(const String &) = delete; String &operator=(const String &) = delete; diff --git a/src/core/config/border_agent.h b/src/core/config/border_agent.h index 1294657e2..556837156 100644 --- a/src/core/config/border_agent.h +++ b/src/core/config/border_agent.h @@ -122,6 +122,18 @@ #define OPENTHREAD_CONFIG_BORDER_AGENT_MESHCOP_SERVICE_BASE_NAME "OpenThread BR (unspecified vendor) " #endif +/** + * @def OPENTHREAD_CONFIG_BORDER_AGENT_TRACKER_ENABLE + * + * Define to 1 to enable the Border Agent Tracker feature. + * + * The Border Agent Tracker feature discovers and tracks Border Agents on the infrastructure network. This feature + * requires either `OPENTHREAD_CONFIG_PLATFORM_DNSSD_ENABLE` or `OPENTHREAD_CONFIG_MULTICAST_DNS_ENABLE` to be enabled. + */ +#ifndef OPENTHREAD_CONFIG_BORDER_AGENT_TRACKER_ENABLE +#define OPENTHREAD_CONFIG_BORDER_AGENT_TRACKER_ENABLE 0 +#endif + /** * @} */ diff --git a/src/core/instance/instance.cpp b/src/core/instance/instance.cpp index c3caa6a09..1a5eb9908 100644 --- a/src/core/instance/instance.cpp +++ b/src/core/instance/instance.cpp @@ -174,6 +174,9 @@ Instance::Instance(void) #if OPENTHREAD_CONFIG_BORDER_AGENT_ENABLE , mBorderAgent(*this) #endif +#if OPENTHREAD_CONFIG_BORDER_AGENT_TRACKER_ENABLE + , mBorderAgentTracker(*this) +#endif #if OPENTHREAD_CONFIG_COMMISSIONER_ENABLE && OPENTHREAD_FTD , mCommissioner(*this) #endif diff --git a/src/core/instance/instance.hpp b/src/core/instance/instance.hpp index 1d2bcb996..97aa03f37 100644 --- a/src/core/instance/instance.hpp +++ b/src/core/instance/instance.hpp @@ -84,6 +84,7 @@ #include "mac/mac.hpp" #include "mac/wakeup_tx_scheduler.hpp" #include "meshcop/border_agent.hpp" +#include "meshcop/border_agent_tracker.hpp" #include "meshcop/commissioner.hpp" #include "meshcop/dataset_manager.hpp" #include "meshcop/dataset_updater.hpp" @@ -589,6 +590,10 @@ private: MeshCoP::BorderAgent mBorderAgent; #endif +#if OPENTHREAD_CONFIG_BORDER_AGENT_TRACKER_ENABLE + MeshCoP::BorderAgentTracker mBorderAgentTracker; +#endif + #if OPENTHREAD_CONFIG_COMMISSIONER_ENABLE && OPENTHREAD_FTD MeshCoP::Commissioner mCommissioner; #endif @@ -1024,6 +1029,10 @@ template <> inline MeshCoP::DatasetUpdater &Instance::Get(void) { return mDatase template <> inline MeshCoP::BorderAgent &Instance::Get(void) { return mBorderAgent; } #endif +#if OPENTHREAD_CONFIG_BORDER_AGENT_TRACKER_ENABLE +template <> inline MeshCoP::BorderAgentTracker &Instance::Get(void) { return mBorderAgentTracker; } +#endif + #if OPENTHREAD_CONFIG_BORDER_AGENT_ENABLE && OPENTHREAD_CONFIG_BORDER_AGENT_EPHEMERAL_KEY_ENABLE template <> inline MeshCoP::BorderAgent::EphemeralKeyManager &Instance::Get(void) { diff --git a/src/core/meshcop/border_agent_tracker.cpp b/src/core/meshcop/border_agent_tracker.cpp new file mode 100644 index 000000000..fe4b30654 --- /dev/null +++ b/src/core/meshcop/border_agent_tracker.cpp @@ -0,0 +1,569 @@ +/* + * 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. + */ + +/** + * @file + * This file implements the Border Agent Tracker. + */ + +#include "border_agent_tracker.hpp" + +#if OPENTHREAD_CONFIG_BORDER_AGENT_TRACKER_ENABLE + +#include "instance/instance.hpp" + +namespace ot { +namespace MeshCoP { + +RegisterLogModule("BaTracker"); + +//--------------------------------------------------------------------------------------------------------------------- +// BorderAgentTracker + +const char BorderAgentTracker::kServiceType[] = "_meshcop._udp"; + +BorderAgentTracker::BorderAgentTracker(Instance &aInstance) + : InstanceLocator(aInstance) + , mState(kStateStopped) + , mUserEnabled(false) + , mStackEnabled(false) +{ +} + +void BorderAgentTracker::SetEnabled(bool aEnable, Requester aRequester) +{ + switch (aRequester) + { + case kRequesterUser: + mUserEnabled = aEnable; + break; + case kRequesterStack: + mStackEnabled = aEnable; + break; + } + + UpdateState(); +} + +void BorderAgentTracker::HandleDnssdPlatformStateChange(void) { UpdateState(); } + +void BorderAgentTracker::UpdateState(void) +{ + State newState; + + if (mUserEnabled || mStackEnabled) + { + newState = Get().IsReady() ? kStateRunning : kStatePendingDnssd; + } + else + { + newState = kStateStopped; + } + + VerifyOrExit(newState != mState); + + if (mState == kStateRunning) + { + Get().StopBrowser(Browser()); + mAgents.Free(); + } + + LogInfo("State: %s -> %s", StateToString(mState), StateToString(newState)); + mState = newState; + + // It is important to start browser after `mState` is updated. + // This ensures that if the `HandleBrowseResult()` callback is + // invoked immediately from within the `StartBrowser()` method + // call, the state is valid. + + if (newState == kStateRunning) + { + Get().StartBrowser(Browser()); + } + +exit: + return; +} + +void BorderAgentTracker::HandleBrowseResult(otInstance *aInstance, const otPlatDnssdBrowseResult *aResult) +{ + AsCoreType(aInstance).Get().HandleBrowseResult(*aResult); +} + +void BorderAgentTracker::HandleBrowseResult(const Dnssd::BrowseResult &aResult) +{ + Error error = kErrorNone; + Agent *newAgent; + + VerifyOrExit(IsRunning()); + + VerifyOrExit(aResult.mServiceInstance != nullptr); + + if (aResult.mTtl == 0) + { + mAgents.RemoveMatching(kMatchServiceName, aResult.mServiceInstance); + ExitNow(); + } + + VerifyOrExit(!mAgents.ContainsMatching(kMatchServiceName, aResult.mServiceInstance)); + + LogInfo("Discovered agent %s", aResult.mServiceInstance); + + newAgent = Agent::Allocate(GetInstance()); + VerifyOrExit(newAgent != nullptr, error = kErrorNoBufs); + + // We add the new agent to the list first before setting service + // name and starting the SRV and TXT resolvers. This ensures that + // if the `HandleSrvResult()` or `HandleTxtResult()` callbacks + // are invoked immediately from within the method call that start + // the resolvers, the agent entry can be correctly found in the + // list. + + mAgents.Push(*newAgent); + + if (newAgent->SetServiceNameAndStartSrvTxtResolvers(aResult.mServiceInstance) != kErrorNone) + { + // `Pop()` returns a temporary `OwnedPtr` which is + // immediately destroyed, freeing the allocated agent. + mAgents.Pop(); + error = kErrorNoBufs; + } + +exit: + LogOnError(error, "add new agent", aResult.mServiceInstance); +} + +void BorderAgentTracker::HandleSrvResult(otInstance *aInstance, const otPlatDnssdSrvResult *aResult) +{ + AsCoreType(aInstance).Get().HandleSrvResult(*aResult); +} + +void BorderAgentTracker::HandleSrvResult(const Dnssd::SrvResult &aResult) +{ + Agent *agent; + + VerifyOrExit(IsRunning()); + + agent = mAgents.FindMatching(kMatchServiceName, aResult.mServiceInstance); + VerifyOrExit(agent != nullptr); + + if (aResult.mTtl == 0) + { + agent->SetPort(0); + agent->ClearHost(); + ExitNow(); + } + + agent->SetPort(aResult.mPort); + + VerifyOrExit(aResult.mHostName != nullptr); + agent->SetHost(aResult.mHostName); + +exit: + return; +} + +void BorderAgentTracker::HandleTxtResult(otInstance *aInstance, const otPlatDnssdTxtResult *aResult) +{ + AsCoreType(aInstance).Get().HandleTxtResult(*aResult); +} + +void BorderAgentTracker::HandleTxtResult(const Dnssd::TxtResult &aResult) +{ + Agent *agent; + + VerifyOrExit(IsRunning()); + + agent = mAgents.FindMatching(kMatchServiceName, aResult.mServiceInstance); + VerifyOrExit(agent != nullptr); + + if ((aResult.mTtl == 0) || (aResult.mTxtData == nullptr)) + { + agent->ClearTxtData(); + } + else + { + agent->SetTxtData(aResult.mTxtData, aResult.mTxtDataLength); + } + +exit: + return; +} + +void BorderAgentTracker::HandleAddressResult(otInstance *aInstance, const otPlatDnssdAddressResult *aResult) +{ + AsCoreType(aInstance).Get().HandleAddressResult(*aResult); +} + +void BorderAgentTracker::HandleAddressResult(const Dnssd::AddressResult &aResult) +{ + Agent *agent; + + VerifyOrExit(IsRunning()); + + agent = mAgents.FindMatching(kMatchHostName, aResult.mHostName); + VerifyOrExit(agent != nullptr); + + agent->mHost->SetAddresses(aResult); + +exit: + return; +} + +bool BorderAgentTracker::NameMatch(const Heap::String &aHeapString, const char *aName) +{ + return !aHeapString.IsNull() && StringMatch(aHeapString.AsCString(), aName, kStringCaseInsensitiveMatch); +} + +#if OT_SHOULD_LOG_AT(OT_LOG_LEVEL_WARN) + +void BorderAgentTracker::LogOnError(Error aError, const char *aText, const char *aName) +{ + if (aError != kErrorNone) + { + LogWarn("Error %s - Failed to %s - %s", ErrorToString(aError), aText, (aName != nullptr) ? aName : ""); + } +} + +#else + +void BorderAgentTracker::LogOnError(Error, const char *, const char *) {} + +#endif + +const char *BorderAgentTracker::StateToString(State aState) +{ + static const char *const kStateStrings[] = { + "Stopped", + "PendingDnssd", + "Running", + }; + + struct EnumCheck + { + InitEnumValidatorCounter(); + ValidateNextEnum(kStateStopped); + ValidateNextEnum(kStatePendingDnssd); + ValidateNextEnum(kStateRunning); + }; + + return kStateStrings[aState]; +} + +//--------------------------------------------------------------------------------------------------------------------- +// BorderAgentTracker::Iterator + +void BorderAgentTracker::Iterator::Init(Instance &aInstance) +{ + SetAgentEntry(aInstance.Get().mAgents.GetHead()); + SetInitUptime(aInstance.Get().GetUptime()); +} + +Error BorderAgentTracker::Iterator::GetNextAgentInfo(AgentInfo &aInfo) +{ + Error error = kErrorNone; + const Agent *agent = GetAgentEntry(); + + VerifyOrExit(agent != nullptr, error = kErrorNotFound); + agent->CopyInfoTo(aInfo, GetInitUptime()); + SetAgentEntry(agent->GetNext()); + +exit: + return error; +} + +//--------------------------------------------------------------------------------------------------------------------- +// BorderAgentTracker::Host + +BorderAgentTracker::Host::Host(Instance &aInstance) + : InstanceLocator(aInstance) +{ +} + +BorderAgentTracker::Host::~Host(void) +{ + VerifyOrExit(mName != nullptr); + Get().StopIp6AddressResolver(AddressResolver(mName.AsCString())); + +exit: + return; +} + +Error BorderAgentTracker::Host::SetNameAndStartAddrResolver(const char *aHostName) +{ + Error error; + + SuccessOrExit(error = mName.Set(aHostName)); + Get().StartIp6AddressResolver(AddressResolver(mName.AsCString())); + +exit: + LogOnError(error, "set host name", aHostName); + return error; +} + +void BorderAgentTracker::Host::SetAddresses(const Dnssd::AddressResult &aResult) +{ + Error error = kErrorNone; + uint16_t length; + const Dnssd::AddressAndTtl *addrAndTtl; + + mAddresses.Free(); + + length = aResult.mAddressesLength; + + SuccessOrExit(error = mAddresses.ReserveCapacity(length)); + + for (addrAndTtl = aResult.mAddresses; length > 0; length--, addrAndTtl++) + { + const Ip6::Address &addr = AsCoreType(&addrAndTtl->mAddress); + + if (addrAndTtl->mTtl == 0) + { + continue; + } + + if (!mAddresses.Contains(addr)) + { + SuccessOrAssert(mAddresses.PushBack(addr)); + } + } + +exit: + LogOnError(error, "set host addresses", mName.AsCString()); +} + +//--------------------------------------------------------------------------------------------------------------------- +// BorderAgentTracker::Agent + +BorderAgentTracker::Agent::Agent(Instance &aInstance) + : InstanceLocator(aInstance) + , mNext(nullptr) + , mDiscoverUptime(aInstance.Get().GetUptime()) + , mLastUpdateUptime(mDiscoverUptime) + , mPort(0) +{ +} + +BorderAgentTracker::Agent::~Agent(void) +{ + VerifyOrExit(mServiceName != nullptr); + Get().StopSrvResolver(SrvResolver(mServiceName.AsCString())); + Get().StopTxtResolver(TxtResolver(mServiceName.AsCString())); + +exit: + return; +} + +Error BorderAgentTracker::Agent::SetServiceNameAndStartSrvTxtResolvers(const char *aServiceName) +{ + Error error; + + SuccessOrExit(error = mServiceName.Set(aServiceName)); + + SetUpdateTimeToNow(); + + Get().StartSrvResolver(SrvResolver(mServiceName.AsCString())); + Get().StartTxtResolver(TxtResolver(mServiceName.AsCString())); + +exit: + return error; +} + +void BorderAgentTracker::Agent::SetHost(const char *aHostName) +{ + Agent *matchingHostAgent; + + if (mHost != nullptr) + { + VerifyOrExit(!NameMatch(mHost->mName, aHostName)); + } + + SetUpdateTimeToNow(); + + // We handle the case where multiple meshcop services are + // advertised from the same host. While this is unlikely in + // actual deployments, it can be useful for testing. To minimize + // resource usage (memory and mDNS queries), we check if another + // `Agent` is already tracking the same host. If so, we share its + // `Host` entry. Otherwise, we allocate a new one. Note that + // `mHost` is defined as `RetainPtr` which does ref-counting. + + matchingHostAgent = Get().mAgents.FindMatching(kMatchHostName, aHostName); + + if (matchingHostAgent != nullptr) + { + mHost = matchingHostAgent->mHost; + } + else + { + mHost.Reset(Host::Allocate(GetInstance())); + VerifyOrExit(mHost != nullptr); + + if (mHost->SetNameAndStartAddrResolver(aHostName) != kErrorNone) + { + ClearHost(); + } + } + +exit: + return; +} + +void BorderAgentTracker::Agent::ClearHost(void) +{ + VerifyOrExit(mHost != nullptr); + + mHost.Reset(); + SetUpdateTimeToNow(); + +exit: + return; +} + +void BorderAgentTracker::Agent::SetPort(uint16_t aPort) +{ + VerifyOrExit(mPort != aPort); + + SetUpdateTimeToNow(); + mPort = aPort; + +exit: + return; +} + +void BorderAgentTracker::Agent::SetTxtData(const uint8_t *aData, uint16_t aDataLength) +{ + Error error = kErrorNone; + + VerifyOrExit(!mTxtData.Matches(aData, aDataLength)); + + SuccessOrExit(error = mTxtData.SetFrom(aData, aDataLength)); + SetUpdateTimeToNow(); + +exit: + LogOnError(error, "set TXT data", mServiceName.AsCString()); +} + +void BorderAgentTracker::Agent::ClearTxtData(void) +{ + VerifyOrExit(!mTxtData.IsNull()); + + SetUpdateTimeToNow(); + mTxtData.Free(); + +exit: + return; +} + +void BorderAgentTracker::Agent::SetUpdateTimeToNow(void) { mLastUpdateUptime = Get().GetUptime(); } + +bool BorderAgentTracker::Agent::Matches(MatchType aType, const char *aName) const +{ + bool matches = false; + + switch (aType) + { + case kMatchServiceName: + matches = NameMatch(mServiceName, aName); + break; + + case kMatchHostName: + VerifyOrExit(mHost != nullptr); + matches = NameMatch(mHost->mName, aName); + break; + } + +exit: + return matches; +} + +void BorderAgentTracker::Agent::CopyInfoTo(AgentInfo &aInfo, uint64_t aUptimeNow) const +{ + ClearAllBytes(aInfo); + + aInfo.mServiceName = mServiceName.AsCString(); + aInfo.mPort = mPort; + aInfo.mTxtData = mTxtData.GetBytes(); + aInfo.mTxtDataLength = mTxtData.GetLength(); + aInfo.mMsecSinceDiscovered = aUptimeNow - mDiscoverUptime; + aInfo.mMsecSinceLastChange = aUptimeNow - mLastUpdateUptime; + + if (mHost != nullptr) + { + aInfo.mHostName = mHost->mName.AsCString(); + aInfo.mAddresses = mHost->mAddresses.AsCArray(); + aInfo.mNumAddresses = mHost->mAddresses.GetLength(); + } +} + +//---------------------------------------------------------------------------------------------------------------------- +// BorderAgentTracker::Browser + +BorderAgentTracker::Browser::Browser(void) +{ + Clear(); + mServiceType = kServiceType; + mCallback = BorderAgentTracker::HandleBrowseResult; +} + +//---------------------------------------------------------------------------------------------------------------------- +// BorderAgentTracker::SrvResolver + +BorderAgentTracker::SrvResolver::SrvResolver(const char *aServiceName) +{ + Clear(); + mServiceInstance = aServiceName; + mServiceType = kServiceType; + mCallback = BorderAgentTracker::HandleSrvResult; +} + +//---------------------------------------------------------------------------------------------------------------------- +// BorderAgentTracker::TxtResolver + +BorderAgentTracker::TxtResolver::TxtResolver(const char *aServiceName) +{ + Clear(); + mServiceInstance = aServiceName; + mServiceType = kServiceType; + mCallback = BorderAgentTracker::HandleTxtResult; +} + +//---------------------------------------------------------------------------------------------------------------------- +// BorderAgentTracker::AddressResolver + +BorderAgentTracker::AddressResolver::AddressResolver(const char *aHostName) +{ + Clear(); + mHostName = aHostName; + mCallback = BorderAgentTracker::HandleAddressResult; +} + +} // namespace MeshCoP +} // namespace ot + +#endif // OPENTHREAD_CONFIG_BORDER_AGENT_TRACKER_ENABLE diff --git a/src/core/meshcop/border_agent_tracker.hpp b/src/core/meshcop/border_agent_tracker.hpp new file mode 100644 index 000000000..087f6e02b --- /dev/null +++ b/src/core/meshcop/border_agent_tracker.hpp @@ -0,0 +1,245 @@ +/* + * 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. + */ + +/** + * @file + * This file includes definitions for Border Agent Tracker. + */ + +#ifndef BORDER_AGENT_TRACKER_HPP_ +#define BORDER_AGENT_TRACKER_HPP_ + +#include "openthread-core-config.h" + +#if OPENTHREAD_CONFIG_BORDER_AGENT_TRACKER_ENABLE + +#if !OPENTHREAD_CONFIG_PLATFORM_DNSSD_ENABLE && !OPENTHREAD_CONFIG_MULTICAST_DNS_ENABLE +#error "BORDER_AGENT_TRACKER_ENABLE requires either the native mDNS or platform DNS-SD APIs" +#endif + +#include + +#include "common/as_core_type.hpp" +#include "common/heap_allocatable.hpp" +#include "common/heap_array.hpp" +#include "common/heap_data.hpp" +#include "common/heap_string.hpp" +#include "common/locator.hpp" +#include "common/owning_list.hpp" +#include "common/retain_ptr.hpp" +#include "net/dnssd.hpp" +#include "net/ip6_address.hpp" + +namespace ot { +namespace MeshCoP { + +/** + * Implements the Border Agent Tracker. + */ +class BorderAgentTracker : public InstanceLocator +{ + friend class ot::Dnssd; + + struct Agent; + +public: + typedef otBorderAgentTrackerAgentInfo AgentInfo; ///< Information about a discovered Border Agent. + + /** + * Represents an entity requesting to start or stop. + */ + enum Requester : uint8_t + { + kRequesterUser, ///< Requested by user (public OT API). + kRequesterStack, ///< Requested by stack itself (other OT modules). + }; + + class Iterator : public otBorderAgentTrackerIterator + { + public: + /** + * Initializes the iterator. + * + * An iterator MUST be initialized before being used. + * + * @param[in] aInstance The OpenThread instance. + */ + void Init(Instance &aInstance); + + /** + * Gets the information for the next discovered Border Agent. + * + * @param[out] aInfo A reference to an `AgentInfo` to populate with the agent's information. + * + * @retval kErrorNone Successfully retrieved the information for the next agent. + * @retval kErrorNotFound No more agents were found. + */ + Error GetNextAgentInfo(AgentInfo &aInfo); + + private: + const Agent *GetAgentEntry(void) const { return static_cast(mPtr); } + void SetAgentEntry(const Agent *aEntry) { mPtr = aEntry; } + uint64_t GetInitUptime(void) const { return mData; } + void SetInitUptime(uint64_t aUptime) { mData = aUptime; } + }; + + /** + * Initializes the Border Agent Tracker. + * + * @param[in] aInstance The OpenThread instance. + */ + explicit BorderAgentTracker(Instance &aInstance); + + /** + * Enables or disables the Border Agent Tracker. + * + * When enabled, the tracker browses for the `_meshcop._udp` mDNS service to discover and track Border Agents on + * the infra-if network. + * + * The Border Agent Tracker can be enabled by multiple requesters (see `Requester`). It remains enabled as long as + * at least one requester has it enabled. It is disabled only when all requesters have disabled it. + * + * @param[in] aEnable A boolean to enable/disable the Border Agent Tracker. + * @param[in] aRequester The entity requesting to enable/disable. + */ + void SetEnabled(bool aEnable, Requester aRequester); + + /** + * Indicates whether the tracker is running. + * + * The tracker is running if at least one requester (see `Requester`) has enabled it and the underlying DNS-SD + * platform is ready. + * + * @retval TRUE If the tracker is running. + * @retval FALSE If the tracker is not running. + */ + bool IsRunning(void) const { return mState == kStateRunning; } + +private: + enum State : uint8_t + { + kStateStopped, + kStatePendingDnssd, + kStateRunning, + }; + + enum MatchType : uint8_t + { + kMatchServiceName, + kMatchHostName, + }; + + struct Host : public InstanceLocator, public RetainCountable, public Heap::Allocatable + { + explicit Host(Instance &aInstance); + ~Host(void); + + Error SetNameAndStartAddrResolver(const char *aHostName); + void SetAddresses(const Dnssd::AddressResult &aResult); + + Heap::String mName; + Heap::Array mAddresses; + }; + + struct Agent : public InstanceLocator, public Heap::Allocatable, public LinkedListEntry + { + explicit Agent(Instance &aInstance); + ~Agent(void); + + Error SetServiceNameAndStartSrvTxtResolvers(const char *aServiceName); + void SetHost(const char *aHostName); + void ClearHost(void); + void SetPort(uint16_t aPort); + void SetTxtData(const uint8_t *aData, uint16_t aDataLength); + void ClearTxtData(void); + void SetUpdateTimeToNow(void); + bool Matches(MatchType aType, const char *aName) const; + void CopyInfoTo(AgentInfo &aInfo, uint64_t aUptimeNow) const; + + Agent *mNext; + Heap::String mServiceName; + RetainPtr mHost; + Heap::Data mTxtData; + uint64_t mDiscoverUptime; + uint64_t mLastUpdateUptime; + uint16_t mPort; + }; + + struct Browser : public Dnssd::Browser + { + Browser(void); + }; + + struct SrvResolver : public Dnssd::SrvResolver + { + explicit SrvResolver(const char *aServiceName); + }; + + struct TxtResolver : public Dnssd::TxtResolver + { + explicit TxtResolver(const char *aServiceName); + }; + + struct AddressResolver : public Dnssd::AddressResolver + { + explicit AddressResolver(const char *aHostName); + }; + + void UpdateState(void); + void HandleDnssdPlatformStateChange(void); + void HandleBrowseResult(const Dnssd::BrowseResult &aResult); + void HandleSrvResult(const Dnssd::SrvResult &aResult); + void HandleTxtResult(const Dnssd::TxtResult &aResult); + void HandleAddressResult(const Dnssd::AddressResult &aResult); + + static void HandleBrowseResult(otInstance *aInstance, const otPlatDnssdBrowseResult *aResult); + static void HandleSrvResult(otInstance *aInstance, const otPlatDnssdSrvResult *aResult); + static void HandleTxtResult(otInstance *aInstance, const otPlatDnssdTxtResult *aResult); + static void HandleAddressResult(otInstance *aInstance, const otPlatDnssdAddressResult *aResult); + + static bool NameMatch(const Heap::String &aHeapString, const char *aName); + static void LogOnError(Error aError, const char *aText, const char *aName); + static const char *StateToString(State aState); + + static const char kServiceType[]; + + State mState; + bool mUserEnabled : 1; + bool mStackEnabled : 1; + OwningList mAgents; +}; + +} // namespace MeshCoP + +DefineCoreType(otBorderAgentTrackerIterator, MeshCoP::BorderAgentTracker::Iterator); + +} // namespace ot + +#endif // OPENTHREAD_CONFIG_BORDER_AGENT_TRACKER_ENABLE + +#endif // BORDER_AGENT_TRACKER_HPP_ diff --git a/src/core/net/dnssd.cpp b/src/core/net/dnssd.cpp index e2445e8e0..cd698296f 100644 --- a/src/core/net/dnssd.cpp +++ b/src/core/net/dnssd.cpp @@ -531,6 +531,10 @@ void Dnssd::HandleStateChange(void) Get().HandleDnssdPlatformStateChange(); #endif +#if OPENTHREAD_CONFIG_BORDER_AGENT_TRACKER_ENABLE + Get().HandleDnssdPlatformStateChange(); +#endif + #if OPENTHREAD_CONFIG_RADIO_LINK_TREL_ENABLE && OPENTHREAD_CONFIG_TREL_MANAGE_DNSSD_ENABLE Get().HandleDnssdPlatformStateChange(); #endif diff --git a/tests/nexus/CMakeLists.txt b/tests/nexus/CMakeLists.txt index a5b3a6076..00cf83177 100644 --- a/tests/nexus/CMakeLists.txt +++ b/tests/nexus/CMakeLists.txt @@ -112,6 +112,7 @@ endmacro() #---------------------------------------------------------------------------------------------------------------------- ot_nexus_test(border_agent) +ot_nexus_test(border_agent_tracker) ot_nexus_test(dtls) ot_nexus_test(form_join) ot_nexus_test(full_network_reset) diff --git a/tests/nexus/openthread-core-nexus-config.h b/tests/nexus/openthread-core-nexus-config.h index 0ef01b35b..fd0d98506 100644 --- a/tests/nexus/openthread-core-nexus-config.h +++ b/tests/nexus/openthread-core-nexus-config.h @@ -46,6 +46,7 @@ #define OPENTHREAD_CONFIG_BORDER_AGENT_ENABLE 1 #define OPENTHREAD_CONFIG_BORDER_AGENT_EPHEMERAL_KEY_ENABLE 1 #define OPENTHREAD_CONFIG_BORDER_AGENT_ID_ENABLE 1 +#define OPENTHREAD_CONFIG_BORDER_AGENT_TRACKER_ENABLE 1 #define OPENTHREAD_CONFIG_BORDER_ROUTER_ENABLE 1 #define OPENTHREAD_CONFIG_BORDER_ROUTING_DHCP6_PD_ENABLE 1 #define OPENTHREAD_CONFIG_BORDER_ROUTING_DHCP6_PD_CLIENT_ENABLE 0 diff --git a/tests/nexus/test_border_agent_tracker.cpp b/tests/nexus/test_border_agent_tracker.cpp new file mode 100644 index 000000000..f22084262 --- /dev/null +++ b/tests/nexus/test_border_agent_tracker.cpp @@ -0,0 +1,271 @@ +/* + * 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. + */ + +#include +#include +#include + +#include "platform/nexus_core.hpp" +#include "platform/nexus_node.hpp" + +namespace ot { +namespace Nexus { + +void TestBorderAgentTracker(void) +{ + static constexpr uint32_t kInfraIfIndex = 1; + + Core nexus; + Node &node0 = nexus.CreateNode(); + Node &node1 = nexus.CreateNode(); + Node &node2 = nexus.CreateNode(); + Node &node3 = nexus.CreateNode(); + MeshCoP::BorderAgentTracker::Iterator iterator; + MeshCoP::BorderAgentTracker::AgentInfo agent; + Dns::Multicast::Core::Service service; + uint16_t count; + bool found; + + Log("------------------------------------------------------------------------------------------------------"); + Log("TestBorderAgentTracker"); + + SuccessOrQuit(node0.Get().SetEnabled(true, kInfraIfIndex)); + SuccessOrQuit(node1.Get().SetEnabled(true, kInfraIfIndex)); + SuccessOrQuit(node2.Get().SetEnabled(true, kInfraIfIndex)); + SuccessOrQuit(node3.Get().SetEnabled(true, kInfraIfIndex)); + + node0.Form(); + nexus.AdvanceTime(13 * 1000); + VerifyOrQuit(node0.Get().IsLeader()); + + node1.Join(node0); + node2.Join(node0); + node3.Form(); + nexus.AdvanceTime(600 * 1000); + + VerifyOrQuit(node1.Get().IsRouter()); + VerifyOrQuit(node2.Get().IsRouter()); + VerifyOrQuit(node3.Get().IsLeader()); + + // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + Log("Check Border Agent Tracker's initial state"); + + VerifyOrQuit(!node0.Get().IsRunning()); + + // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + Log("Enable Border Agent Tracker"); + + node0.Get().SetEnabled(true, MeshCoP::BorderAgentTracker::kRequesterUser); + nexus.AdvanceTime(10); + + VerifyOrQuit(node0.Get().IsRunning()); + + nexus.AdvanceTime(5000); + + // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + Log("Check the tracked agents"); + + iterator.Init(node0.GetInstance()); + count = 0; + + while (iterator.GetNextAgentInfo(agent) == kErrorNone) + { + count++; + Log("- %u) \"%s\", host:\"%s\", port:%u", count, agent.mServiceName, agent.mHostName, agent.mPort); + + VerifyOrQuit(agent.mHostName != nullptr); + VerifyOrQuit(agent.mPort != 0); + VerifyOrQuit(agent.mTxtData != nullptr); + VerifyOrQuit(agent.mAddresses != nullptr); + } + + VerifyOrQuit(count == 4); + + // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + Log("Disable BA function on node0, ensure that it is removed from the `BorderAgentTracker` list"); + + node0.Get().SetEnabled(false); + nexus.AdvanceTime(5000); + + iterator.Init(node0.GetInstance()); + count = 0; + + while (iterator.GetNextAgentInfo(agent) == kErrorNone) + { + count++; + Log("- %u) \"%s\", host:\"%s\", port:%u", count, agent.mServiceName, agent.mHostName, agent.mPort); + + VerifyOrQuit(agent.mHostName != nullptr); + VerifyOrQuit(agent.mPort != 0); + VerifyOrQuit(agent.mTxtData != nullptr); + VerifyOrQuit(agent.mAddresses != nullptr); + } + + VerifyOrQuit(count == 3); + + // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + Log("Re-enable BA function on node0, ensure that it is added again in the `BorderAgentTracker` list"); + + node0.Get().SetEnabled(true); + nexus.AdvanceTime(5000); + + iterator.Init(node0.GetInstance()); + count = 0; + + while (iterator.GetNextAgentInfo(agent) == kErrorNone) + { + count++; + Log("- %u) \"%s\", host:\"%s\", port:%u", count, agent.mServiceName, agent.mHostName, agent.mPort); + + VerifyOrQuit(agent.mHostName != nullptr); + VerifyOrQuit(agent.mPort != 0); + VerifyOrQuit(agent.mTxtData != nullptr); + VerifyOrQuit(agent.mAddresses != nullptr); + } + + VerifyOrQuit(count == 4); + + // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + Log("Disable Border Agent Tracker"); + + node0.Get().SetEnabled(false, MeshCoP::BorderAgentTracker::kRequesterUser); + nexus.AdvanceTime(10); + + VerifyOrQuit(!node0.Get().IsRunning()); + + iterator.Init(node0.GetInstance()); + VerifyOrQuit(iterator.GetNextAgentInfo(agent) == kErrorNotFound); + + // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + Log("Re-enable BA tracker and ensure all agents are discovered again"); + + node0.Get().SetEnabled(true, MeshCoP::BorderAgentTracker::kRequesterUser); + nexus.AdvanceTime(10); + + VerifyOrQuit(node0.Get().IsRunning()); + + nexus.AdvanceTime(5000); + + iterator.Init(node0.GetInstance()); + count = 0; + + while (iterator.GetNextAgentInfo(agent) == kErrorNone) + { + count++; + Log("- %u) \"%s\", host:\"%s\", port:%u", count, agent.mServiceName, agent.mHostName, agent.mPort); + + VerifyOrQuit(agent.mHostName != nullptr); + VerifyOrQuit(agent.mPort != 0); + VerifyOrQuit(agent.mTxtData != nullptr); + VerifyOrQuit(agent.mAddresses != nullptr); + } + + VerifyOrQuit(count == 4); + + // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + Log("Manually register a second `_meshcop._udp` service on node3"); + + ClearAllBytes(service); + service.mServiceInstance = "extra"; + service.mServiceType = "_meshcop._udp"; + service.mPort = 1234; + + SuccessOrQuit(node3.Get().RegisterService(service, /* aRequestId */ 0, nullptr)); + + nexus.AdvanceTime(5 * 1000); + + // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + Log("Validate that both agent services from node3 are discovered and tracked correctly"); + + iterator.Init(node0.GetInstance()); + count = 0; + found = false; + + while (iterator.GetNextAgentInfo(agent) == kErrorNone) + { + count++; + Log("- %u) \"%s\", host:\"%s\", port:%u", count, agent.mServiceName, agent.mHostName, agent.mPort); + + VerifyOrQuit(agent.mHostName != nullptr); + VerifyOrQuit(agent.mPort != 0); + VerifyOrQuit(agent.mTxtData != nullptr); + VerifyOrQuit(agent.mAddresses != nullptr); + + if (StringMatch(agent.mServiceName, service.mServiceInstance, kStringCaseInsensitiveMatch)) + { + VerifyOrQuit(agent.mPort == service.mPort); + found = true; + } + } + + VerifyOrQuit(count == 5); + VerifyOrQuit(found); + + // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + Log("Unregister the manually added second `_meshcop._udp` service"); + + SuccessOrQuit(node3.Get().UnregisterService(service)); + + nexus.AdvanceTime(5 * 1000); + + // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + Log("Validate that tracked agents is updated"); + + iterator.Init(node0.GetInstance()); + count = 0; + found = false; + + while (iterator.GetNextAgentInfo(agent) == kErrorNone) + { + count++; + Log("- %u) \"%s\", host:\"%s\", port:%u", count, agent.mServiceName, agent.mHostName, agent.mPort); + + VerifyOrQuit(agent.mHostName != nullptr); + VerifyOrQuit(agent.mPort != 0); + VerifyOrQuit(agent.mTxtData != nullptr); + VerifyOrQuit(agent.mAddresses != nullptr); + + if (StringMatch(agent.mServiceName, service.mServiceInstance, kStringCaseInsensitiveMatch)) + { + found = true; + } + } + + VerifyOrQuit(count == 4); + VerifyOrQuit(!found); +} + +} // namespace Nexus +} // namespace ot + +int main(void) +{ + ot::Nexus::TestBorderAgentTracker(); + printf("All tests passed\n"); + return 0; +} diff --git a/tests/toranj/openthread-core-toranj-config.h b/tests/toranj/openthread-core-toranj-config.h index b0e47898e..99ad01a6b 100644 --- a/tests/toranj/openthread-core-toranj-config.h +++ b/tests/toranj/openthread-core-toranj-config.h @@ -82,6 +82,8 @@ #define OPENTHREAD_CONFIG_BORDER_AGENT_ID_ENABLE 1 +#define OPENTHREAD_CONFIG_BORDER_AGENT_TRACKER_ENABLE 1 + #define OPENTHREAD_CONFIG_DIAG_ENABLE 1 #define OPENTHREAD_CONFIG_MLE_LINK_METRICS_SUBJECT_ENABLE 1