diff --git a/.github/workflows/toranj.yml b/.github/workflows/toranj.yml index a0c7dd370..e553ed2ff 100644 --- a/.github/workflows/toranj.yml +++ b/.github/workflows/toranj.yml @@ -212,6 +212,9 @@ jobs: run: | ./tests/nexus/build.sh ninja test + git clean -dfx + ./tests/nexus/build.sh trel + ./tests/nexus/nexus_trel upload-coverage: needs: diff --git a/etc/cmake/options.cmake b/etc/cmake/options.cmake index bffda1fa4..3d982e6bb 100644 --- a/etc/cmake/options.cmake +++ b/etc/cmake/options.cmake @@ -255,6 +255,7 @@ ot_option(OT_SRP_SERVER_FAST_START_MDOE OPENTHREAD_CONFIG_SRP_SERVER_FAST_START_ ot_option(OT_TCP OPENTHREAD_CONFIG_TCP_ENABLE "TCP") ot_option(OT_TIME_SYNC OPENTHREAD_CONFIG_TIME_SYNC_ENABLE "time synchronization service") ot_option(OT_TREL OPENTHREAD_CONFIG_RADIO_LINK_TREL_ENABLE "TREL radio link for Thread over Infrastructure feature") +ot_option(OT_TREL_MANAGE_DNSSD OPENTHREAD_CONFIG_TREL_MANAGE_DNSSD_ENABLE "TREL to manage DNSSD and peer discovery") ot_option(OT_TX_BEACON_PAYLOAD OPENTHREAD_CONFIG_MAC_OUTGOING_BEACON_PAYLOAD_ENABLE "tx beacon payload") ot_option(OT_TX_QUEUE_STATS OPENTHREAD_CONFIG_TX_QUEUE_STATISTICS_ENABLE "tx queue statistics") ot_option(OT_UDP_FORWARD OPENTHREAD_CONFIG_UDP_FORWARD_ENABLE "UDP forward") diff --git a/include/openthread/instance.h b/include/openthread/instance.h index 8ad6355e0..ea90d556d 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 (509) +#define OPENTHREAD_API_VERSION (510) /** * @addtogroup api-instance diff --git a/include/openthread/platform/trel.h b/include/openthread/platform/trel.h index 8c9ff64ef..8401b0d8f 100644 --- a/include/openthread/platform/trel.h +++ b/include/openthread/platform/trel.h @@ -65,18 +65,19 @@ extern "C" { * The socket is also bound to network interface(s) on which TREL is to be supported. The socket and the chosen port * should stay valid while TREL is enabled. * - * 2) Platform layer MUST initiate an ongoing DNS-SD browse on the service name "_trel._udp" within the local browsing - * domain to discover other devices supporting TREL. The ongoing browse will produce two different types of events: - * "add" events and "remove" events. When the browse is started, it should produce an "add" event for every TREL peer - * currently present on the network. Whenever a TREL peer goes offline, a "remove" event should be produced. "remove" - * events are not guaranteed, however. When a TREL service instance is discovered, a new ongoing DNS-SD query for an - * AAAA record should be started on the hostname indicated in the SRV record of the discovered instance. If multiple - * host IPv6 addressees are discovered for a peer, one with highest scope among all addresses MUST be reported (if - * there are multiple address at same scope, one must be selected randomly). - * - * TREL platform MUST signal back the discovered peer info using `otPlatTrelHandleDiscoveredPeerInfo()` callback. This - * callback MUST be invoked when a new peer is discovered, when there is a change in an existing entry (e.g., new - * TXT record or new port number or new IPv6 address), or when the peer is removed. + * 2) If `OPENTHREAD_CONFIG_TREL_MANAGE_DNSSD_ENABLE` is enabled, the OpenThread core TREL implementation itself will + * handle mDNS (DNS-SD) TREL service registration and peer discovery. Otherwise the platform layer MUST initiate an + * ongoing DNS-SD browse on the service name "_trel._udp" within the local browsing domain to discover other devices + * supporting TREL. The ongoing browse will produce two different types of events: "add" events and "remove" events. + * When the browse is started, it should produce an "add" event for every TREL peer currently present on the network. + * Whenever a TREL peer goes offline, a "remove" event should be produced. "remove" events are not guaranteed, however. + * When a TREL service instance is discovered, a new ongoing DNS-SD query for an AAAA record should be started on the + * hostname indicated in the SRV record of the discovered instance. If multiple host IPv6 addressees are discovered for + * a peer, one with highest scope among all addresses MUST be reported (if there are multiple address at same scope, + * one must be selected randomly). TREL platform MUST signal back the discovered peer info using + * `otPlatTrelHandleDiscoveredPeerInfo()` callback. This callback MUST be invoked when a new peer is discovered, when + * there is a change in an existing entry (e.g., new TXT record or new port number or new IPv6 address), or when the + * peer is removed. * * @param[in] aInstance The OpenThread instance. * @param[out] aUdpPort A pointer to return the selected port number by platform layer. @@ -127,6 +128,8 @@ typedef struct otPlatTrelPeerInfo /** * This is a callback function from platform layer to report a discovered TREL peer info. * + * This is only applicable when `OPENTHREAD_CONFIG_TREL_MANAGE_DNSSD_ENABLE` is disabled. + * * @note The @p aInfo structure and its content (e.g., the `mTxtData` buffer) does not need to persist after returning * from this call. OpenThread code will make a copy of all the info it needs. * @@ -139,6 +142,8 @@ extern void otPlatTrelHandleDiscoveredPeerInfo(otInstance *aInstance, const otPl * Notifies platform that a TREL packet is received from a peer using a different socket address than the one reported * earlier from `otPlatTrelHandleDiscoveredPeerInfo()`. * + * This is only applicable when `OPENTHREAD_CONFIG_TREL_MANAGE_DNSSD_ENABLE` is disabled. + * * Ideally the platform underlying DNS-SD should detect changes to advertised port and addresses by peers, however, * there are situations where this is not detected reliably. This function signals to the platform layer than we * received a packet from a peer with it using a different port or address. This can be used by the playroom layer to @@ -155,6 +160,8 @@ void otPlatTrelNotifyPeerSocketAddressDifference(otInstance *aInstance, /** * Registers a new service to be advertised using DNS-SD [RFC6763]. * + * This is only applicable when `OPENTHREAD_CONFIG_TREL_MANAGE_DNSSD_ENABLE` is disabled. + * * The service name is "_trel._udp". The platform should use its own hostname, which when combined with the service * name and the local DNS-SD domain name will produce the full service instance name, for example * "example-host._trel._udp.local.". diff --git a/src/core/config/trel.h b/src/core/config/trel.h index dbd4dea25..f7b9d2a7c 100644 --- a/src/core/config/trel.h +++ b/src/core/config/trel.h @@ -43,13 +43,26 @@ * @{ */ +/** + * @def OPENTHREAD_CONFIG_TREL_MANAGE_DNSSD_ENABLE + * + * Define as 1 to have the OpenThread core TREL implementation directly manage mDNS (DNS-SD) service registration + * and peer discovery (browse and service/address resolution of TREL service for TREL peer discovery). + * + * When this feature is disabled, the mDNS (DNS-SD) functions are delegated to the platform layer. More details are + * provided in the `platform/trel.h` API documentation. + */ +#ifndef OPENTHREAD_CONFIG_TREL_MANAGE_DNSSD_ENABLE +#define OPENTHREAD_CONFIG_TREL_MANAGE_DNSSD_ENABLE 0 +#endif + /** * @def OPENTHREAD_CONFIG_TREL_USE_HEAP_ENABLE * * Define as 1 to allow TREL modules to use heap allocated objects (e.g. for the TREL peer table). */ #ifndef OPENTHREAD_CONFIG_TREL_USE_HEAP_ENABLE -#define OPENTHREAD_CONFIG_TREL_USE_HEAP_ENABLE 0 +#define OPENTHREAD_CONFIG_TREL_USE_HEAP_ENABLE OPENTHREAD_CONFIG_TREL_MANAGE_DNSSD_ENABLE #endif /** diff --git a/src/core/instance/instance.cpp b/src/core/instance/instance.cpp index e0748b359..65384bba4 100644 --- a/src/core/instance/instance.cpp +++ b/src/core/instance/instance.cpp @@ -95,6 +95,9 @@ Instance::Instance(void) // allow other modules to use it. , mDnssd(*this) #endif +#if OPENTHREAD_CONFIG_MULTICAST_DNS_ENABLE + , mMdnsCore(*this) +#endif #if OPENTHREAD_CONFIG_PLATFORM_KEY_REFERENCES_ENABLE , mCryptoStorageKeyRefManager(*this) #endif @@ -128,9 +131,6 @@ Instance::Instance(void) #if OPENTHREAD_CONFIG_DNS_DSO_ENABLE , mDnsDso(*this) #endif -#if OPENTHREAD_CONFIG_MULTICAST_DNS_ENABLE - , mMdnsCore(*this) -#endif #if OPENTHREAD_CONFIG_SNTP_CLIENT_ENABLE , mSntpClient(*this) #endif diff --git a/src/core/instance/instance.hpp b/src/core/instance/instance.hpp index 210311027..6a4ce30d8 100644 --- a/src/core/instance/instance.hpp +++ b/src/core/instance/instance.hpp @@ -480,10 +480,13 @@ private: MessagePool mMessagePool; #if OPENTHREAD_CONFIG_PLATFORM_DNSSD_ENABLE || OPENTHREAD_CONFIG_MULTICAST_DNS_ENABLE - // DNS-SD (mDNS) platform is initialized early to - // allow other modules to use it. + // DNS-SD platform and mDNS are initialized early to + // allow other modules to use them. Dnssd mDnssd; #endif +#if OPENTHREAD_CONFIG_MULTICAST_DNS_ENABLE + Dns::Multicast::Core mMdnsCore; +#endif #if OPENTHREAD_CONFIG_PLATFORM_KEY_REFERENCES_ENABLE Crypto::Storage::KeyRefManager mCryptoStorageKeyRefManager; @@ -529,10 +532,6 @@ private: Dns::Dso mDnsDso; #endif -#if OPENTHREAD_CONFIG_MULTICAST_DNS_ENABLE - Dns::Multicast::Core mMdnsCore; -#endif - #if OPENTHREAD_CONFIG_SNTP_CLIENT_ENABLE Sntp::Client mSntpClient; #endif diff --git a/src/core/net/dnssd.cpp b/src/core/net/dnssd.cpp index 27f193088..e2445e8e0 100644 --- a/src/core/net/dnssd.cpp +++ b/src/core/net/dnssd.cpp @@ -530,6 +530,10 @@ void Dnssd::HandleStateChange(void) #if OPENTHREAD_CONFIG_BORDER_AGENT_ENABLE && OPENTHREAD_CONFIG_BORDER_AGENT_MESHCOP_SERVICE_ENABLE Get().HandleDnssdPlatformStateChange(); #endif + +#if OPENTHREAD_CONFIG_RADIO_LINK_TREL_ENABLE && OPENTHREAD_CONFIG_TREL_MANAGE_DNSSD_ENABLE + Get().HandleDnssdPlatformStateChange(); +#endif } #if OPENTHREAD_CONFIG_MULTICAST_DNS_ENABLE diff --git a/src/core/net/mdns.cpp b/src/core/net/mdns.cpp index 80ad10d5e..4f2e858be 100644 --- a/src/core/net/mdns.cpp +++ b/src/core/net/mdns.cpp @@ -114,11 +114,12 @@ Error Core::SetEnabled(bool aEnable, uint32_t aInfraIfIndex) Error error = kErrorNone; VerifyOrExit(aEnable != mIsEnabled, error = kErrorAlready); - SuccessOrExit(error = otPlatMdnsSetListeningEnabled(&GetInstance(), aEnable, aInfraIfIndex)); mIsEnabled = aEnable; mInfraIfIndex = aInfraIfIndex; + SuccessOrExit(error = otPlatMdnsSetListeningEnabled(&GetInstance(), aEnable, aInfraIfIndex)); + if (mIsEnabled) { LogInfo("Enabling on infra-if-index %lu", ToUlong(mInfraIfIndex)); diff --git a/src/core/radio/trel_interface.cpp b/src/core/radio/trel_interface.cpp index 280839249..ac64ffbc2 100644 --- a/src/core/radio/trel_interface.cpp +++ b/src/core/radio/trel_interface.cpp @@ -127,6 +127,11 @@ Error Interface::Send(Packet &aPacket, bool aIsDiscovery) Header::AckMode originalAckMode = aPacket.GetHeader().GetAckMode(); Neighbor *neighbor; + if (!peer.IsStateValid()) + { + continue; + } + if (!aIsDiscovery && (peer.GetExtPanId() != Get().GetExtPanId())) { continue; @@ -151,7 +156,7 @@ Error Interface::Send(Packet &aPacket, bool aIsDiscovery) case Header::kTypeUnicast: case Header::kTypeAck: peerEntry = Get().FindMatching(aPacket.GetHeader().GetDestination()); - VerifyOrExit(peerEntry != nullptr, error = kErrorAbort); + VerifyOrExit((peerEntry != nullptr) && peerEntry->IsStateValid(), error = kErrorAbort); otPlatTrelSend(&GetInstance(), aPacket.GetBuffer(), aPacket.GetLength(), &peerEntry->mSockAddr); break; } diff --git a/src/core/radio/trel_link.cpp b/src/core/radio/trel_link.cpp index 9d39a79da..6cbda5eed 100644 --- a/src/core/radio/trel_link.cpp +++ b/src/core/radio/trel_link.cpp @@ -407,7 +407,7 @@ void Link::CheckPeerAddrOnRxSuccess(PeerSockAddrUpdateMode aMode) if (aMode == kAllowPeerSockAddrUpdate) { LogNote("Updating the peer sock-addr to the newly received"); - mRxPacketPeer->SetSockAddr(mRxPacketSenderAddr); + mRxPacketPeer->UpdateSockAddrBasedOnRx(mRxPacketSenderAddr); } mPeerDiscoverer.NotifyPeerSocketAddressDifference(prevSockAddr, mRxPacketSenderAddr); diff --git a/src/core/radio/trel_peer.cpp b/src/core/radio/trel_peer.cpp index 6a56759c5..51ea048a2 100644 --- a/src/core/radio/trel_peer.cpp +++ b/src/core/radio/trel_peer.cpp @@ -52,11 +52,23 @@ void Peer::Init(Instance &aInstance) AsCoreType(&mExtPanId).Clear(); AsCoreType(&mSockAddr).Clear(); +#if OPENTHREAD_CONFIG_TREL_MANAGE_DNSSD_ENABLE + mPort = 0; + mExtAddressSet = false; + mResolvingService = false; + mResolvingHost = false; + mTxtDataValidated = false; + mSockAddrUpdatedBasedOnRx = false; + mState = kStateResolving; +#else mState = kStateValid; +#endif } void Peer::Free(void) { + SignalPeerRemoval(); + Log(kDeleted); #if OPENTHREAD_CONFIG_TREL_USE_HEAP_ENABLE @@ -67,22 +79,62 @@ void Peer::Free(void) #endif } +void Peer::UpdateSockAddrBasedOnRx(const Ip6::SockAddr &aSockAddr) +{ + VerifyOrExit(GetSockAddr() != aSockAddr); + + SetSockAddr(aSockAddr); +#if OPENTHREAD_CONFIG_TREL_MANAGE_DNSSD_ENABLE + mSockAddrUpdatedBasedOnRx = true; +#endif + Log(kUpdated); + +exit: + return; +} + void Peer::ScheduleToRemoveAfter(uint32_t aDelay) { - VerifyOrExit(IsStateValid()); + VerifyOrExit(!IsStateRemoving()); mRemoveTime = TimerMilli::GetNow() + aDelay; + + Log(kRemoving); + LogInfo(" after %u msec", aDelay); + SetState(kStateRemoving); Get().mTimer.FireAtIfEarlier(mRemoveTime); - Log(kRemoving); - LogInfo(" after %u msec", aDelay); + SignalPeerRemoval(); exit: return; } +void Peer::SetExtAddress(const Mac::ExtAddress &aExtAddress) +{ + mExtAddress = aExtAddress; +#if OPENTHREAD_CONFIG_TREL_MANAGE_DNSSD_ENABLE + mExtAddressSet = true; +#endif +} + +bool Peer::Matches(const Mac::ExtAddress &aExtAddress) const +{ + bool matches = false; + +#if OPENTHREAD_CONFIG_TREL_MANAGE_DNSSD_ENABLE + VerifyOrExit(mExtAddressSet); +#endif + + VerifyOrExit(GetExtAddress() == aExtAddress); + matches = true; + +exit: + return matches; +} + bool Peer::Matches(const NonNeighborMatcher &aMatcher) const { // Matches only if the peer is not a neighbor. This is used when @@ -102,14 +154,109 @@ exit: return matches; } +#if OPENTHREAD_CONFIG_TREL_MANAGE_DNSSD_ENABLE + +void Peer::SignalPeerRemoval(void) +{ + // Signal to `PeerDiscoverer` that peer is removed and/or about to be + // freed. The `PeerDiscoverer` will then stop any active resolvers + // associated with this peer. + + Get().HandlePeerRemoval(*this); +} + +void Peer::SetPort(uint16_t aPort) +{ + VerifyOrExit(mPort != aPort); + + mPort = aPort; + + if (mPort != 0) + { + AsCoreType(&mSockAddr).SetPort(mPort); + } + +exit: + return; +} + +bool Peer::Matches(const ServiceNameMatcher &aMatcher) const { return NameMatch(mServiceName, aMatcher.mServiceName); } + +bool Peer::Matches(const HostNameMatcher &aMatcher) const +{ + return mResolvingHost && NameMatch(mHostName, aMatcher.mHostName); +} + +bool Peer::NameMatch(const Heap::String &aHeapString, const char *aName) +{ + // Compares a DNS name given as a `Heap::String` with a + // `aName` C string. + return !aHeapString.IsNull() && StringMatch(aHeapString.AsCString(), aName, kStringCaseInsensitiveMatch); +} + +#endif + #if OT_SHOULD_LOG_AT(OT_LOG_LEVEL_INFO) +#if OPENTHREAD_CONFIG_TREL_MANAGE_DNSSD_ENABLE + +const char *Peer::StateToString(State aState) +{ + static const char *const kStateStrings[] = { + "valid", // (0) kStateValid + "removing", // (1) kStateRemoving + "resolving", // (2) kStateResolving + }; + + struct EnumCheck + { + InitEnumValidatorCounter(); + ValidateNextEnum(kStateValid); + ValidateNextEnum(kStateRemoving); + ValidateNextEnum(kStateResolving); + }; + + return kStateStrings[aState]; +} + +void Peer::Log(Action aAction) const +{ + LogInfo("%s peer %s, state:%s", ActionToString(aAction), + mServiceName.IsNull() ? "(null)" : mServiceName.AsCString(), StateToString(mState)); + + if (!mHostName.IsNull()) + { + LogInfo(" host:%s, port:%u", mHostName.AsCString(), mPort); + + if (mHostAddresses.GetLength() > 0) + { + LogInfo(" num-addrs:%u, %s", mHostAddresses.GetLength(), mHostAddresses[0].ToString().AsCString()); + } + } + + if (mExtAddressSet) + { + LogInfo(" ext-addr:%s, ext-panid:%s", GetExtAddress().ToString().AsCString(), + GetExtPanId().ToString().AsCString()); + } + + if (!GetSockAddr().GetAddress().IsUnspecified()) + { + LogInfo(" sock-addr:%s, based-on-rx:%s", GetSockAddr().ToString().AsCString(), + ToYesNo(mSockAddrUpdatedBasedOnRx)); + } +} + +#else + void Peer::Log(Action aAction) const { LogInfo("%s peer mac:%s, xpan:%s, %s", ActionToString(aAction), GetExtAddress().ToString().AsCString(), GetExtPanId().ToString().AsCString(), GetSockAddr().ToString().AsCString()); } +#endif + const char *Peer::ActionToString(Action aAction) { static const char *const kActionStrings[] = { @@ -137,6 +284,29 @@ const char *Peer::ActionToString(Action aAction) #endif // OT_SHOULD_LOG_AT(OT_LOG_LEVEL_INFO) +#if OPENTHREAD_CONFIG_TREL_MANAGE_DNSSD_ENABLE + +//--------------------------------------------------------------------------------------------------------------------- +// Peer::AddressArray + +void Peer::AddressArray::CloneFrom(const AddressArray &aOtherArray) +{ + Free(); + + VerifyOrExit(!aOtherArray.IsEmpty()); + SuccessOrAssert(ReserveCapacity(aOtherArray.GetLength())); + + for (const Ip6::Address &addr : aOtherArray) + { + SuccessOrAssert(PushBack(addr)); + } + +exit: + return; +} + +#endif + //--------------------------------------------------------------------------------------------------------------------- // PeerTable diff --git a/src/core/radio/trel_peer.hpp b/src/core/radio/trel_peer.hpp index 16a63d40a..4b8fa5847 100644 --- a/src/core/radio/trel_peer.hpp +++ b/src/core/radio/trel_peer.hpp @@ -43,6 +43,9 @@ #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/log.hpp" #include "common/non_copyable.hpp" @@ -83,6 +86,14 @@ class Peer : public InstanceLocatorInit, friend class LinkedListEntry; public: + /** + * Indicates whether the `Peer` is in valid state. + * + * @returns TRUE If the `peer` is in valid state. + * @returns FALSE If the `Peer` is not in valid state (e.g., still being resolved or scheduled for removal). + */ + bool IsStateValid(void) const { return mState == kStateValid; } + /** * Returns the Extended MAC Address of the discovered TREL peer. * @@ -105,17 +116,45 @@ public: const Ip6::SockAddr &GetSockAddr(void) const { return AsCoreType(&mSockAddr); } /** - * Set the IPv6 socket address of the discovered TREL peer. + * Updates the IPv6 socket address of the discovered TREL peer based on a received message from peer. * - * @param[in] aSockAddr The IPv6 socket address. + * @param[in] aSockAddr The IPv6 socket address discovered based on received message from the peer */ - void SetSockAddr(const Ip6::SockAddr &aSockAddr) { mSockAddr = aSockAddr; } + void UpdateSockAddrBasedOnRx(const Ip6::SockAddr &aSockAddr); + +#if OPENTHREAD_CONFIG_TREL_MANAGE_DNSSD_ENABLE + + /** + * Returns the TREL peer service name (service instance label). + * + * @returns The peer service name, or `nullptr` if not yet known. + */ + const char *GetServiceName(void) const { return mServiceName.AsCString(); } + + /** + * Returns the host name of the discovered TREL peer. + * + * @returns The host name as a null-terminated C string, or `nullptr` if not yet known. + */ + const char *GetHostName(void) const { return mHostName.AsCString(); } + + /** + * Returns the list of discovered IPv6 host addresses for the TREL peer. + * + * The array is sorted to place preferred addresses at the top of the list. + * + * @returns An array of IPv6 addresses. + */ + const Heap::Array &GetHostAddresses(void) const { return mHostAddresses; } + +#endif // OPENTHREAD_CONFIG_TREL_MANAGE_DNSSD_ENABLE private: enum State : uint8_t { kStateValid, kStateRemoving, + kStateResolving, }; enum Action : uint8_t @@ -158,24 +197,68 @@ private: TimeMilli mNow; }; +#if OPENTHREAD_CONFIG_TREL_MANAGE_DNSSD_ENABLE + struct ServiceNameMatcher + { + explicit ServiceNameMatcher(const char *SrerviceName) + : mServiceName(SrerviceName) + { + } + + const char *mServiceName; + }; + + struct HostNameMatcher + { + explicit HostNameMatcher(const char *aHostName) + : mHostName(aHostName) + { + } + + const char *mHostName; + }; + + class AddressArray : public Heap::Array + { + public: + bool IsEmpty(void) const { return (GetLength() == 0); } + void CloneFrom(const AddressArray &aOtherArray); + }; + +#endif + void Init(Instance &aInstance); void Free(void); void SetState(State aState) { mState = aState; } - bool IsStateValid(void) const { return mState == kStateValid; } bool IsStateRemoving(void) const { return mState == kStateRemoving; } - void SetExtAddress(const Mac::ExtAddress &aExtAddress) { mExtAddress = aExtAddress; } + bool IsStateResolving(void) const { return mState == kStateResolving; } + void SetExtAddress(const Mac::ExtAddress &aExtAddress); void SetExtPanId(const MeshCoP::ExtendedPanId &aExtPanId) { mExtPanId = aExtPanId; } + void SetSockAddr(const Ip6::SockAddr &aSockAddr) { mSockAddr = aSockAddr; } void ScheduleToRemoveAfter(uint32_t aDelay); - bool Matches(const Mac::ExtAddress &aExtAddress) const { return GetExtAddress() == aExtAddress; } + bool Matches(const Mac::ExtAddress &aExtAddress) const; bool Matches(const Ip6::SockAddr &aSockAddr) const { return GetSockAddr() == aSockAddr; } bool Matches(State aState) const { return mState == aState; } + bool Matches(const Peer &aPeer) const { return this == &aPeer; } bool Matches(const OtherExtPanIdMatcher &aMatcher) const { return GetExtPanId() != aMatcher.mExtPanId; } bool Matches(const NonNeighborMatcher &aMatcher) const; bool Matches(const ExpireChecker &aChecker) const { return IsStateRemoving() && (aChecker.mNow >= mRemoveTime); } +#if OPENTHREAD_CONFIG_TREL_MANAGE_DNSSD_ENABLE + void SetPort(uint16_t aPort); + bool Matches(const ServiceNameMatcher &aMatcher) const; + bool Matches(const HostNameMatcher &aMatcher) const; + void SignalPeerRemoval(void); + + static bool NameMatch(const Heap::String &aHeapString, const char *aName); +#else + void SignalPeerRemoval(void) {} +#endif + #if OT_SHOULD_LOG_AT(OT_LOG_LEVEL_INFO) void Log(Action aAction) const; static const char *ActionToString(Action aAction); + static const char *StateToString(State aState); #else void Log(Action) const {} #endif @@ -183,6 +266,17 @@ private: Peer *mNext; State mState; TimeMilli mRemoveTime; +#if OPENTHREAD_CONFIG_TREL_MANAGE_DNSSD_ENABLE + bool mExtAddressSet : 1; + bool mResolvingService : 1; + bool mResolvingHost : 1; + bool mTxtDataValidated : 1; + bool mSockAddrUpdatedBasedOnRx : 1; + uint16_t mPort; + Heap::String mServiceName; + Heap::String mHostName; + AddressArray mHostAddresses; +#endif }; //--------------------------------------------------------------------------------------------------------------------- diff --git a/src/core/radio/trel_peer_discoverer.cpp b/src/core/radio/trel_peer_discoverer.cpp index 15b9c466f..896d0fe0a 100644 --- a/src/core/radio/trel_peer_discoverer.cpp +++ b/src/core/radio/trel_peer_discoverer.cpp @@ -41,19 +41,42 @@ namespace Trel { RegisterLogModule("TrelDiscoverer"); +#if OPENTHREAD_CONFIG_TREL_MANAGE_DNSSD_ENABLE +const char PeerDiscoverer::kTrelServiceType[] = "_trel._udp"; +#endif + PeerDiscoverer::PeerDiscoverer(Instance &aInstance) : InstanceLocator(aInstance) - , mIsRunning(false) - , mRegisterServiceTask(aInstance) + , mState(kStateStopped) + , mServiceTask(aInstance) +#if OPENTHREAD_CONFIG_TREL_MANAGE_DNSSD_ENABLE + , mServiceName(aInstance) + , mBrowsing(false) +#endif { } +PeerDiscoverer::~PeerDiscoverer(void) +{ + mState = kStateStopped; + Get().Clear(); +} + void PeerDiscoverer::Start(void) { - VerifyOrExit(!mIsRunning); + VerifyOrExit(mState == kStateStopped); - mIsRunning = true; - PostRegisterServiceTask(); +#if OPENTHREAD_CONFIG_TREL_MANAGE_DNSSD_ENABLE + if (!Get().IsReady()) + { + mState = kStatePendingDnssd; + } + else +#endif + { + mState = kStateRunning; + PostServiceTask(); + } exit: return; @@ -61,11 +84,21 @@ exit: void PeerDiscoverer::Stop(void) { - VerifyOrExit(mIsRunning); + VerifyOrExit(mState != kStateStopped); - mIsRunning = false; + mState = kStateStopped; Get().Clear(); +#if OPENTHREAD_CONFIG_TREL_MANAGE_DNSSD_ENABLE + UnregisterService(); + + if (mBrowsing) + { + mBrowsing = false; + Get().StopBrowser(Browser()); + } +#endif + exit: return; } @@ -76,32 +109,51 @@ void PeerDiscoverer::NotifyPeerSocketAddressDifference(const Ip6::SockAddr &aPee otPlatTrelNotifyPeerSocketAddressDifference(&GetInstance(), &aPeerSockAddr, &aRxSockAddr); } -void PeerDiscoverer::PostRegisterServiceTask(void) +void PeerDiscoverer::PostServiceTask(void) { - if (mIsRunning) + if (IsRunning()) { - mRegisterServiceTask.Post(); + mServiceTask.Post(); } } +void PeerDiscoverer::HandleServiceTask(void) +{ + VerifyOrExit(IsRunning()); + + RegisterService(); + +#if OPENTHREAD_CONFIG_TREL_MANAGE_DNSSD_ENABLE + if (!mBrowsing) + { + mBrowsing = true; + Get().StartBrowser(Browser()); + } +#endif + +exit: + return; +} + void PeerDiscoverer::RegisterService(void) { TxtDataEncoder txtData(GetInstance()); uint16_t port; - VerifyOrExit(mIsRunning); - port = Get().GetUdpPort(); txtData.Encode(); +#if OPENTHREAD_CONFIG_TREL_MANAGE_DNSSD_ENABLE + RegisterService(port, txtData); +#else LogInfo("Registering DNS-SD service: port:%u", port); otPlatTrelRegisterService(&GetInstance(), port, txtData.GetBytes(), static_cast(txtData.GetLength())); - -exit: - return; +#endif } +#if !OPENTHREAD_CONFIG_TREL_MANAGE_DNSSD_ENABLE + extern "C" void otPlatTrelHandleDiscoveredPeerInfo(otInstance *aInstance, const otPlatTrelPeerInfo *aInfo) { Instance &instance = AsCoreType(aInstance); @@ -121,7 +173,7 @@ void PeerDiscoverer::HandleDiscoveredPeerInfo(const PeerInfo &aInfo) Peer::Action action = Peer::kUpdated; bool shouldLog = true; - VerifyOrExit(mIsRunning); + VerifyOrExit(IsRunning()); txtData.Init(aInfo.mTxtData, aInfo.mTxtLength); SuccessOrExit(txtData.Decode(txtInfo)); @@ -186,6 +238,493 @@ exit: return; } +#endif // !OPENTHREAD_CONFIG_TREL_MANAGE_DNSSD_ENABLE + +//- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + +#if OPENTHREAD_CONFIG_TREL_MANAGE_DNSSD_ENABLE + +void PeerDiscoverer::HandleDnssdPlatformStateChange(void) +{ + if (Get().IsReady()) + { + VerifyOrExit(mState == kStatePendingDnssd); + mState = kStateRunning; + PostServiceTask(); + } + else + { + VerifyOrExit(mState != kStateStopped); + mState = kStatePendingDnssd; + mBrowsing = false; + Get().Clear(); + } + +exit: + return; +} + +void PeerDiscoverer::RegisterService(uint16_t aPort, const TxtData &aTxtData) +{ + Dnssd::Service service; + + service.Clear(); + service.mServiceType = kTrelServiceType; + service.mServiceInstance = mServiceName.GetName(); + service.mTxtData = aTxtData.GetBytes(); + service.mTxtDataLength = aTxtData.GetLength(); + service.mPort = aPort; + + LogInfo("Registering service %s.%s", service.mServiceInstance, kTrelServiceType); + LogInfo(" port:%u, ext-addr:%s, ext-panid:%s", aPort, Get().GetExtAddress().ToString().AsCString(), + Get().GetExtPanId().ToString().AsCString()); + + Get().RegisterService(service, /* aRequestId */ 0, HandleRegisterDone); +} + +void PeerDiscoverer::UnregisterService(void) +{ + Dnssd::Service service; + + service.Clear(); + service.mServiceType = kTrelServiceType; + service.mServiceInstance = mServiceName.GetName(); + + Get().UnregisterService(service, /* aRequestId */ 0, /* aCallback */ nullptr); +} + +void PeerDiscoverer::HandleRegisterDone(otInstance *aInstance, Dnssd::RequestId aRequestId, otError aError) +{ + OT_UNUSED_VARIABLE(aRequestId); + AsCoreType(aInstance).Get().HandleRegisterDone(aError); +} + +void PeerDiscoverer::HandleRegisterDone(Error aError) +{ + VerifyOrExit(IsRunning()); + + if (aError == kErrorNone) + { + LogInfo("DNS-SD service registered successfully"); + } + else + { + LogInfo("Failed to register DNS-SD service with name:%s, Error:%s", mServiceName.GetName(), + ErrorToString(aError)); + UnregisterService(); + + // Generate a new name (appending a suffix index to the name) + // and try again. + mServiceName.GenerateName(); + PostServiceTask(); + } + +exit: + return; +} + +void PeerDiscoverer::HandleBrowseResult(otInstance *aInstance, const otPlatDnssdBrowseResult *aResult) +{ + AsCoreType(aInstance).Get().HandleBrowseResult(*aResult); +} + +void PeerDiscoverer::HandleBrowseResult(const Dnssd::BrowseResult &aResult) +{ + Peer *peer; + + VerifyOrExit(IsRunning()); + + peer = Get().FindMatching(Peer::ServiceNameMatcher(aResult.mServiceInstance)); + + if (aResult.mTtl == 0) + { + // Previously discovered service is now removed. + + VerifyOrExit(peer != nullptr); + peer->ScheduleToRemoveAfter(kRemoveDelay); + } + else + { + // A new service is discovered. + + Peer::Action action = Peer::kAdded; + + if (peer == nullptr) + { + peer = Get().AllocateAndAddNewPeer(); + VerifyOrExit(peer != nullptr); + SuccessOrAssert(peer->mServiceName.Set(aResult.mServiceInstance)); + } + else + { + action = Peer::kReAdded; + } + + peer->SetState(Peer::kStateResolving); + peer->Log(action); + + StartServiceResolvers(*peer); + } + +exit: + return; +} + +void PeerDiscoverer::StartServiceResolvers(Peer &aPeer) +{ + VerifyOrExit(!aPeer.mResolvingService); + + aPeer.mResolvingService = true; + Get().StartSrvResolver(SrvResolver(aPeer)); + Get().StartTxtResolver(TxtResolver(aPeer)); + +exit: + return; +} + +void PeerDiscoverer::StopServiceResolvers(Peer &aPeer) +{ + VerifyOrExit(aPeer.mResolvingService); + + aPeer.mResolvingService = false; + aPeer.mTxtDataValidated = false; + aPeer.mPort = 0; + aPeer.mHostName.Free(); + + Get().StopSrvResolver(SrvResolver(aPeer)); + Get().StopTxtResolver(TxtResolver(aPeer)); + +exit: + return; +} + +void PeerDiscoverer::HandleSrvResult(otInstance *aInstance, const otPlatDnssdSrvResult *aResult) +{ + AsCoreType(aInstance).Get().HandleSrvResult(*aResult); +} + +void PeerDiscoverer::HandleSrvResult(const Dnssd::SrvResult &aResult) +{ + Peer *peer; + + VerifyOrExit(IsRunning()); + + peer = Get().FindMatching(Peer::ServiceNameMatcher(aResult.mServiceInstance)); + VerifyOrExit(peer != nullptr); + + if (aResult.mTtl == 0) + { + peer->SetPort(0); + StopHostAddressResolver(*peer); + } + else + { + peer->SetPort(aResult.mPort); + + if (!Peer::NameMatch(peer->mHostName, aResult.mHostName)) + { + StopHostAddressResolver(*peer); + SuccessOrAssert(peer->mHostName.Set(aResult.mHostName)); + StartHostAddressResolver(*peer); + } + } + + UpdatePeerState(*peer); + +exit: + return; +} + +void PeerDiscoverer::HandleTxtResult(otInstance *aInstance, const otPlatDnssdTxtResult *aResult) +{ + AsCoreType(aInstance).Get().HandleTxtResult(*aResult); +} + +void PeerDiscoverer::HandleTxtResult(const Dnssd::TxtResult &aResult) +{ + Peer *peer; + + VerifyOrExit(IsRunning()); + + peer = Get().FindMatching(Peer::ServiceNameMatcher(aResult.mServiceInstance)); + VerifyOrExit(peer != nullptr); + + ProcessPeerTxtData(aResult, *peer); + + UpdatePeerState(*peer); + +exit: + return; +} + +void PeerDiscoverer::ProcessPeerTxtData(const Dnssd::TxtResult &aResult, Peer &aPeer) +{ + TxtData txtData; + TxtData::Info txtInfo; + + aPeer.mTxtDataValidated = false; + + VerifyOrExit(aResult.mTtl != 0); + + txtData.Init(aResult.mTxtData, aResult.mTxtDataLength); + + SuccessOrExit(txtData.Decode(txtInfo)); + + if (txtInfo.mExtAddress == Get().GetExtAddress()) + { + LogInfo("Peer %s is this device itself", aPeer.mServiceName.AsCString()); + aPeer.ScheduleToRemoveAfter(0); + ExitNow(); + } + + aPeer.SetExtPanId(txtInfo.mExtPanId); + + if (aPeer.GetExtAddress() != txtInfo.mExtAddress) + { + // Remove any peer that is associated with the same ExtAddress. + // These are likely stale entries. This ensure we have at most + // one entry associated with an `ExtAddress`. + + Get().RemoveAndFreeAllMatching(txtInfo.mExtAddress); + + aPeer.SetExtAddress(txtInfo.mExtAddress); + } + + aPeer.mTxtDataValidated = true; + +exit: + return; +} + +void PeerDiscoverer::StartHostAddressResolver(Peer &aPeer) +{ + Peer *sameHostPeer; + + VerifyOrExit(!aPeer.mResolvingHost); + + sameHostPeer = Get().FindMatching(Peer::HostNameMatcher(aPeer.mHostName.AsCString())); + + aPeer.mResolvingHost = true; + + if (sameHostPeer != nullptr) + { + UpdatePeerAddresses(aPeer, sameHostPeer->mHostAddresses); + } + else + { + Get().StartIp6AddressResolver(AddressResolver(aPeer)); + } + +exit: + return; +} + +void PeerDiscoverer::StopHostAddressResolver(Peer &aPeer) +{ + // We check if any `Peer` in the table is associated with the + // same host name before we decide whether we stop the + // `AddressResolver` for the given host name. + + VerifyOrExit(aPeer.mResolvingHost); + + aPeer.mResolvingHost = false; + aPeer.mHostAddresses.Free(); + + VerifyOrExit(!Get().ContainsMatching(Peer::HostNameMatcher(aPeer.mHostName.AsCString()))); + + Get().StopIp6AddressResolver(AddressResolver(aPeer)); + +exit: + return; +} + +void PeerDiscoverer::HandleAddressResult(otInstance *aInstance, const otPlatDnssdAddressResult *aResult) +{ + AsCoreType(aInstance).Get().HandleAddressResult(*aResult); +} + +void PeerDiscoverer::HandleAddressResult(const Dnssd::AddressResult &aResult) +{ + Peer::AddressArray sortedAddresses; + + VerifyOrExit(IsRunning()); + + while (true) + { + // Iterate through addresses in `aResult`, adding them one by + // one to `sortedAddresses` such that more favored addresses + // are placed at the beginning of the array. + + AddrAndTtl favored; + + favored.Clear(); + + for (uint16_t index = 0; index < aResult.mAddressesLength; index++) + { + const Dnssd::AddressAndTtl &newAddrAndTtl = aResult.mAddresses[index]; + const Ip6::Address &newAddr = AsCoreType(&aResult.mAddresses[index].mAddress); + uint32_t newTtl = aResult.mAddresses[index].mTtl; + + // Skip the address if it is already in the `sortedAddresses` + // list or if the address is invalid(e.g., zero TTL or + // multicast). Then check if it is favored over the current + // `favored` selection and update `favored` accordingly. + + if (sortedAddresses.Contains(newAddr)) + { + continue; + } + + if ((newTtl == 0) || newAddr.IsUnspecified() || newAddr.IsLoopback() || newAddr.IsMulticast()) + { + continue; + } + + if (favored.IsFavoredOver(newAddrAndTtl)) + { + continue; + } + + favored.SetFrom(newAddrAndTtl); + } + + if (favored.IsEmpty()) + { + break; + } + + SuccessOrAssert(sortedAddresses.PushBack(favored.mAddress)); + } + + // Update the addresses of all `Peer`s that are associated with + // the resolved `aResult.mHostName`. + // + // This handles the case where multiple TREL services may be + // present on the same host machine. While this is unlikely in + // actual deployments, it can be useful for testing and + // simulation where a single machine may be acting as multiple + // Thread nodes thus advertising multiple TREL services from the + // same host name. + + for (Peer &peer : Get()) + { + if (peer.IsStateRemoving()) + { + continue; + } + + if (peer.Matches(Peer::HostNameMatcher(aResult.mHostName))) + { + UpdatePeerAddresses(peer, sortedAddresses); + UpdatePeerState(peer); + } + } + +exit: + return; +} + +void PeerDiscoverer::UpdatePeerAddresses(Peer &aPeer, const Peer::AddressArray &aSortedAddresses) +{ + // Updates `aPeer.mHostAddresses` and decides whether to update + // `aPeer.mSockAddr`(the primary address used for communication + // with the peer). + + bool shouldChangeSockAddr = false; + + // If the new `aSortedAddresses` is empty, clear + // `aPeer.mHostAddresses` but leave `aPeer.mSockAddr` + // unchanged (retaining the last known good address). + + if (aSortedAddresses.IsEmpty()) + { + aPeer.mHostAddresses.Clear(); + ExitNow(); + } + + // Determine if `aPeer.mSockAddr` should be updated: The goal is + // to use the most stable address, preferring one learned from a + // received packet if mDNS still considers it valid or if mDNS + // information is unstable. + // + // If `mSockAddr` was not set by a received packet, then the + // `mSockAddr` was last set by a previous mDNS resolution. Always + // update it with the new address `aSortedAddresses[0]`. + // + // If `mSockAddr` was previously set by a received packet + // (`aPeer.mSockAddrUpdatedBasedOnRx` is true): + // + // - If the current `mSockAddr` is in the `aSortedAddresses` list + // then we keep the current `mSockAddr`. It's packet-verified + // and now mDNS-confirmed. + // + // - If the current `mSockAddr` (from rx packet) is not present in + // the `aSortedAddresses`, then we only update and use + // `aSortedAddresses[0]` if this differs from the previous one + // discovered through mDNS resolution. This approach avoids + // changing a working, rx-verified address due to transient + // mDNS issues. If we see a change to the list of addresses + // reported through mDNS, we can be sure that an mDNS answer + // was indeed received and processed (which updated the list, + // so we know the list is most likely more recent and correct) + + if (!aPeer.mSockAddrUpdatedBasedOnRx) + { + shouldChangeSockAddr = true; + } + else if (!aSortedAddresses.Contains(aPeer.GetSockAddr().GetAddress())) + { + const Ip6::Address *prevFront = aPeer.mHostAddresses.Front(); + + if ((prevFront == nullptr) || (*prevFront != aSortedAddresses[0])) + { + shouldChangeSockAddr = true; + } + } + + if (shouldChangeSockAddr && (aPeer.GetSockAddr().GetAddress() != aSortedAddresses[0])) + { + AsCoreType(&aPeer.mSockAddr).SetAddress(aSortedAddresses[0]); + aPeer.mSockAddrUpdatedBasedOnRx = false; + } + + aPeer.mHostAddresses.CloneFrom(aSortedAddresses); + +exit: + return; +} + +void PeerDiscoverer::UpdatePeerState(Peer &aPeer) +{ + VerifyOrExit(aPeer.IsStateResolving()); + VerifyOrExit(aPeer.mResolvingService && aPeer.mResolvingHost); + VerifyOrExit(aPeer.mTxtDataValidated); + VerifyOrExit(aPeer.mPort != 0); + VerifyOrExit(aPeer.mHostAddresses.GetLength() > 0); + + aPeer.SetState(Peer::kStateValid); + aPeer.Log(Peer::kUpdated); + +exit: + return; +} + +void PeerDiscoverer::HandlePeerRemoval(Peer &aPeer) +{ + // Callback from `Peer` signaling that peer is being removed + // or scheduled to be removed. We stop any active resolvers + // associated with this peer. + // + // The order of calls is important here since the + // `StopServiceResolvers()` clears the `aPeer.mHostName` which + // would be needed in `StopHostAddressResolver()`. + + StopHostAddressResolver(aPeer); + StopServiceResolvers(aPeer); +} + +#endif // OPENTHREAD_CONFIG_TREL_MANAGE_DNSSD_ENABLE + //---------------------------------------------------------------------------------------------------------------------- // PeerDiscoverer::TxtData @@ -267,6 +806,134 @@ void PeerDiscoverer::TxtDataEncoder::Encode(void) mLength = encoder.GetLength(); } +#if OPENTHREAD_CONFIG_TREL_MANAGE_DNSSD_ENABLE + +//---------------------------------------------------------------------------------------------------------------------- +// PeerDiscoverer::ServiceName + +const char PeerDiscoverer::ServiceName::kNamePrefix[] = "otTREL"; + +PeerDiscoverer::ServiceName::ServiceName(Instance &aInstance) + : InstanceLocator(aInstance) + , mSuffixIndex(0) +{ + ClearAllBytes(mName); +} + +const char *PeerDiscoverer::ServiceName::GetName(void) +{ + if (mName[0] == kNullChar) + { + GenerateName(); + } + + return mName; +} + +void PeerDiscoverer::ServiceName::GenerateName(void) +{ + StringWriter writer(mName, sizeof(mName)); + + writer.Append("%s%s", kNamePrefix, Get().GetExtAddress().ToString().AsCString()); + + if (mSuffixIndex != 0) + { + writer.Append("(%u)", mSuffixIndex); + } + + mSuffixIndex++; +} + +//---------------------------------------------------------------------------------------------------------------------- +// PeerDiscoverer::Browser + +PeerDiscoverer::Browser::Browser(void) +{ + Clear(); + mServiceType = kTrelServiceType; + mCallback = PeerDiscoverer::HandleBrowseResult; +} + +//---------------------------------------------------------------------------------------------------------------------- +// PeerDiscoverer::SrvResolver + +PeerDiscoverer::SrvResolver::SrvResolver(const Peer &aPeer) +{ + Clear(); + mServiceInstance = aPeer.mServiceName.AsCString(); + mServiceType = kTrelServiceType; + mCallback = PeerDiscoverer::HandleSrvResult; +} + +//---------------------------------------------------------------------------------------------------------------------- +// PeerDiscoverer::TxtResolver + +PeerDiscoverer::TxtResolver::TxtResolver(const Peer &aPeer) +{ + Clear(); + mServiceInstance = aPeer.mServiceName.AsCString(); + mServiceType = kTrelServiceType; + mCallback = PeerDiscoverer::HandleTxtResult; +} + +//---------------------------------------------------------------------------------------------------------------------- +// PeerDiscoverer::AddressResolver + +PeerDiscoverer::AddressResolver::AddressResolver(const Peer &aPeer) +{ + Clear(); + mHostName = aPeer.mHostName.AsCString(); + mCallback = PeerDiscoverer::HandleAddressResult; +} + +//---------------------------------------------------------------------------------------------------------------------- +// PeerDiscoverer::AddrAndTtl + +void PeerDiscoverer::AddrAndTtl::SetFrom(const Dnssd::AddressAndTtl &aAddrAndTtl) +{ + mAddress = AsCoreType(&aAddrAndTtl.mAddress); + mTtl = aAddrAndTtl.mTtl; +} + +bool PeerDiscoverer::AddrAndTtl::IsFavoredOver(const Dnssd::AddressAndTtl &aAddrAndTtl) const +{ + int compare; + const Ip6::Address &newAddress = AsCoreType(&aAddrAndTtl.mAddress); + uint32_t newTtl = aAddrAndTtl.mTtl; + Ip6::Prefix prefix; + Ip6::Prefix newPrefix; + + if (IsEmpty()) + { + // Prefer any address when empty. + ExitNow(compare = -1); + } + + // Prefer a link-local address over non-link-local. + + compare = ThreeWayCompare(mAddress.IsLinkLocalUnicast(), newAddress.IsLinkLocalUnicast()); + VerifyOrExit(compare == 0); + + // Prefer non-ULA address over ULA address + + mAddress.GetPrefix(Ip6::NetworkPrefix::kLength, prefix); + newAddress.GetPrefix(Ip6::NetworkPrefix::kLength, newPrefix); + compare = ThreeWayCompare(!prefix.IsUniqueLocal(), !newPrefix.IsUniqueLocal()); + VerifyOrExit(compare == 0); + + // Prefer address with longer TTL. + + compare = ThreeWayCompare(mTtl, newTtl); + VerifyOrExit(compare == 0); + + compare = (mAddress < newAddress) ? 1 : 0; + +exit: + return (compare > 0); +} + +#endif // OPENTHREAD_CONFIG_TREL_MANAGE_DNSSD_ENABLE + } // namespace Trel } // namespace ot diff --git a/src/core/radio/trel_peer_discoverer.hpp b/src/core/radio/trel_peer_discoverer.hpp index 9b94ee4c8..b45b8eeb4 100644 --- a/src/core/radio/trel_peer_discoverer.hpp +++ b/src/core/radio/trel_peer_discoverer.hpp @@ -43,8 +43,22 @@ #include "common/clearable.hpp" #include "common/locator.hpp" #include "common/tasklet.hpp" +#include "net/dns_types.hpp" +#include "net/dnssd.hpp" #include "radio/trel_peer.hpp" +#if OPENTHREAD_CONFIG_TREL_MANAGE_DNSSD_ENABLE + +#if !(OPENTHREAD_CONFIG_PLATFORM_DNSSD_ENABLE || OPENTHREAD_CONFIG_MULTICAST_DNS_ENABLE) +#error "OPENTHREAD_CONFIG_TREL_MANAGE_DNSSD_ENABLE requires either the native mDNS or platform DNS-SD APIs" +#endif + +#if !OPENTHREAD_CONFIG_TREL_USE_HEAP_ENABLE +#error "OPENTHREAD_CONFIG_TREL_MANAGE_DNSSD_ENABLE requires OPENTHREAD_CONFIG_TREL_USE_HEAP_ENABLE" +#endif + +#endif + namespace ot { namespace Trel { @@ -58,7 +72,12 @@ extern "C" void otPlatTrelHandleDiscoveredPeerInfo(otInstance *aInstance, const class PeerDiscoverer : public InstanceLocator { friend class Link; +#if OPENTHREAD_CONFIG_TREL_MANAGE_DNSSD_ENABLE + friend class ot::Dnssd; + friend class Peer; +#else friend void otPlatTrelHandleDiscoveredPeerInfo(otInstance *aInstance, const otPlatTrelPeerInfo *aInfo); +#endif public: /** @@ -74,12 +93,12 @@ public: /** * Notifies that device's Extended MAC Address has changed. */ - void HandleExtAddressChange(void) { PostRegisterServiceTask(); } + void HandleExtAddressChange(void) { PostServiceTask(); } /** * Notifies that device's Extended PAN Identifier has changed. */ - void HandleExtPanIdChange(void) { PostRegisterServiceTask(); } + void HandleExtPanIdChange(void) { PostServiceTask(); } /** * Notifies that a TREL packet is received from a peer using a different socket address than the one reported @@ -90,9 +109,25 @@ public: */ void NotifyPeerSocketAddressDifference(const Ip6::SockAddr &aPeerSockAddr, const Ip6::SockAddr &aRxSockAddr); +#if OPENTHREAD_CONFIG_TREL_MANAGE_DNSSD_ENABLE + /** + * Returns the TREL service name (service instance label) used by the device itself when advertising TREL service. + * + * @returns The TREL service name. + */ + const char *GetServiceName(void) { return mServiceName.GetName(); } +#endif + private: static constexpr uint32_t kRemoveDelay = 7 * Time::kOneSecondInMsec; + enum State : uint8_t + { + kStateStopped, // Stopped. + kStatePendingDnssd, // Started but waiting for `Dnssd` to be ready. + kStateRunning, // Started and `Dnssd` is also ready. + }; + class TxtData { public: @@ -133,22 +168,121 @@ private: uint8_t mBuffer[kMaxSize]; }; +#if OPENTHREAD_CONFIG_TREL_MANAGE_DNSSD_ENABLE + + class ServiceName : public InstanceLocator + { + // Tracks the service name (instance label) to use when + // registering TREL mDNS service + + public: + explicit ServiceName(Instance &aInstance); + + const char *GetName(void); + void GenerateName(void); + + private: + static const char kNamePrefix[]; + + Dns::Name::LabelBuffer mName; + uint8_t mSuffixIndex; + }; + + class Browser : public Dnssd::Browser + { + public: + Browser(void); + }; + + class SrvResolver : public Dnssd::SrvResolver + { + public: + explicit SrvResolver(const Peer &aPeer); + }; + + class TxtResolver : public Dnssd::TxtResolver + { + public: + explicit TxtResolver(const Peer &aPeer); + }; + + class AddressResolver : public Dnssd::AddressResolver + { + public: + explicit AddressResolver(const Peer &aPeer); + }; + + struct AddrAndTtl : public Clearable + { + void SetFrom(const Dnssd::AddressAndTtl &aAddrAndTtl); + bool IsFavoredOver(const Dnssd::AddressAndTtl &aAddrAndTtl) const; + bool IsEmpty(void) const { return mTtl == 0; } + + Ip6::Address mAddress; + uint32_t mTtl; + }; + +#endif // OPENTHREAD_CONFIG_TREL_MANAGE_DNSSD_ENABLE + +#if !OPENTHREAD_CONFIG_TREL_MANAGE_DNSSD_ENABLE struct PeerInfo : public otPlatTrelPeerInfo { bool IsRemoved(void) const { return mRemoved; } const Ip6::SockAddr &GetSockAddr(void) const { return AsCoreType(&mSockAddr); } }; +#endif explicit PeerDiscoverer(Instance &aInstance); + ~PeerDiscoverer(void); - void HandleDiscoveredPeerInfo(const PeerInfo &aInfo); - void PostRegisterServiceTask(void); + bool IsRunning(void) const { return mState == kStateRunning; } + void PostServiceTask(void); + void HandleServiceTask(void); void RegisterService(void); - using RegisterServiceTask = TaskletIn; +#if OPENTHREAD_CONFIG_TREL_MANAGE_DNSSD_ENABLE + // Callback from `Dnssd` + void HandleDnssdPlatformStateChange(void); + // Callback from `Peer` class. + void HandlePeerRemoval(Peer &aPeer); - bool mIsRunning; - RegisterServiceTask mRegisterServiceTask; + void RegisterService(uint16_t aPort, const TxtData &aTxtData); + void UnregisterService(void); + void HandleRegisterDone(Error aError); + void HandleBrowseResult(const Dnssd::BrowseResult &aResult); + void StartServiceResolvers(Peer &aPeer); + void StopServiceResolvers(Peer &aPeer); + void HandleSrvResult(const Dnssd::SrvResult &aResult); + void HandleTxtResult(const Dnssd::TxtResult &aResult); + void ProcessPeerTxtData(const Dnssd::TxtResult &aResult, Peer &aPeer); + void StartHostAddressResolver(Peer &aPeer); + void StopHostAddressResolver(Peer &aPeer); + void HandleAddressResult(const Dnssd::AddressResult &aResult); + void UpdatePeerAddresses(Peer &aPeer, const Peer::AddressArray &aSortedAddresses); + void UpdatePeerState(Peer &aPeer); + + static void HandleRegisterDone(otInstance *aInstance, Dnssd::RequestId aRequestId, otError aError); + 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); + +#else + void HandleDiscoveredPeerInfo(const PeerInfo &aInfo); +#endif + + using ServiceTask = TaskletIn; + +#if OPENTHREAD_CONFIG_TREL_MANAGE_DNSSD_ENABLE + static const char kTrelServiceType[]; +#endif + + State mState; + ServiceTask mServiceTask; +#if OPENTHREAD_CONFIG_TREL_MANAGE_DNSSD_ENABLE + ServiceName mServiceName; + bool mBrowsing; +#endif }; } // namespace Trel diff --git a/tests/nexus/CMakeLists.txt b/tests/nexus/CMakeLists.txt index 3e46523ec..002f89f39 100644 --- a/tests/nexus/CMakeLists.txt +++ b/tests/nexus/CMakeLists.txt @@ -47,6 +47,7 @@ add_library(ot-nexus-platform platform/nexus_node.cpp platform/nexus_radio.cpp platform/nexus_settings.cpp + platform/nexus_trel.cpp ) target_include_directories(ot-nexus-platform @@ -113,3 +114,4 @@ ot_nexus_test(border_agent) ot_nexus_test(dtls) ot_nexus_test(form_join) ot_nexus_test(large_network) +ot_nexus_test(trel) diff --git a/tests/nexus/build.sh b/tests/nexus/build.sh index 94718aa2d..f830765f1 100755 --- a/tests/nexus/build.sh +++ b/tests/nexus/build.sh @@ -44,6 +44,17 @@ else top_builddir=. fi +case $1 in + trel) + trel=ON + fifteenfour=OFF + ;; + *) + trel=OFF + fifteenfour=ON + ;; +esac + echo "====================================================================================================" echo "Building OpenThread Nexus test platform" echo "====================================================================================================" @@ -51,6 +62,7 @@ cd "${top_builddir}" || die "cd failed" cmake -GNinja -DOT_PLATFORM=nexus -DOT_COMPILE_WARNING_AS_ERROR=ON \ -DOT_MULTIPLE_INSTANCE=ON \ -DOT_THREAD_VERSION=1.4 -DOT_APP_CLI=OFF -DOT_APP_NCP=OFF -DOT_APP_RCP=OFF \ + -DOT_15_4=${fifteenfour} -DOT_TREL=${trel} \ -DOT_PROJECT_CONFIG=../tests/nexus/openthread-core-nexus-config.h \ "${top_srcdir}" || die ninja || die diff --git a/tests/nexus/openthread-core-nexus-config.h b/tests/nexus/openthread-core-nexus-config.h index d18a2cae4..39fbb0445 100644 --- a/tests/nexus/openthread-core-nexus-config.h +++ b/tests/nexus/openthread-core-nexus-config.h @@ -127,6 +127,8 @@ #define OPENTHREAD_CONFIG_TMF_NETDATA_SERVICE_ENABLE 1 #define OPENTHREAD_CONFIG_TMF_NETDIAG_CLIENT_ENABLE 1 #define OPENTHREAD_CONFIG_TMF_SNOOP_CACHE_ENTRY_TIMEOUT 3 +#define OPENTHREAD_CONFIG_TREL_MANAGE_DNSSD_ENABLE 1 +#define OPENTHREAD_CONFIG_TREL_USE_HEAP_ENABLE 1 #define OPENTHREAD_CONFIG_UPTIME_ENABLE 1 #endif /* OT_CORE_NEXUS_CONFIG_H_ */ diff --git a/tests/nexus/platform/nexus_core.cpp b/tests/nexus/platform/nexus_core.cpp index 695d147de..34a18917b 100644 --- a/tests/nexus/platform/nexus_core.cpp +++ b/tests/nexus/platform/nexus_core.cpp @@ -32,20 +32,22 @@ namespace ot { namespace Nexus { -Core *Core::sCore = nullptr; +Core *Core::sCore = nullptr; +bool Core::sInUse = false; Core::Core(void) : mNow(0) , mCurNodeId(0) , mPendingAction(false) { - VerifyOrQuit(sCore == nullptr); - sCore = this; + VerifyOrQuit(!sInUse); + sCore = this; + sInUse = true; mNextAlarmTime = mNow.GetDistantFuture(); } -Core::~Core(void) { sCore = nullptr; } +Core::~Core(void) { sInUse = false; } Node &Core::CreateNode(void) { @@ -101,6 +103,9 @@ void Core::Process(Node &aNode) ProcessRadio(aNode); ProcessMdns(aNode); +#if OPENTHREAD_CONFIG_RADIO_LINK_TREL_ENABLE + ProcessTrel(aNode); +#endif if (aNode.mAlarm.ShouldTrigger(mNow)) { @@ -221,5 +226,33 @@ void Core::ProcessMdns(Node &aNode) aNode.mMdns.mPendingTxList.Free(); } +#if OPENTHREAD_CONFIG_RADIO_LINK_TREL_ENABLE + +void Core::ProcessTrel(Node &aNode) +{ + Ip6::SockAddr senderSockAddr; + Ip6::SockAddr rxNodeSockAddr; + + aNode.GetTrelSockAddr(senderSockAddr); + + for (Trel::PendingTx &pendingTx : aNode.mTrel.mPendingTxList) + { + for (Node &rxNode : mNodes) + { + rxNode.GetTrelSockAddr(rxNodeSockAddr); + + if (pendingTx.mDestSockAddr == rxNodeSockAddr) + { + rxNode.mTrel.Receive(rxNode.GetInstance(), pendingTx.mPayloadData, senderSockAddr); + break; + } + } + } + + aNode.mTrel.mPendingTxList.Free(); +} + +#endif // OPENTHREAD_CONFIG_RADIO_LINK_TREL_ENABLE + } // namespace Nexus } // namespace ot diff --git a/tests/nexus/platform/nexus_core.hpp b/tests/nexus/platform/nexus_core.hpp index 4b69c00b0..44d5e8a83 100644 --- a/tests/nexus/platform/nexus_core.hpp +++ b/tests/nexus/platform/nexus_core.hpp @@ -77,8 +77,12 @@ private: void Process(Node &aNode); void ProcessRadio(Node &aNode); void ProcessMdns(Node &aNode); +#if OPENTHREAD_CONFIG_RADIO_LINK_TREL_ENABLE + void ProcessTrel(Node &aNode); +#endif static Core *sCore; + static bool sInUse; OwningList mNodes; uint16_t mCurNodeId; diff --git a/tests/nexus/platform/nexus_node.cpp b/tests/nexus/platform/nexus_node.cpp index 14d819bd9..270260c18 100644 --- a/tests/nexus/platform/nexus_node.cpp +++ b/tests/nexus/platform/nexus_node.cpp @@ -83,5 +83,13 @@ void Node::AllowList(Node &aNode) void Node::UnallowList(Node &aNode) { Get().RemoveAddress(aNode.Get().GetExtAddress()); } +#if OPENTHREAD_CONFIG_RADIO_LINK_TREL_ENABLE +void Node::GetTrelSockAddr(Ip6::SockAddr &aSockAddr) const +{ + aSockAddr.SetAddress(mMdns.mIfAddresses[0]); + aSockAddr.SetPort(mTrel.mUdpPort); +} +#endif + } // namespace Nexus } // namespace ot diff --git a/tests/nexus/platform/nexus_node.hpp b/tests/nexus/platform/nexus_node.hpp index e0e4af174..649798c94 100644 --- a/tests/nexus/platform/nexus_node.hpp +++ b/tests/nexus/platform/nexus_node.hpp @@ -36,6 +36,7 @@ #include "nexus_mdns.hpp" #include "nexus_radio.hpp" #include "nexus_settings.hpp" +#include "nexus_trel.hpp" #include "nexus_utils.hpp" namespace ot { @@ -58,6 +59,9 @@ public: void Join(Node &aNode, JoinMode aJoinMode = kAsFtd); void AllowList(Node &aNode); void UnallowList(Node &aNode); +#if OPENTHREAD_CONFIG_RADIO_LINK_TREL_ENABLE + void GetTrelSockAddr(Ip6::SockAddr &aSockAddr) const; +#endif //- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - @@ -82,7 +86,10 @@ public: Alarm mAlarm; Mdns mMdns; Settings mSettings; - bool mPendingTasklet; +#if OPENTHREAD_CONFIG_RADIO_LINK_TREL_ENABLE + Trel mTrel; +#endif + bool mPendingTasklet; private: Node(void) = default; diff --git a/tests/nexus/platform/nexus_trel.cpp b/tests/nexus/platform/nexus_trel.cpp new file mode 100644 index 000000000..4242b5f33 --- /dev/null +++ b/tests/nexus/platform/nexus_trel.cpp @@ -0,0 +1,120 @@ +/* + * 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 "nexus_core.hpp" +#include "nexus_node.hpp" + +#if OPENTHREAD_CONFIG_RADIO_LINK_TREL_ENABLE + +namespace ot { +namespace Nexus { + +//--------------------------------------------------------------------------------------------------------------------- +// otPlatTrel APIs + +extern "C" { + +void otPlatTrelEnable(otInstance *aInstance, uint16_t *aUdpPort) { AsNode(aInstance).mTrel.Enable(*aUdpPort); } + +void otPlatTrelDisable(otInstance *aInstance) { AsNode(aInstance).mTrel.Disable(); } + +void otPlatTrelSend(otInstance *aInstance, + const uint8_t *aUdpPayload, + uint16_t aUdpPayloadLen, + const otSockAddr *aDestSockAddr) +{ + AsNode(aInstance).mTrel.Send(aUdpPayload, aUdpPayloadLen, AsCoreType(aDestSockAddr)); +} + +void otPlatTrelNotifyPeerSocketAddressDifference(otInstance *aInstance, + const otSockAddr *aPeerSockAddr, + const otSockAddr *aRxSockAddr) +{ + OT_UNUSED_VARIABLE(aInstance); + OT_UNUSED_VARIABLE(aPeerSockAddr); + OT_UNUSED_VARIABLE(aRxSockAddr); +} + +const otPlatTrelCounters *otPlatTrelGetCounters(otInstance *aInstance) { return &AsNode(aInstance).mTrel.mCounters; } + +void otPlatTrelResetCounters(otInstance *aInstance) { AsNode(aInstance).mTrel.ResetCounters(); } + +} // extern "C" + +//--------------------------------------------------------------------------------------------------------------------- +// Trel + +uint16_t Trel::sLastUsedUdpPort = kUdpPortStart; + +Trel::Trel(void) + : mEnabled(false) + , mUdpPort(sLastUsedUdpPort++) +{ + ClearAllBytes(mCounters); +} + +void Trel::Enable(uint16_t &aUdpPort) +{ + mEnabled = true; + aUdpPort = mUdpPort; +} + +void Trel::Disable(void) +{ + mEnabled = false; + mPendingTxList.Free(); +} + +void Trel::Send(const uint8_t *aUdpPayload, uint16_t aUdpPayloadLen, const Ip6::SockAddr &aDestSockAddr) +{ + PendingTx *pendingTx = PendingTx::Allocate(); + + VerifyOrQuit(pendingTx != nullptr); + SuccessOrQuit(pendingTx->mPayloadData.SetFrom(aUdpPayload, aUdpPayloadLen)); + pendingTx->mDestSockAddr = aDestSockAddr; + + mPendingTxList.PushAfterTail(*pendingTx); + + mCounters.mTxPackets++; + mCounters.mTxBytes += aUdpPayloadLen; +} + +void Trel::Receive(Instance &aInstance, Heap::Data &aPayloadData, const Ip6::SockAddr &aSenderAddr) +{ + mCounters.mRxPackets++; + mCounters.mRxBytes += aPayloadData.GetLength(); + + otPlatTrelHandleReceived(&aInstance, AsNonConst(aPayloadData.GetBytes()), aPayloadData.GetLength(), &aSenderAddr); +} + +} // namespace Nexus +} // namespace ot + +#endif // OPENTHREAD_CONFIG_RADIO_LINK_TREL_ENABLE diff --git a/tests/nexus/platform/nexus_trel.hpp b/tests/nexus/platform/nexus_trel.hpp new file mode 100644 index 000000000..9dfe3222d --- /dev/null +++ b/tests/nexus/platform/nexus_trel.hpp @@ -0,0 +1,72 @@ +/* + * 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. + */ + +#ifndef OT_NEXUS_TREL_HPP_ +#define OT_NEXUS_TREL_HPP_ + +#include "instance/instance.hpp" + +#if OPENTHREAD_CONFIG_RADIO_LINK_TREL_ENABLE + +namespace ot { +namespace Nexus { + +struct Trel +{ + static constexpr uint16_t kUdpPortStart = 49152; + + typedef otPlatTrelCounters Counters; + + Trel(void); + void Enable(uint16_t &aUdpPort); + void Disable(void); + void Send(const uint8_t *aUdpPayload, uint16_t aUdpPayloadLen, const Ip6::SockAddr &aDestSockAddr); + void ResetCounters(void) { ClearAllBytes(mCounters); } + void Receive(Instance &aInstance, Heap::Data &aPayloadData, const Ip6::SockAddr &aSenderAddr); + + struct PendingTx : public Heap::Allocatable, public LinkedListEntry + { + PendingTx *mNext; + Heap::Data mPayloadData; + Ip6::SockAddr mDestSockAddr; + }; + + static uint16_t sLastUsedUdpPort; + + bool mEnabled; + uint16_t mUdpPort; + OwningList mPendingTxList; + Counters mCounters; +}; + +} // namespace Nexus +} // namespace ot + +#endif // OPENTHREAD_CONFIG_RADIO_LINK_TREL_ENABLE + +#endif // OT_NEXUS_TREL_HPP_ diff --git a/tests/nexus/test_trel.cpp b/tests/nexus/test_trel.cpp new file mode 100644 index 000000000..6df006bb4 --- /dev/null +++ b/tests/nexus/test_trel.cpp @@ -0,0 +1,778 @@ +/* + * 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 { + +#if OPENTHREAD_CONFIG_RADIO_LINK_TREL_ENABLE + +static constexpr uint32_t kInfraIfIndex = 1; +static constexpr uint16_t kMaxTxtDataSize = 128; + +void TestTrelBasic(void) +{ + // Validate basic operations, forming a network and + // joining as router, FED, MED, and SED using TREL. + // Also check TREL peer table on all devices. + + Core nexus; + Node &leader = nexus.CreateNode(); + Node &fed = nexus.CreateNode(); + Node &sed = nexus.CreateNode(); + Node &med = nexus.CreateNode(); + Node &router1 = nexus.CreateNode(); + Node &router2 = nexus.CreateNode(); + + Log("---------------------------------------------------------------------------------------"); + Log("TestTrelBasic()"); + + nexus.AdvanceTime(0); + + for (Node &node : nexus.GetNodes()) + { + node.GetInstance().SetLogLevel(kLogLevelWarn); + SuccessOrQuit(node.Get().SetEnabled(true, kInfraIfIndex)); + } + + // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + Log("Form network"); + + leader.Form(); + nexus.AdvanceTime(13 * 1000); + VerifyOrQuit(leader.Get().IsLeader()); + + fed.Join(leader, Node::kAsFed); + nexus.AdvanceTime(10 * 1000); + VerifyOrQuit(fed.Get().IsChild()); + + sed.Join(leader, Node::kAsSed); + nexus.AdvanceTime(2 * 1000); + VerifyOrQuit(sed.Get().IsChild()); + + med.Join(leader, Node::kAsMed); + nexus.AdvanceTime(2 * 1000); + VerifyOrQuit(med.Get().IsChild()); + + router1.Join(leader); + router2.Join(leader); + + // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + Log("Check all nodes roles and device modes"); + + nexus.AdvanceTime(300 * 1000); + + VerifyOrQuit(leader.Get().IsLeader()); + VerifyOrQuit(fed.Get().IsChild()); + VerifyOrQuit(sed.Get().IsChild()); + VerifyOrQuit(router1.Get().IsRouter()); + VerifyOrQuit(router2.Get().IsRouter()); + + VerifyOrQuit(fed.Get().IsRxOnWhenIdle()); + VerifyOrQuit(fed.Get().IsFullThreadDevice()); + + VerifyOrQuit(med.Get().IsRxOnWhenIdle()); + VerifyOrQuit(!med.Get().IsFullThreadDevice()); + VerifyOrQuit(med.Get().IsMinimalEndDevice()); + + VerifyOrQuit(!sed.Get().IsRxOnWhenIdle()); + VerifyOrQuit(!sed.Get().IsFullThreadDevice()); + + // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + Log("Check TREL peer table on all nodes"); + + for (Node &node : nexus.GetNodes()) + { + VerifyOrQuit(node.Get().GetNumberOfPeers() == 5); + + for (const ot::Trel::Peer &peer : node.Get()) + { + bool found = false; + + VerifyOrQuit(peer.IsStateValid()); + VerifyOrQuit(peer.GetExtPanId() == node.Get().GetExtPanId()); + + for (Node &otherNode : nexus.GetNodes()) + { + if (&otherNode == &node) + { + continue; + } + + if (peer.GetExtAddress() == otherNode.Get().GetExtAddress()) + { + Ip6::SockAddr otherSockAddr; + + found = true; + otherNode.GetTrelSockAddr(otherSockAddr); + VerifyOrQuit(peer.GetSockAddr() == otherSockAddr); + + VerifyOrQuit(peer.GetServiceName() != nullptr); + VerifyOrQuit( + StringMatch(peer.GetServiceName(), otherNode.Get().GetServiceName())); + + VerifyOrQuit(peer.GetHostName() != nullptr); + VerifyOrQuit(StringStartsWith(peer.GetHostName(), "ot")); + VerifyOrQuit(StringEndsWith(peer.GetHostName(), + otherNode.Get().GetExtAddress().ToString().AsCString())); + + VerifyOrQuit(peer.GetHostAddresses().GetLength() == 1); + VerifyOrQuit(peer.GetHostAddresses()[0] == otherSockAddr.GetAddress()); + break; + } + } + + VerifyOrQuit(found); + + // Check the format of the TREL service name + VerifyOrQuit(StringStartsWith(node.Get().GetServiceName(), "otTREL")); + VerifyOrQuit(StringEndsWith(node.Get().GetServiceName(), + node.Get().GetExtAddress().ToString().AsCString())); + } + } +} + +void TestTrelDelayedMdnsStartAndPeerRemovalDelay(void) +{ + Core nexus; + Node &node1 = nexus.CreateNode(); + Node &node2 = nexus.CreateNode(); + const ot::Trel::Peer *peer; + + Log("---------------------------------------------------------------------------------------"); + Log("TestTrelDelayedMdnsStartAndPeerRemovalDelay()"); + + nexus.AdvanceTime(0); + + for (Node &node : nexus.GetNodes()) + { + node.GetInstance().SetLogLevel(kLogLevelWarn); + VerifyOrQuit(!node.Get().IsEnabled()); + } + + // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + Log("Start first network on node1"); + + SuccessOrQuit(node1.Get().SetEnabled(true, kInfraIfIndex)); + node1.Form(); + nexus.AdvanceTime(45 * 1000); + VerifyOrQuit(node1.Get().IsLeader()); + + // Check that `node1` did not discover any `TREL` peer, + // as `mDNS` is not yet enabled on other nodes. + // Additionally, the `TREL` peer table must exclude the device itself. + + VerifyOrQuit(node1.Get().IsEmpty()); + + // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + Log("Enable mDNS on `node2`, form a new network"); + + SuccessOrQuit(node2.Get().SetEnabled(true, kInfraIfIndex)); + node2.Form(); + nexus.AdvanceTime(45 * 1000); + VerifyOrQuit(node1.Get().IsLeader()); + + // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + Log("Validate that peer tables are correctly updated on both `node1` and `node2`"); + + VerifyOrQuit(node1.Get().GetNumberOfPeers() == 1); + VerifyOrQuit(node2.Get().GetNumberOfPeers() == 1); + + // Check peer on `node1` to match `node2` info. + peer = node1.Get().GetHead(); + VerifyOrQuit(peer != nullptr); + + VerifyOrQuit(peer->IsStateValid()); + VerifyOrQuit(peer->GetExtPanId() == node2.Get().GetExtPanId()); + VerifyOrQuit(peer->GetExtAddress() == node2.Get().GetExtAddress()); + VerifyOrQuit(peer->GetServiceName() != nullptr); + VerifyOrQuit(StringMatch(peer->GetServiceName(), node2.Get().GetServiceName())); + VerifyOrQuit(peer->GetHostName() != nullptr); + VerifyOrQuit(StringStartsWith(peer->GetHostName(), "ot")); + VerifyOrQuit(StringEndsWith(peer->GetHostName(), node2.Get().GetExtAddress().ToString().AsCString())); + VerifyOrQuit(peer->GetSockAddr().GetPort() == node2.mTrel.mUdpPort); + VerifyOrQuit(peer->GetSockAddr().GetAddress() == node2.mMdns.mIfAddresses[0]); + VerifyOrQuit(peer->GetHostAddresses().GetLength() == 1); + VerifyOrQuit(peer->GetHostAddresses()[0] == node2.mMdns.mIfAddresses[0]); + VerifyOrQuit(peer->GetNext() == nullptr); + + // Check peer on `node2` to match `node1` info. + peer = node2.Get().GetHead(); + VerifyOrQuit(peer != nullptr); + + VerifyOrQuit(peer->IsStateValid()); + VerifyOrQuit(peer->GetExtPanId() == node1.Get().GetExtPanId()); + VerifyOrQuit(peer->GetExtAddress() == node1.Get().GetExtAddress()); + VerifyOrQuit(peer->GetServiceName() != nullptr); + VerifyOrQuit(StringMatch(peer->GetServiceName(), node1.Get().GetServiceName())); + VerifyOrQuit(peer->GetHostName() != nullptr); + VerifyOrQuit(StringStartsWith(peer->GetHostName(), "ot")); + VerifyOrQuit(StringEndsWith(peer->GetHostName(), node1.Get().GetExtAddress().ToString().AsCString())); + VerifyOrQuit(peer->GetSockAddr().GetPort() == node1.mTrel.mUdpPort); + VerifyOrQuit(peer->GetSockAddr().GetAddress() == node1.mMdns.mIfAddresses[0]); + VerifyOrQuit(peer->GetHostAddresses().GetLength() == 1); + VerifyOrQuit(peer->GetHostAddresses()[0] == node1.mMdns.mIfAddresses[0]); + + // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + Log("Disable TREL Interface (and `PeerDiscoverer`) on `node2`"); + + node2.Get().Disable(); + nexus.AdvanceTime(2 * 1000); + + VerifyOrQuit(node2.Get().GetNumberOfPeers() == 0); + + // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + Log("Validate that `PeerTable` is properly updated on `node1`"); + + // Check peer on `node1` is still present but not longer `IsStateValid()`. + peer = node1.Get().GetHead(); + VerifyOrQuit(peer != nullptr); + + VerifyOrQuit(!peer->IsStateValid()); + VerifyOrQuit(peer->GetExtPanId() == node2.Get().GetExtPanId()); + VerifyOrQuit(peer->GetExtAddress() == node2.Get().GetExtAddress()); + VerifyOrQuit(peer->GetServiceName() != nullptr); + VerifyOrQuit(StringMatch(peer->GetServiceName(), node2.Get().GetServiceName())); + VerifyOrQuit(peer->GetHostName() == nullptr); + VerifyOrQuit(peer->GetHostAddresses().GetLength() == 0); + VerifyOrQuit(peer->GetNext() == nullptr); + + // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + Log("Re-enable TREL Interface (and `PeerDiscoverer`) on `node2`"); + + node2.Get().Enable(); + nexus.AdvanceTime(15 * 1000); + + // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + Log("Validate that peer tables again updated on both nodes"); + + VerifyOrQuit(node1.Get().GetNumberOfPeers() == 1); + VerifyOrQuit(node2.Get().GetNumberOfPeers() == 1); + + // Check peer on `node1` to match `node2` info. + peer = node1.Get().GetHead(); + VerifyOrQuit(peer != nullptr); + + VerifyOrQuit(peer->IsStateValid()); + VerifyOrQuit(peer->GetExtPanId() == node2.Get().GetExtPanId()); + VerifyOrQuit(peer->GetExtAddress() == node2.Get().GetExtAddress()); + VerifyOrQuit(peer->GetServiceName() != nullptr); + VerifyOrQuit(StringMatch(peer->GetServiceName(), node2.Get().GetServiceName())); + VerifyOrQuit(peer->GetHostName() != nullptr); + VerifyOrQuit(StringStartsWith(peer->GetHostName(), "ot")); + VerifyOrQuit(StringEndsWith(peer->GetHostName(), node2.Get().GetExtAddress().ToString().AsCString())); + VerifyOrQuit(peer->GetSockAddr().GetPort() == node2.mTrel.mUdpPort); + VerifyOrQuit(peer->GetSockAddr().GetAddress() == node2.mMdns.mIfAddresses[0]); + VerifyOrQuit(peer->GetHostAddresses().GetLength() == 1); + VerifyOrQuit(peer->GetHostAddresses()[0] == node2.mMdns.mIfAddresses[0]); + VerifyOrQuit(peer->GetNext() == nullptr); + + // Check peer on `node2` to match `node1` info. + peer = node2.Get().GetHead(); + VerifyOrQuit(peer != nullptr); + + VerifyOrQuit(peer->IsStateValid()); + VerifyOrQuit(peer->GetExtPanId() == node1.Get().GetExtPanId()); + VerifyOrQuit(peer->GetExtAddress() == node1.Get().GetExtAddress()); + VerifyOrQuit(peer->GetServiceName() != nullptr); + VerifyOrQuit(StringMatch(peer->GetServiceName(), node1.Get().GetServiceName())); + VerifyOrQuit(peer->GetHostName() != nullptr); + VerifyOrQuit(StringStartsWith(peer->GetHostName(), "ot")); + VerifyOrQuit(StringEndsWith(peer->GetHostName(), node1.Get().GetExtAddress().ToString().AsCString())); + VerifyOrQuit(peer->GetSockAddr().GetPort() == node1.mTrel.mUdpPort); + VerifyOrQuit(peer->GetSockAddr().GetAddress() == node1.mMdns.mIfAddresses[0]); + VerifyOrQuit(peer->GetHostAddresses().GetLength() == 1); + VerifyOrQuit(peer->GetHostAddresses()[0] == node1.mMdns.mIfAddresses[0]); + + // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + Log("Disable TREL Interface (and `PeerDiscoverer`) on `node2` again"); + + node2.Get().Disable(); + + Log("Wait for long enough for the `node2` peer to be fully deleted on `node1`"); + + nexus.AdvanceTime(15 * 1000); + + VerifyOrQuit(node1.Get().IsEmpty()); + VerifyOrQuit(node2.Get().IsEmpty()); +} + +void TestServiceNameConflict(void) +{ + Core nexus; + Node &node1 = nexus.CreateNode(); + Node &node2 = nexus.CreateNode(); + Node &conflictNode = nexus.CreateNode(); + Dns::Multicast::Core::Service service; + Dns::Name::Buffer conflictName; + bool peersValidated; + + Log("---------------------------------------------------------------------------------------"); + Log("TestServiceNameConflict()"); + + nexus.AdvanceTime(0); + + for (Node &node : nexus.GetNodes()) + { + node.GetInstance().SetLogLevel(kLogLevelWarn); + VerifyOrQuit(!node.Get().IsEnabled()); + } + + // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + Log("Disable TREL interface but enable mDNS on `conflictNode`"); + + conflictNode.Get().Disable(); + SuccessOrQuit(conflictNode.Get().SetEnabled(true, kInfraIfIndex)); + + Log("Register a service on `conflictNode` with same name that `node1` would use"); + + SuccessOrQuit(StringCopy(conflictName, node1.Get().GetServiceName())); + + ClearAllBytes(service); + service.mServiceType = "_trel._udp"; + service.mServiceInstance = conflictName; + service.mPort = 12345; + + SuccessOrQuit(conflictNode.Get().RegisterService(service, /* aRequestId */ 0, nullptr)); + + nexus.AdvanceTime(15 * 1000); + + // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + Log("Enable mDNS on `node1` and `node2` and form a new network"); + + SuccessOrQuit(node1.Get().SetEnabled(true, kInfraIfIndex)); + SuccessOrQuit(node2.Get().SetEnabled(true, kInfraIfIndex)); + + node1.Form(); + nexus.AdvanceTime(45 * 1000); + VerifyOrQuit(node1.Get().IsLeader()); + + node2.Join(node1, Node::kAsFed); + nexus.AdvanceTime(15 * 1000); + VerifyOrQuit(node2.Get().IsChild()); + + // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + Log("Validate that `node1` correctly detected the name conflict and appended a (1) to its service name"); + + VerifyOrQuit(StringStartsWith(node1.Get().GetServiceName(), conflictName)); + VerifyOrQuit(StringEndsWith(node1.Get().GetServiceName(), "(1)")); + + Log("Check peer table on `node2` to match `node1` info"); + + peersValidated = false; + + for (const ot::Trel::Peer &peer : node2.Get()) + { + if (peer.IsStateValid()) + { + VerifyOrQuit(peer.GetExtPanId() == node1.Get().GetExtPanId()); + VerifyOrQuit(peer.GetExtAddress() == node1.Get().GetExtAddress()); + VerifyOrQuit(peer.GetServiceName() != nullptr); + VerifyOrQuit(StringMatch(peer.GetServiceName(), node1.Get().GetServiceName())); + peersValidated = true; + break; + } + } + + VerifyOrQuit(peersValidated); +} + +void TestHostAddressChange(void) +{ + Core nexus; + Node &node1 = nexus.CreateNode(); + Node &node2 = nexus.CreateNode(); + const ot::Trel::Peer *peer; + Dns::Multicast::Core::Service service; + uint8_t txtData[kMaxTxtDataSize]; + Dns::TxtDataEncoder encoder(txtData, sizeof(txtData)); + Ip6::Address linkLocalAddr; + Ip6::Address guaAddr; + Ip6::Address ulaAddr; + + Log("---------------------------------------------------------------------------------------"); + Log("TestHostAddressChange()"); + + nexus.AdvanceTime(0); + + for (Node &node : nexus.GetNodes()) + { + node.GetInstance().SetLogLevel(kLogLevelWarn); + VerifyOrQuit(!node.Get().IsEnabled()); + } + + // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + Log("Disable TREL interface but enable mDNS on `node2`"); + + node2.Get().Disable(); + SuccessOrQuit(node2.Get().SetEnabled(true, kInfraIfIndex)); + + Log("Manually register a TREL service on `node2` with proper TXT data"); + + SuccessOrQuit(encoder.AppendEntry("xa", node2.Get().GetExtAddress())); + SuccessOrQuit(encoder.AppendEntry("xp", node2.Get().GetExtPanId())); + + ClearAllBytes(service); + service.mServiceType = "_trel._udp"; + service.mServiceInstance = "node2"; + service.mTxtData = txtData; + service.mTxtDataLength = encoder.GetLength(); + service.mPort = 3333; + + SuccessOrQuit(node2.Get().RegisterService(service, /* aRequestId */ 0, nullptr)); + + nexus.AdvanceTime(15 * 1000); + + // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + Log("Form a new network on `node1`"); + + SuccessOrQuit(node1.Get().SetEnabled(true, kInfraIfIndex)); + node1.Form(); + nexus.AdvanceTime(45 * 1000); + VerifyOrQuit(node1.Get().IsLeader()); + + // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + Log("Validate peer table on `node1` and `node2` is discovered properly"); + + peer = node1.Get().GetHead(); + VerifyOrQuit(peer != nullptr); + + VerifyOrQuit(peer->IsStateValid()); + VerifyOrQuit(peer->GetExtPanId() == node2.Get().GetExtPanId()); + VerifyOrQuit(peer->GetExtAddress() == node2.Get().GetExtAddress()); + + VerifyOrQuit(peer->GetServiceName() != nullptr); + VerifyOrQuit(StringMatch(peer->GetServiceName(), service.mServiceInstance)); + VerifyOrQuit(peer->GetHostName() != nullptr); + VerifyOrQuit(StringStartsWith(peer->GetHostName(), "ot")); + VerifyOrQuit(StringEndsWith(peer->GetHostName(), node2.Get().GetExtAddress().ToString().AsCString())); + VerifyOrQuit(peer->GetSockAddr().GetPort() == service.mPort); + + VerifyOrQuit(peer->GetSockAddr().GetAddress() == node2.mMdns.mIfAddresses[0]); + VerifyOrQuit(peer->GetHostAddresses().GetLength() == 1); + VerifyOrQuit(peer->GetHostAddresses()[0] == node2.mMdns.mIfAddresses[0]); + + VerifyOrQuit(peer->GetNext() == nullptr); + + // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + Log("Update the advertised local host addresses on `node2`"); + + node2.mMdns.mIfAddresses.Clear(); + + SuccessOrQuit(guaAddr.FromString("2001:cafe::4567")); + SuccessOrQuit(node2.mMdns.mIfAddresses.PushBack(guaAddr)); + + SuccessOrQuit(ulaAddr.FromString("fd00:abba::1234")); + SuccessOrQuit(node2.mMdns.mIfAddresses.PushBack(ulaAddr)); + + SuccessOrQuit(linkLocalAddr.FromString("fe80::bd2c:a124")); + SuccessOrQuit(node2.mMdns.mIfAddresses.PushBack(linkLocalAddr)); + + node2.mMdns.SignalIfAddresses(node2.GetInstance()); + + nexus.AdvanceTime(3 * 1000); + + // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + Log("Validate peer table on `node1` discovers all the new addresses"); + + peer = node1.Get().GetHead(); + VerifyOrQuit(peer != nullptr); + + VerifyOrQuit(peer->IsStateValid()); + VerifyOrQuit(peer->GetExtPanId() == node2.Get().GetExtPanId()); + VerifyOrQuit(peer->GetExtAddress() == node2.Get().GetExtAddress()); + + VerifyOrQuit(peer->GetServiceName() != nullptr); + VerifyOrQuit(StringMatch(peer->GetServiceName(), service.mServiceInstance)); + + VerifyOrQuit(peer->GetHostName() != nullptr); + VerifyOrQuit(StringStartsWith(peer->GetHostName(), "ot")); + VerifyOrQuit(StringEndsWith(peer->GetHostName(), node2.Get().GetExtAddress().ToString().AsCString())); + VerifyOrQuit(peer->GetSockAddr().GetPort() == service.mPort); + + VerifyOrQuit(peer->GetHostAddresses().GetLength() == 3); + + for (const Ip6::Address &hostAddress : peer->GetHostAddresses()) + { + VerifyOrQuit(node2.mMdns.mIfAddresses.Contains(hostAddress)); + } + + for (const Ip6::Address &ifAddress : node2.mMdns.mIfAddresses) + { + VerifyOrQuit(peer->GetHostAddresses().Contains(ifAddress)); + } + + // Check the order of addresses in list: + + VerifyOrQuit(peer->GetHostAddresses()[0] == linkLocalAddr); + VerifyOrQuit(peer->GetHostAddresses()[1] == guaAddr); + VerifyOrQuit(peer->GetHostAddresses()[2] == ulaAddr); + + Log("Validate the peer `SockAddr` is correctly updated based on new discovered host addresses"); + + VerifyOrQuit(peer->GetSockAddr().GetAddress() == linkLocalAddr); + VerifyOrQuit(peer->GetSockAddr().GetPort() == service.mPort); + + VerifyOrQuit(peer->GetNext() == nullptr); +} + +void TestMultiServiceSameHost(void) +{ + Core nexus; + Node &node = nexus.CreateNode(); + Node &multiServiceNode = nexus.CreateNode(); + const ot::Trel::Peer *peer; + Dns::Multicast::Core::Service services[3]; + uint8_t txtData[kMaxTxtDataSize]; + Ip6::Address address; + + Log("---------------------------------------------------------------------------------------"); + Log("TestMultiServiceSameHost()"); + + nexus.AdvanceTime(0); + + for (Node &node : nexus.GetNodes()) + { + node.GetInstance().SetLogLevel(kLogLevelInfo); + VerifyOrQuit(!node.Get().IsEnabled()); + } + + // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + Log("Disable TREL interface but enable mDNS on `multiServiceNode`"); + + multiServiceNode.Get().Disable(); + SuccessOrQuit(multiServiceNode.Get().SetEnabled(true, kInfraIfIndex)); + + Log("Manually register three TREL services on the `multiServiceNode`"); + + { + Dns::TxtDataEncoder encoder(txtData, sizeof(txtData)); + + SuccessOrQuit(encoder.AppendEntry("xa", "0102030405060708")); + SuccessOrQuit(encoder.AppendEntry("xp", "0102030405060708")); + ClearAllBytes(services[0]); + services[0].mServiceType = "_trel._udp"; + services[0].mServiceInstance = "service0"; + services[0].mTxtData = txtData; + services[0].mTxtDataLength = encoder.GetLength(); + services[0].mPort = 11111; + } + + SuccessOrQuit( + multiServiceNode.Get().RegisterService(services[0], /* aRequestId */ 0, nullptr)); + + { + Dns::TxtDataEncoder encoder(txtData, sizeof(txtData)); + SuccessOrQuit(encoder.AppendEntry("xa", "1122334455667788")); + SuccessOrQuit(encoder.AppendEntry("xp", "1122334455667788")); + ClearAllBytes(services[1]); + services[1].mServiceType = "_trel._udp"; + services[1].mServiceInstance = "service1"; + services[1].mTxtData = txtData; + services[1].mTxtDataLength = encoder.GetLength(); + services[1].mPort = 2222; + } + + SuccessOrQuit( + multiServiceNode.Get().RegisterService(services[1], /* aRequestId */ 0, nullptr)); + + { + Dns::TxtDataEncoder encoder(txtData, sizeof(txtData)); + + SuccessOrQuit(encoder.AppendEntry("xa", "1020304050607080")); + SuccessOrQuit(encoder.AppendEntry("xp", "1020304050607080")); + ClearAllBytes(services[2]); + services[2].mServiceType = "_trel._udp"; + services[2].mServiceInstance = "service2"; + services[2].mTxtData = txtData; + services[2].mTxtDataLength = encoder.GetLength(); + services[2].mPort = 3333; + } + + SuccessOrQuit( + multiServiceNode.Get().RegisterService(services[2], /* aRequestId */ 0, nullptr)); + + nexus.AdvanceTime(15 * 1000); + + // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + Log("Form a new network on `node`"); + + SuccessOrQuit(node.Get().SetEnabled(true, kInfraIfIndex)); + node.Form(); + nexus.AdvanceTime(45 * 1000); + VerifyOrQuit(node.Get().IsLeader()); + + // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + Log("Validate peer table on `node` and all services are discovered from the same host"); + + VerifyOrQuit(node.Get().GetNumberOfPeers() == 3); + + for (const ot::Trel::Peer &peer : node.Get()) + { + bool found = false; + + VerifyOrQuit(peer.IsStateValid()); + VerifyOrQuit(peer.GetServiceName() != nullptr); + VerifyOrQuit(peer.GetHostName() != nullptr); + VerifyOrQuit(StringStartsWith(peer.GetHostName(), "ot")); + VerifyOrQuit(StringEndsWith(peer.GetHostName(), + multiServiceNode.Get().GetExtAddress().ToString().AsCString())); + + VerifyOrQuit(peer.GetSockAddr().GetAddress() == multiServiceNode.mMdns.mIfAddresses[0]); + VerifyOrQuit(peer.GetHostAddresses().GetLength() == 1); + VerifyOrQuit(peer.GetHostAddresses()[0] == multiServiceNode.mMdns.mIfAddresses[0]); + + for (const Dns::Multicast::Core::Service &service : services) + { + if (StringMatch(peer.GetServiceName(), service.mServiceInstance)) + { + found = true; + } + } + + VerifyOrQuit(found); + } + + // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + Log("Unregister the third service on `multiServiceNode`"); + + SuccessOrQuit(multiServiceNode.Get().UnregisterService(services[2])); + + nexus.AdvanceTime(15 * 1000); + + // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + Log("Validate peer table on `node`"); + + VerifyOrQuit(node.Get().GetNumberOfPeers() == 2); + + for (const ot::Trel::Peer &peer : node.Get()) + { + bool found = false; + + if (!peer.IsStateValid()) + { + continue; + } + + VerifyOrQuit(peer.GetServiceName() != nullptr); + VerifyOrQuit(peer.GetHostName() != nullptr); + VerifyOrQuit(StringStartsWith(peer.GetHostName(), "ot")); + VerifyOrQuit(StringEndsWith(peer.GetHostName(), + multiServiceNode.Get().GetExtAddress().ToString().AsCString())); + + VerifyOrQuit(peer.GetSockAddr().GetAddress() == multiServiceNode.mMdns.mIfAddresses[0]); + VerifyOrQuit(peer.GetHostAddresses().GetLength() == 1); + VerifyOrQuit(peer.GetHostAddresses()[0] == multiServiceNode.mMdns.mIfAddresses[0]); + + for (uint16_t index = 0; index < 2; index++) + { + if (StringMatch(peer.GetServiceName(), services[index].mServiceInstance)) + { + found = true; + } + } + + VerifyOrQuit(found); + } + + // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + Log("Update the local host addresses on `multiServiceNode`"); + + SuccessOrQuit(address.FromString("fd00:abba::1234")); + SuccessOrQuit(multiServiceNode.mMdns.mIfAddresses.PushBack(address)); + + multiServiceNode.mMdns.SignalIfAddresses(multiServiceNode.GetInstance()); + + nexus.AdvanceTime(5 * 1000); + + // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + Log("Validate all peers get the updated list"); + + VerifyOrQuit(node.Get().GetNumberOfPeers() == 2); + + for (const ot::Trel::Peer &peer : node.Get()) + { + bool found = false; + + if (!peer.IsStateValid()) + { + continue; + } + + VerifyOrQuit(peer.GetServiceName() != nullptr); + VerifyOrQuit(peer.GetHostName() != nullptr); + VerifyOrQuit(StringStartsWith(peer.GetHostName(), "ot")); + VerifyOrQuit(StringEndsWith(peer.GetHostName(), + multiServiceNode.Get().GetExtAddress().ToString().AsCString())); + + VerifyOrQuit(peer.GetSockAddr().GetAddress() == multiServiceNode.mMdns.mIfAddresses[0]); + VerifyOrQuit(peer.GetHostAddresses().GetLength() == 2); + VerifyOrQuit(peer.GetHostAddresses()[0] == multiServiceNode.mMdns.mIfAddresses[0]); + VerifyOrQuit(peer.GetHostAddresses()[1] == multiServiceNode.mMdns.mIfAddresses[1]); + + for (uint16_t index = 0; index < 2; index++) + { + if (StringMatch(peer.GetServiceName(), services[index].mServiceInstance)) + { + found = true; + } + } + + VerifyOrQuit(found); + } +} + +#endif // OPENTHREAD_CONFIG_RADIO_LINK_TREL_ENABLE + +} // namespace Nexus +} // namespace ot + +int main(void) +{ +#if OPENTHREAD_CONFIG_RADIO_LINK_TREL_ENABLE + ot::Nexus::TestTrelBasic(); + ot::Nexus::TestTrelDelayedMdnsStartAndPeerRemovalDelay(); + ot::Nexus::TestServiceNameConflict(); + ot::Nexus::TestHostAddressChange(); + ot::Nexus::TestMultiServiceSameHost(); + printf("All tests passed\n"); +#else + printf("TREL is not enabled - test skipped\n"); +#endif + + return 0; +} diff --git a/tests/toranj/openthread-core-toranj-config-posix.h b/tests/toranj/openthread-core-toranj-config-posix.h index a61047805..f7b968352 100644 --- a/tests/toranj/openthread-core-toranj-config-posix.h +++ b/tests/toranj/openthread-core-toranj-config-posix.h @@ -46,6 +46,8 @@ #define OPENTHREAD_CONFIG_PLATFORM_DNSSD_ENABLE 0 +#define OPENTHREAD_CONFIG_TREL_MANAGE_DNSSD_ENABLE 1 + #ifdef __linux__ #define OPENTHREAD_CONFIG_BACKBONE_ROUTER_ENABLE 1 #endif @@ -66,12 +68,12 @@ #define OPENTHREAD_POSIX_CONFIG_NAT64_AIL_PREFIX_ENABLE 1 -// The following two features (`USE_HEAP`) are disabled explicitly on posix `toranj` +// The following two features (`USE_HEAP`) are set explicitly on posix `toranj` // to validate the build with these config. The `toranj` build under simulation // platform covers the opposite configs (allows `USE_HEAP`). #define OPENTHREAD_CONFIG_BORDER_ROUTING_USE_HEAP_ENABLE 0 -#define OPENTHREAD_CONFIG_TREL_USE_HEAP_ENABLE 0 +#define OPENTHREAD_CONFIG_TREL_USE_HEAP_ENABLE 1 #endif /* OPENTHREAD_CORE_TORANJ_CONFIG_POSIX_H_ */ diff --git a/tests/toranj/openthread-core-toranj-config-simulation.h b/tests/toranj/openthread-core-toranj-config-simulation.h index cdb4f3e6a..4071198f2 100644 --- a/tests/toranj/openthread-core-toranj-config-simulation.h +++ b/tests/toranj/openthread-core-toranj-config-simulation.h @@ -87,6 +87,8 @@ #define OPENTHREAD_CONFIG_RADIO_STATS_ENABLE 0 -#define OPENTHREAD_CONFIG_TREL_USE_HEAP_ENABLE 1 +#define OPENTHREAD_CONFIG_TREL_MANAGE_DNSSD_ENABLE 0 + +#define OPENTHREAD_CONFIG_TREL_USE_HEAP_ENABLE 0 #endif /* OPENTHREAD_CORE_TORANJ_CONFIG_SIMULATION_H_ */