From e4479fb6b1f6df883e2a06520c3d13db75f331d6 Mon Sep 17 00:00:00 2001 From: Esko Dijk Date: Wed, 29 Oct 2025 16:29:09 +0100 Subject: [PATCH] [tcat] add TCAT_ENABLE.req TMF command (#12013) This adds support for the TMF command to enable TCAT remotely. A test is added that uses the 'UDP send' mechanism to send the new TMF command to a target node. Some fixes/additions to the test framework are made to support the new test, including a new argument for udp_send() to send a specific byte array and udp_rx() to receive data by a UDP client on a node. --- include/openthread/dataset.h | 1 + include/openthread/instance.h | 2 +- include/openthread/tcat.h | 1 + src/core/meshcop/meshcop_tlvs.hpp | 6 + src/core/meshcop/tcat_agent.cpp | 117 ++++++++++++--- src/core/meshcop/tcat_agent.hpp | 29 ++-- src/core/thread/tmf.cpp | 3 + src/core/thread/uri_paths.cpp | 4 + src/core/thread/uri_paths.hpp | 2 + tests/scripts/thread-cert/node.py | 31 +++- .../thread-cert/test_tcat_tmf_enable_req.py | 139 ++++++++++++++++++ 11 files changed, 301 insertions(+), 34 deletions(-) create mode 100755 tests/scripts/thread-cert/test_tcat_tmf_enable_req.py diff --git a/include/openthread/dataset.h b/include/openthread/dataset.h index 807241ca8..6dbd9e68b 100644 --- a/include/openthread/dataset.h +++ b/include/openthread/dataset.h @@ -286,6 +286,7 @@ typedef enum otMeshcopTlvType OT_MESHCOP_TLV_JOINER_IID = 19, ///< meshcop Joiner IID TLV OT_MESHCOP_TLV_JOINER_RLOC = 20, ///< meshcop Joiner Router Locator TLV OT_MESHCOP_TLV_JOINER_ROUTER_KEK = 21, ///< meshcop Joiner Router KEK TLV + OT_MESHCOP_TLV_DURATION = 23, ///< meshcop Duration TLV OT_MESHCOP_TLV_PROVISIONING_URL = 32, ///< meshcop Provisioning URL TLV OT_MESHCOP_TLV_VENDOR_NAME_TLV = 33, ///< meshcop Vendor Name TLV OT_MESHCOP_TLV_VENDOR_MODEL_TLV = 34, ///< meshcop Vendor Model TLV diff --git a/include/openthread/instance.h b/include/openthread/instance.h index 23fe89621..dfdac71a3 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 (546) +#define OPENTHREAD_API_VERSION (547) /** * @addtogroup api-instance diff --git a/include/openthread/tcat.h b/include/openthread/tcat.h index e9af0f450..f0de079e9 100644 --- a/include/openthread/tcat.h +++ b/include/openthread/tcat.h @@ -73,6 +73,7 @@ extern "C" { #define OT_TCAT_OPCODE 0x2 ///< TCAT Advertisement Operation Code. #define OT_TCAT_MAX_ADVERTISED_DEVICEID_SIZE 5 ///< TCAT max size of any type of advertised Device ID. #define OT_TCAT_MAX_DEVICEID_SIZE 64 ///< TCAT max size of device ID. +#define OT_TCAT_ENABLE_MAX 600 ///< TCAT_ENABLE_MAX, default max TMF TCAT enable time, in seconds. /** * Represents TCAT status code. diff --git a/src/core/meshcop/meshcop_tlvs.hpp b/src/core/meshcop/meshcop_tlvs.hpp index 8bd56484d..06e2f1a7b 100644 --- a/src/core/meshcop/meshcop_tlvs.hpp +++ b/src/core/meshcop/meshcop_tlvs.hpp @@ -94,6 +94,7 @@ public: kJoinerIid = OT_MESHCOP_TLV_JOINER_IID, ///< Joiner IID TLV kJoinerRouterLocator = OT_MESHCOP_TLV_JOINER_RLOC, ///< Joiner Router Locator TLV kJoinerRouterKek = OT_MESHCOP_TLV_JOINER_ROUTER_KEK, ///< Joiner Router KEK TLV + kDuration = OT_MESHCOP_TLV_DURATION, ///< Duration TLV kProvisioningUrl = OT_MESHCOP_TLV_PROVISIONING_URL, ///< Provisioning URL TLV kVendorName = OT_MESHCOP_TLV_VENDOR_NAME_TLV, ///< meshcop Vendor Name TLV kVendorModel = OT_MESHCOP_TLV_VENDOR_MODEL_TLV, ///< meshcop Vendor Model TLV @@ -205,6 +206,11 @@ typedef UintTlvInfo JoinerRouterLocatorTlv; */ typedef SimpleTlvInfo JoinerRouterKekTlv; +/** + * Defines Duration TLV constants and types. + */ +typedef UintTlvInfo DurationTlv; + /** * Defines Count TLV constants and types. */ diff --git a/src/core/meshcop/tcat_agent.cpp b/src/core/meshcop/tcat_agent.cpp index 21b819a3b..bcc184530 100644 --- a/src/core/meshcop/tcat_agent.cpp +++ b/src/core/meshcop/tcat_agent.cpp @@ -60,6 +60,7 @@ TcatAgent::TcatAgent(Instance &aInstance) , mVendorInfo(nullptr) , mState(kStateDisabled) , mNextState(kStateDisabled) + , mTimerSetsToActive(false) , mActiveOrStandbyTimer(aInstance) , mTcatActiveDurationMs(0) { @@ -95,7 +96,6 @@ Error TcatAgent::Start(AppDataReceiveCallback aAppDataReceiveCallback, JoinCallb mState = kStateActive; mNextState = kStateActive; mTcatActiveDurationMs = 0; - mActiveOrStandbyTimer.Stop(); LogInfo("Start"); exit: @@ -109,6 +109,7 @@ void TcatAgent::Stop(void) mAppDataReceiveCallback.Clear(); mJoinCallback.Clear(); mState = kStateDisabled; + mActiveOrStandbyTimer.Stop(); ClearCommissionerState(); LogInfo("Stop"); } @@ -122,7 +123,7 @@ Error TcatAgent::Standby(void) mTcatActiveDurationMs = 0; mActiveOrStandbyTimer.Stop(); mNextState = kStateStandby; - if (!IsConnected()) + if (!IsConnected() && mState != kStateStandby) { // if already TLS-connected, only move to 'standby' once the connection is done. // if not yet fully connected, go to 'standby' immediately (ignoring a TLS handshake that may be ongoing) @@ -141,14 +142,17 @@ Error TcatAgent::Activate(const uint32_t aDelayMs, const uint32_t aDurationMs) Error error = kErrorNone; VerifyOrExit(IsStarted(), error = kErrorInvalidState); + VerifyOrExit(mState != kStateActive); mTcatActiveDurationMs = aDurationMs; + mTimerSetsToActive = true; if (aDelayMs > 0) { mActiveOrStandbyTimer.Start(aDelayMs); } else { + mActiveOrStandbyTimer.Stop(); HandleTimer(); } @@ -219,8 +223,7 @@ Error TcatAgent::Connected(MeshCoP::Tls::Extension &aTls) } // A temporary enablement stops after disconnect: to standby. - // For others, return to prior state, upon disconnect. - mNextState = (mState == kStateActiveTemporary) ? kStateStandby : mState; + mNextState = (mState == kStateActiveTemporary) ? kStateStandby : kStateActive; mState = kStateConnected; NotifyStateChange(); LogInfo("Connected"); @@ -237,8 +240,8 @@ void TcatAgent::Disconnected(void) if (mState != kStateDisabled) { mState = mNextState; - NotifyStateChange(); LogInfo("Disconnected"); + NotifyStateChange(); ClearCommissionerState(); } } @@ -1071,28 +1074,30 @@ void TcatAgent::HandleTimer(void) { case kStateStandby: case kStateStandbyTemporary: - if (mTcatActiveDurationMs > 0) + case kStateActiveTemporary: + if (mTimerSetsToActive) { - mActiveOrStandbyTimer.Start(mTcatActiveDurationMs); - mState = kStateActiveTemporary; + mTimerSetsToActive = false; + if (mTcatActiveDurationMs > 0) + { + mState = kStateActiveTemporary; + mActiveOrStandbyTimer.Start(mTcatActiveDurationMs); + } + else + { + mState = kStateActive; + } + NotifyStateChange(); + LogInfo("Active"); } else { - mState = kStateActive; + IgnoreError(Standby()); } - NotifyStateChange(); - LogInfo("Active"); - break; - - case kStateActiveTemporary: - IgnoreError(Standby()); - break; - - case kStateConnected: - mNextState = (mTcatActiveDurationMs > 0) ? kStateStandby : kStateActive; break; // kStateActive: will not go to standby, based on timer. Application has forced it to 'active'. + // kStateConnected: no change here, mNextState already set. default: break; } @@ -1105,6 +1110,80 @@ void TcatAgent::NotifyStateChange(void) mState == kStateConnected); } +template <> void TcatAgent::HandleTmf(Coap::Message &aMessage, const Ip6::MessageInfo &aMessageInfo) +{ + Error error = kErrorNone; + Coap::Message *message = nullptr; + uint32_t delayTimerMs = 0; + uint16_t durationSec = 0; + uint32_t durationMs; + + VerifyOrExit(aMessage.IsConfirmablePostRequest()); + LogInfo("Received %s from %s", UriToString(), aMessageInfo.GetPeerAddr().ToString().AsCString()); + message = Get().NewResponseMessage(aMessage); + VerifyOrExit(message != nullptr, error = kErrorNoBufs); + + SuccessOrExit(error = Tlv::Find(aMessage, delayTimerMs)); + switch (Tlv::Find(aMessage, durationSec)) + { + case kErrorNone: + break; + case kErrorNotFound: + durationSec = kTcatTmfEnableDefaultSec; // If Duration TLV absent: use default duration + break; + default: + ExitNow(error = kErrorParse); + } + durationMs = Time::SecToMsec(durationSec); + + // if an existing activation is ongoing, adapt the requested one to be compatible. + AdaptToExistingActivePeriod(delayTimerMs, durationMs); + + error = Activate(delayTimerMs, durationMs); + +exit: + if (message != nullptr) + { + error = + Tlv::Append(*message, error == kErrorNone ? StateTlv::State::kAccept : StateTlv::State::kReject); + if (error == kErrorNone) + { + error = Get().SendMessage(*message, aMessageInfo); + } + FreeMessageOnError(message, error); + } + LogWarnOnError(error, "send TCAT_ENABLE.rsp"); +} + +// Adapts delay/duration parameters of the given TCAT temporary activation period to the +// already-ongoing temporary TCAT activation (if any). The goals of the adaptation are: +// - not shorten the duration of an existing (scheduled/ongoing) activation +// - not set the start of activation later than the existing scheduled activation time +void TcatAgent::AdaptToExistingActivePeriod(uint32_t &aPeriodDelayMs, uint32_t &aPeriodDurationMs) +{ + VerifyOrExit(mState != kStateActive); + + if (mActiveOrStandbyTimer.IsRunning()) + { + TimeMilli now = TimerMilli::GetNow(); + uint32_t remainingMs; + remainingMs = (mActiveOrStandbyTimer.GetFireTime() > now) ? mActiveOrStandbyTimer.GetFireTime() - now : 0; + if (mTimerSetsToActive) + { + aPeriodDelayMs = Min(aPeriodDelayMs, remainingMs); + aPeriodDurationMs = Max(aPeriodDurationMs, remainingMs + mTcatActiveDurationMs - aPeriodDelayMs); + } + else + { + aPeriodDelayMs = 0; + aPeriodDurationMs = Max(aPeriodDurationMs, remainingMs); + } + } + +exit: + return; +} + void SerializeTcatAdvertisementTlv(uint8_t *aBuffer, uint16_t &aOffset, TcatAdvertisementTlvType aType, diff --git a/src/core/meshcop/tcat_agent.hpp b/src/core/meshcop/tcat_agent.hpp index 89ace7b1b..4533f57f3 100644 --- a/src/core/meshcop/tcat_agent.hpp +++ b/src/core/meshcop/tcat_agent.hpp @@ -52,6 +52,7 @@ #include "meshcop/meshcop.hpp" #include "meshcop/meshcop_tlvs.hpp" #include "meshcop/secure_transport.hpp" +#include "thread/tmf.hpp" namespace ot { @@ -330,7 +331,7 @@ public: * Activate TCAT functions of the TCAT agent. * * This requires the TCAT agent to be already started. - * The state transitions to kStateActive of kStateActiveTemporary. In these states, TCAT Advertisements + * The state transitions to kStateActive or kStateActiveTemporary. In these states, TCAT Advertisements * are actively sent and TCAT Commissioners are able to connect. From here, TCAT can be set to standby * again using Standby(). * If a connection is ongoing and aDurationMs==0, this call will ensure that kStateActive will @@ -338,7 +339,7 @@ public: * This function will override any ongoing temporary activation of TCAT, or any * previously scheduled activation for a future time. * - * @param[in] aDelayMs Delay in ms before activating. If 0, activate immediately. + * @param[in] aDelayMs Delay in ms before activating. If 0, activate immediately. * @param[in] aDurationMs Duration in ms of the activation. If 0, activate indefinitely. * * @retval kErrorNone Successfully set the TCAT agent to kStateActive now, OR scheduled @@ -413,6 +414,8 @@ public: */ bool GetApplicationResponsePending(void) const { return mApplicationResponsePending; } + template void HandleTmf(Coap::Message &aMessage, const Ip6::MessageInfo &aMessageInfo); + private: void NotifyApplicationResponseSent(void) { mApplicationResponsePending = false; } void NotifyStateChange(void); @@ -456,7 +459,7 @@ private: TcatApplicationProtocol aApplicationProtocol, bool &aResponse); void HandleTimer(void); - + void AdaptToExistingActivePeriod(uint32_t &aPeriodDelayMs, uint32_t &aPeriodDurationMs); Error VerifyHash(const Message &aIncomingMessage, uint16_t aOffset, uint16_t aLength, @@ -478,6 +481,7 @@ private: static constexpr uint16_t kBufferReserve = 2048 / (Buffer::kSize - sizeof(otMessageBuffer)) + 1; static constexpr uint8_t kServiceNameMaxLength = OT_TCAT_SERVICE_NAME_MAX_LENGTH; static constexpr uint8_t kApplicationLayerMaxCount = OT_TCAT_APPLICATION_LAYER_MAX_COUNT; + static constexpr uint16_t kTcatTmfEnableDefaultSec = OT_TCAT_ENABLE_MAX; const VendorInfo *mVendorInfo; Callback mJoinCallback; @@ -488,7 +492,8 @@ private: NetworkName mCommissionerDomainName; ExtendedPanId mCommissionerExtendedPanId; State mState; - State mNextState; + State mNextState; //< desired state after client disconnects + bool mTimerSetsToActive : 1; bool mCommissionerHasNetworkName : 1; bool mCommissionerHasDomainName : 1; bool mCommissionerHasExtendedPanId : 1; @@ -503,15 +508,10 @@ private: uint32_t mTcatActiveDurationMs; }; -} // namespace MeshCoP - -DefineCoreType(otTcatVendorInfo, MeshCoP::TcatAgent::VendorInfo); - -DefineMapEnum(otTcatApplicationProtocol, MeshCoP::TcatAgent::TcatApplicationProtocol); -DefineMapEnum(otTcatAdvertisedDeviceIdType, MeshCoP::TcatAgent::TcatDeviceIdType); +DeclareTmfHandler(TcatAgent, kUriTcatEnable); // Command class TLVs -typedef UintTlvInfo ResponseWithStatusTlv; +typedef UintTlvInfo ResponseWithStatusTlv; /** * Represent TCAT Device Type and Status @@ -545,6 +545,13 @@ enum TcatAdvertisementTlvType : uint8_t kTlvVendorIanaPen = 6, ///< TCAT Vendor IANA PEN }; +} // namespace MeshCoP + +DefineCoreType(otTcatVendorInfo, MeshCoP::TcatAgent::VendorInfo); + +DefineMapEnum(otTcatApplicationProtocol, MeshCoP::TcatAgent::TcatApplicationProtocol); +DefineMapEnum(otTcatAdvertisedDeviceIdType, MeshCoP::TcatAgent::TcatDeviceIdType); + } // namespace ot #endif // OPENTHREAD_CONFIG_BLE_TCAT_ENABLE diff --git a/src/core/thread/tmf.cpp b/src/core/thread/tmf.cpp index fdcbbd14d..bffbb6342 100644 --- a/src/core/thread/tmf.cpp +++ b/src/core/thread/tmf.cpp @@ -179,6 +179,9 @@ bool Agent::HandleResource(const char *aUriPath, Message &aMessage, const Ip6::M #if OPENTHREAD_CONFIG_BACKBONE_ROUTER_DUA_NDPROXYING_ENABLE Case(kUriDuaRegistrationRequest, BackboneRouter::Manager); #endif +#endif +#if OPENTHREAD_CONFIG_BLE_TCAT_ENABLE + Case(kUriTcatEnable, MeshCoP::TcatAgent); #endif default: diff --git a/src/core/thread/uri_paths.cpp b/src/core/thread/uri_paths.cpp index 5fed9e706..039515af1 100644 --- a/src/core/thread/uri_paths.cpp +++ b/src/core/thread/uri_paths.cpp @@ -83,6 +83,7 @@ static constexpr Entry kEntries[] = { {"c/pq"}, // kUriPanIdQuery {"c/ps"}, // kUriPendingSet {"c/rx"}, // kUriRelayRx + {"c/te"}, // kUriTcatEnable {"c/tx"}, // kUriRelayTx {"c/ur"}, // kUriProxyRx {"c/ut"}, // kUriProxyTx @@ -128,6 +129,7 @@ static_assert(AreConstStringsEqual(kEntries[kUriPendingGet].mPath, "c/pg"), "kEn static_assert(AreConstStringsEqual(kEntries[kUriPanIdQuery].mPath, "c/pq"), "kEntries is invalid"); static_assert(AreConstStringsEqual(kEntries[kUriPendingSet].mPath, "c/ps"), "kEntries is invalid"); static_assert(AreConstStringsEqual(kEntries[kUriRelayRx].mPath, "c/rx"), "kEntries is invalid"); +static_assert(AreConstStringsEqual(kEntries[kUriTcatEnable].mPath, "c/te"), "kEntries is invalid"); static_assert(AreConstStringsEqual(kEntries[kUriRelayTx].mPath, "c/tx"), "kEntries is invalid"); static_assert(AreConstStringsEqual(kEntries[kUriProxyRx].mPath, "c/ur"), "kEntries is invalid"); static_assert(AreConstStringsEqual(kEntries[kUriProxyTx].mPath, "c/ut"), "kEntries is invalid"); @@ -172,6 +174,7 @@ struct UriEnumCheck ValidateNextEnum(kUriPanIdQuery); ValidateNextEnum(kUriPendingSet); ValidateNextEnum(kUriRelayRx); + ValidateNextEnum(kUriTcatEnable); ValidateNextEnum(kUriRelayTx); ValidateNextEnum(kUriProxyRx); ValidateNextEnum(kUriProxyTx); @@ -235,6 +238,7 @@ template <> const char *UriToString(void) { return "PendingGet"; template <> const char *UriToString(void) { return "PanIdQuery"; } template <> const char *UriToString(void) { return "PendingSet"; } template <> const char *UriToString(void) { return "RelayRx"; } +template <> const char *UriToString(void) { return "TcatEnable"; } template <> const char *UriToString(void) { return "RelayTx"; } template <> const char *UriToString(void) { return "ProxyRx"; } template <> const char *UriToString(void) { return "ProxyTx"; } diff --git a/src/core/thread/uri_paths.hpp b/src/core/thread/uri_paths.hpp index 80fa4b30b..175a414d9 100644 --- a/src/core/thread/uri_paths.hpp +++ b/src/core/thread/uri_paths.hpp @@ -75,6 +75,7 @@ enum Uri : uint8_t kUriPanIdQuery, ///< PAN ID Query ("c/pq") kUriPendingSet, ///< MGMT_PENDING_SET ("c/ps") kUriRelayRx, ///< Relay RX ("c/rx") + kUriTcatEnable, ///< TCAT Enable ("c/te") kUriRelayTx, ///< Relay TX ("c/tx") kUriProxyRx, ///< Proxy RX ("c/ur") kUriProxyTx, ///< Proxy TX ("c/ut") @@ -146,6 +147,7 @@ template <> const char *UriToString(void); template <> const char *UriToString(void); template <> const char *UriToString(void); template <> const char *UriToString(void); +template <> const char *UriToString(void); template <> const char *UriToString(void); template <> const char *UriToString(void); template <> const char *UriToString(void); diff --git a/tests/scripts/thread-cert/node.py b/tests/scripts/thread-cert/node.py index c6de5afbe..b4dd24a83 100755 --- a/tests/scripts/thread-cert/node.py +++ b/tests/scripts/thread-cert/node.py @@ -951,7 +951,7 @@ class NodeImpl: PROMPT = 'spinel-cli > ' if self.node_type == 'ncp-sim' else '> ' while True: self._expect(r"[^\n]+\n") - line = self.pexpect.match.group(0).decode('utf8').strip() + line = self.pexpect.match.group(0).decode('utf-8', errors='backslashreplace').strip() while line.startswith(PROMPT): line = line[len(PROMPT):] @@ -3268,6 +3268,14 @@ class NodeImpl: payload += tlv.to_hex() self.commissioner_mgmtset(self.bytes_to_hex_str(payload)) + def tcat(self, cmd): + self.send_command(f'tcat {cmd}') + self._expect_done() + + def udp_start_client(self): + self.send_command('udp open') + self._expect_done() + def udp_start(self, local_ipaddr, local_port, bind_unspecified=False): cmd = 'udp open' self.send_command(cmd) @@ -3282,8 +3290,11 @@ class NodeImpl: self.send_command(cmd) self._expect_done() - def udp_send(self, bytes, ipaddr, port, success=True): - cmd = 'udp send %s %d -s %d ' % (ipaddr, port, bytes) + def udp_send(self, bytes_count, ipaddr, port, success=True, data_bytes: bytes = None): + if data_bytes is None: + cmd = 'udp send %s %d -s %d ' % (ipaddr, port, bytes_count) + else: + cmd = 'udp send %s %d -x %s ' % (ipaddr, port, data_bytes.hex()) self.send_command(cmd) if success: self._expect_done() @@ -3293,6 +3304,20 @@ class NodeImpl: def udp_check_rx(self, bytes_should_rx): self._expect('%d bytes' % bytes_should_rx) + def udp_rx(self) -> bytes: + PROMPT = 'spinel-cli > ' if self.node_type == 'ncp-sim' else '> ' + while True: + # match non-newline chars until EOL, such as prompts, whitespace, or UDP results '\d+ bytes from' + self._expect(r"[^\n]+$") + line = self.pexpect.match.group(0) + line_utf = line.decode('utf-8', errors='backslashreplace').lstrip() + if line_utf.startswith(PROMPT) or len(line_utf.rstrip()) == 0 or self.__is_logging_line(line_utf): + continue + else: + break + + return line.strip() + def set_routereligible(self, enable: bool): cmd = f'routereligible {"enable" if enable else "disable"}' self.send_command(cmd) diff --git a/tests/scripts/thread-cert/test_tcat_tmf_enable_req.py b/tests/scripts/thread-cert/test_tcat_tmf_enable_req.py new file mode 100755 index 000000000..028b89dae --- /dev/null +++ b/tests/scripts/thread-cert/test_tcat_tmf_enable_req.py @@ -0,0 +1,139 @@ +#!/usr/bin/env python3 +# +# Copyright (c) 2025, The OpenThread Authors. +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# 1. Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# 2. Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# 3. Neither the name of the copyright holder nor the +# names of its contributors may be used to endorse or promote products +# derived from this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE +# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +# POSSIBILITY OF SUCH DAMAGE. +# + +import config +import unittest + +import thread_cert + +# Test description: +# This test verifies that a TCAT-enabled device can successfully respond to +# a TMF request to enable TCAT (TCAT_ENABLE.req) from another node. +# +# Topology: +# ROUTER_1 (TCAT agent) +# | +# | +# ROUTER_2 (TMF client) +# + +ROUTER_1 = 1 +ROUTER_2 = 2 + + +class TcatTmfEnableReq(thread_cert.TestCase): + USE_MESSAGE_FACTORY = False + SUPPORT_NCP = False + + TOPOLOGY = { + ROUTER_1: { + 'name': 'ROUTER_1_TCAT', + 'network_key': '00112233445566778899aabbccddeeff', + 'mode': 'rdn', + }, + ROUTER_2: { + 'name': 'ROUTER_2_CLIENT', + 'network_key': '00112233445566778899aabbccddeeff', + 'mode': 'rdn', + }, + } + + def test(self): + router1 = self.nodes[ROUTER_1] + router2 = self.nodes[ROUTER_2] + + # + # 0. Start the nodes and form a network. + # + router1.start() + self.simulator.go(config.LEADER_STARTUP_DELAY) + self.assertEqual(router1.get_state(), 'leader') + + router2.start() + self.simulator.go(config.ROUTER_STARTUP_DELAY) + self.assertEqual(router2.get_state(), 'router') + + # Allow some time for the network to stabilize and routes to propagate. + self.simulator.go(5) + + # Enable the TCAT agent on Router 1. + router1.tcat('start') + router1.tcat('standby') + self.simulator.go(1) + print("TCAT agent started and in standby on Router 1.") + + # Router 2 sends a TMF request to Router 1. + rloc1 = router1.get_rloc() + router2.udp_start_client() + print(f"Router 2 sending TCAT_ENABLE.req to Router 1 at RLOC {rloc1}...") + # CoAP header: Ver=1, Type=CON(0), Code=POST(0.02), MID=0x1234 + # URI-Option: 'c' (0x63) + # URI-Option: 'te' (0x7465) (TCAT_ENABLE.req) + # Payload Marker (0xff) + # DelayTimer TLV: type=52/0x34, len=4, val=1024ms + tmf_payload = bytes.fromhex('40021234b163027465ff340400000400') + router2.udp_send(0, rloc1, 61631, data_bytes=tmf_payload) + + # Allow time for the request to be sent and the response to be received. + self.simulator.go(5) + + # Read the UDP command result (comes later). + tmf_rsp = router2.udp_rx() + print(f'Received CoAP TMF response: {tmf_rsp}') + + # Verify OK response + expected_state_tlv = bytes.fromhex('100101') # Accept status 0x01 + tmf_rsp_state_tlv = tmf_rsp[-len(expected_state_tlv):] + self.assertEqual(tmf_rsp_state_tlv, expected_state_tlv, + f"Expected State TLV {expected_state_tlv} but got {tmf_rsp_state_tlv}") + + # TODO: no way yet to verify actual tcat state on Router 1 via CLI. + + # Router 2 sends invalid TCAT_ENABLE.req request. + print(f"Router 2 sending invalid TCAT_ENABLE.req to Router 1 at RLOC {rloc1}...") + # CoAP header: Ver=1, Type=CON(0), Code=POST(0.02), MID=0x5678 + # URI-Path: 'c/te' (TCAT_ENABLE.req) + # Payload Marker (0xff) + # Incomplete payload: contains only a Duration TLV (type 23/0x17) with uint16 value 240 (0xf0), + # but misses a DelayTimer TLV. + tmf_payload = bytes.fromhex('40025678b163027465ff170200f0') + router2.udp_send(0, rloc1, 61631, data_bytes=tmf_payload) + + # And remaining verification steps. + self.simulator.go(5) + tmf_rsp2 = router2.udp_rx() + print(f'Received CoAP TMF response: {tmf_rsp2}') + expected_state_tlv = bytes.fromhex('1001ff') # Reject status 0xff + tmf_rsp_state_tlv = tmf_rsp2[-len(expected_state_tlv):] + self.assertEqual(tmf_rsp_state_tlv, expected_state_tlv, + f"Expected State TLV {expected_state_tlv} but got {tmf_rsp_state_tlv}") + + +if __name__ == '__main__': + unittest.main()