diff --git a/src/cli/BUILD.gn b/src/cli/BUILD.gn index e70ae4fc8..9aefd4095 100644 --- a/src/cli/BUILD.gn +++ b/src/cli/BUILD.gn @@ -63,6 +63,8 @@ openthread_cli_sources = [ "cli_network_data.hpp", "cli_ping.cpp", "cli_ping.hpp", + "cli_plat_tcp.cpp", + "cli_plat_tcp.hpp", "cli_srp_client.cpp", "cli_srp_client.hpp", "cli_srp_server.cpp", diff --git a/src/cli/CMakeLists.txt b/src/cli/CMakeLists.txt index 8eadca79d..31873dcd3 100644 --- a/src/cli/CMakeLists.txt +++ b/src/cli/CMakeLists.txt @@ -51,6 +51,7 @@ set(COMMON_SOURCES cli_mesh_diag.cpp cli_network_data.cpp cli_ping.cpp + cli_plat_tcp.cpp cli_srp_client.cpp cli_srp_server.cpp cli_tcat.cpp diff --git a/src/cli/cli.cpp b/src/cli/cli.cpp index 95a01d183..bcfd81f19 100644 --- a/src/cli/cli.cpp +++ b/src/cli/cli.cpp @@ -108,6 +108,9 @@ Interpreter::Interpreter(Instance *aInstance, otCliOutputCallback aCallback, voi #if OPENTHREAD_CONFIG_TCP_ENABLE && OPENTHREAD_CONFIG_CLI_TCP_ENABLE , mTcp(aInstance, *this) #endif +#if OPENTHREAD_CONFIG_PLATFORM_TCP_ENABLE && OPENTHREAD_CONFIG_CLI_PLAT_TCP_ENABLE + , mPlatTcp(aInstance, *this) +#endif #if OPENTHREAD_CONFIG_COAP_API_ENABLE , mCoap(aInstance, *this) #endif @@ -6935,6 +6938,10 @@ template <> otError Interpreter::Process(Arg aArgs[]) { return mTca template <> otError Interpreter::Process(Arg aArgs[]) { return mTcp.Process(aArgs); } #endif +#if OPENTHREAD_CONFIG_PLATFORM_TCP_ENABLE && OPENTHREAD_CONFIG_CLI_PLAT_TCP_ENABLE +template <> otError Interpreter::Process(Arg aArgs[]) { return mPlatTcp.Process(aArgs); } +#endif + template <> otError Interpreter::Process(Arg aArgs[]) { return mUdp.Process(aArgs); } template <> otError Interpreter::Process(Arg aArgs[]) @@ -8695,6 +8702,9 @@ otError Interpreter::ProcessCommand(Arg aArgs[]) CmdEntry("ping"), #endif CmdEntry("platform"), +#if OPENTHREAD_CONFIG_PLATFORM_TCP_ENABLE && OPENTHREAD_CONFIG_CLI_PLAT_TCP_ENABLE + CmdEntry("plattcp"), +#endif CmdEntry("pollperiod"), #if OPENTHREAD_FTD && OPENTHREAD_CONFIG_REFERENCE_DEVICE_ENABLE CmdEntry("preferrouterid"), diff --git a/src/cli/cli.hpp b/src/cli/cli.hpp index ded37fd92..b02396aef 100644 --- a/src/cli/cli.hpp +++ b/src/cli/cli.hpp @@ -73,6 +73,7 @@ #include "cli/cli_mesh_diag.hpp" #include "cli/cli_network_data.hpp" #include "cli/cli_ping.hpp" +#include "cli/cli_plat_tcp.hpp" #include "cli/cli_srp_client.hpp" #include "cli/cli_srp_server.hpp" #include "cli/cli_tcat.hpp" @@ -445,6 +446,10 @@ private: TcpExample mTcp; #endif +#if OPENTHREAD_CONFIG_PLATFORM_TCP_ENABLE && OPENTHREAD_CONFIG_CLI_PLAT_TCP_ENABLE + PlatTcp mPlatTcp; +#endif + #if OPENTHREAD_CONFIG_COAP_API_ENABLE Coap mCoap; #endif diff --git a/src/cli/cli_config.h b/src/cli/cli_config.h index 11b667de8..3a1525fce 100644 --- a/src/cli/cli_config.h +++ b/src/cli/cli_config.h @@ -97,6 +97,18 @@ #define OPENTHREAD_CONFIG_CLI_IFCONFIG_INIT_ENABLE 0 #endif +/** + * @def OPENTHREAD_CONFIG_CLI_PLAT_TCP_ENABLE + * + * Indicates whether the plat-tcp CLI commands are enabled. + * + * These CLI commands are intended primarily for testing and evaluation of the `otPlatTcp` API implementations. + * + */ +#ifndef OPENTHREAD_CONFIG_CLI_PLAT_TCP_ENABLE +#define OPENTHREAD_CONFIG_CLI_PLAT_TCP_ENABLE 1 +#endif + /** * @def OPENTHREAD_CONFIG_CLI_TCP_ENABLE * diff --git a/src/cli/cli_plat_tcp.cpp b/src/cli/cli_plat_tcp.cpp new file mode 100644 index 000000000..61843699e --- /dev/null +++ b/src/cli/cli_plat_tcp.cpp @@ -0,0 +1,302 @@ +/* + * Copyright (c) 2026, The OpenThread Authors. + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * 3. Neither the name of the copyright holder nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ + +/** + * @file + * This file implements a TCP CLI tool. + */ + +#include "openthread-core-config.h" + +#include "cli_config.h" + +#if OPENTHREAD_CONFIG_PLATFORM_TCP_ENABLE && OPENTHREAD_CONFIG_CLI_PLAT_TCP_ENABLE + +#include "cli_plat_tcp.hpp" + +#include "instance/instance.hpp" + +namespace ot { +namespace Cli { + +//--------------------------------------------------------------------------------------------------------------------- +// PlatTcp::Listener + +PlatTcp::Listener::Listener(Instance &aInstance, PlatTcp &aOwner) + : Ip6::PlatTcp::Listener(aInstance, Accept, nullptr) + , mOwner(aOwner) +{ +} + +Ip6::PlatTcp::Connection *PlatTcp::Listener::Accept(Ip6::PlatTcp::Listener &aListener, const SockAddr &aPeerSockAddr) +{ + return static_cast(aListener).mOwner.Accept(aPeerSockAddr); +} + +//--------------------------------------------------------------------------------------------------------------------- +// PlatTcp::Connection + +PlatTcp::Connection::Connection(Instance &aInstance, PlatTcp &aOwner) + : Ip6::PlatTcp::Connection(aInstance, HandleEvent, nullptr) + , mOwner(aOwner) +{ +} + +void PlatTcp::Connection::HandleEvent(Ip6::PlatTcp::Connection &aConnection, const Event aEvent) +{ + return static_cast(aConnection).mOwner.HandleEvent(aEvent); +} + +//--------------------------------------------------------------------------------------------------------------------- +// PlatTcp + +PlatTcp::PlatTcp(otInstance *aInstance, OutputImplementer &aOutputImplementer) + : Utils(aInstance, aOutputImplementer) + , mListener(AsCoreType(aInstance), *this) + , mConnection(AsCoreType(aInstance), *this) + , mIsBound(false) +{ +} + +PlatTcp::Connection *PlatTcp::Accept(const SockAddr &aPeerSockAddr) +{ + OutputLine("PlatTcp: Accept %s", aPeerSockAddr.ToString().AsCString()); + + return &mConnection; +} + +void PlatTcp::HandleEvent(Connection::Event aEvent) +{ + OutputLine("PlatTcp:: HandleEvent %s", Connection::EventToString(aEvent)); + + if (aEvent == Connection::kEventDisconnected) + { + OutputLine("PlatTcp:: DisconnectReason: %s", + Connection::DisconnectReasonToString(mConnection.GetDisconnectReason())); + } + + if (aEvent == Connection::kEventReceive) + { + const Message *rxMessage = mConnection.GetRxMessage(); + + VerifyOrExit(rxMessage != nullptr); + OutputLine("PlatTcp: Received %u bytes", rxMessage->GetLength()); + + for (uint16_t offset = 0; offset < rxMessage->GetLength();) + { + uint8_t buffer[32]; + uint16_t readLength = rxMessage->ReadBytes(offset, buffer, sizeof(buffer)); + + OutputSpaces(4); + OutputBytesLine(buffer, readLength); + + offset += readLength; + } + + mConnection.FreeRxMessage(); + } + +exit: + return; +} + +otError PlatTcp::GetSockAddr(Arg aArgs[], SockAddr &aSockAddr) +{ + otError error = OT_ERROR_NONE; + uint16_t port; + uint32_t ifIndex; + + aSockAddr.Clear(); + + VerifyOrExit(!aArgs[0].IsEmpty()); + SuccessOrExit(error = aArgs[0].ParseAsIp6Address(aSockAddr.GetAddress())); + + VerifyOrExit(!aArgs[1].IsEmpty()); + SuccessOrExit(error = aArgs[1].ParseAsUint16(port)); + aSockAddr.SetPort(port); + + VerifyOrExit(!aArgs[2].IsEmpty()); + SuccessOrExit(error = aArgs[2].ParseAsUint32(ifIndex)); + aSockAddr.SetIfIndex(ifIndex); + +exit: + return error; +} + +template <> otError PlatTcp::Process(Arg aArgs[]) +{ + otError error = OT_ERROR_NONE; + + if (aArgs[0].IsEmpty()) + { + OutputEnabledDisabledStatus(mListener.GetState() == Listener::kStateEnabled); + } + else if (aArgs[0] == "enable") + { + SockAddr sockAddr; + + SuccessOrExit(error = GetSockAddr(&aArgs[1], sockAddr)); + OutputLine("Enabling listener on %s", sockAddr.ToString().AsCString()); + error = mListener.Enable(sockAddr); + } + else if (aArgs[0] == "disable") + { + VerifyOrExit(aArgs[1].IsEmpty(), error = OT_ERROR_INVALID_ARGS); + mListener.Disable(); + } + else + { + error = OT_ERROR_INVALID_COMMAND; + } + +exit: + return error; +} + +template <> otError PlatTcp::Process(Arg aArgs[]) +{ + otError error = OT_ERROR_NONE; + SockAddr newSockAddr; + + VerifyOrExit(mConnection.GetState() == Connection::kStateUnused, error = OT_ERROR_INVALID_STATE); + + if (aArgs[0].IsEmpty()) + { + mIsBound = false; + mLocalSockAddr.Clear(); + ExitNow(); + } + + SuccessOrExit(error = GetSockAddr(aArgs, newSockAddr)); + mLocalSockAddr = newSockAddr; + mIsBound = true; + OutputLine("LocalSockAddr: %s", mLocalSockAddr.ToString().AsCString()); + +exit: + return error; +} + +template <> otError PlatTcp::Process(Arg aArgs[]) +{ + otError error = OT_ERROR_NONE; + SockAddr peerSockAddr; + + SuccessOrExit(error = GetSockAddr(aArgs, peerSockAddr)); + + if (mIsBound) + { + OutputLine("Binding to %s, connecting to %s", mLocalSockAddr.ToString().AsCString(), + peerSockAddr.ToString().AsCString()); + error = mConnection.BindAndConnect(mLocalSockAddr, peerSockAddr); + } + else + { + OutputLine("Connecting to %s", peerSockAddr.ToString().AsCString()); + error = mConnection.Connect(peerSockAddr); + } + +exit: + return error; +} + +template <> otError PlatTcp::Process(Arg aArgs[]) +{ + otError error = OT_ERROR_NONE; + OwnedPtr message; + uint16_t length; + + SuccessOrExit(error = aArgs[0].ParseAsUint16(length)); + VerifyOrExit(aArgs[1].IsEmpty(), error = OT_ERROR_INVALID_ARGS); + + message.Reset(AsCoreType(GetInstancePtr()).Get().Allocate(Message::kTypeOther)); + VerifyOrExit(message != nullptr, error = OT_ERROR_NO_BUFS); + + for (uint16_t i = 0; i < length; i++) + { + SuccessOrExit(error = message->Append(static_cast(i & 0xff) ^ (i >> 8))); + } + + error = mConnection.Send(message.PassOwnership()); + +exit: + return error; +} + +template <> otError PlatTcp::Process(Arg aArgs[]) +{ + otError error = OT_ERROR_NONE; + + VerifyOrExit(aArgs[0].IsEmpty(), error = OT_ERROR_INVALID_ARGS); + mConnection.Close(); + +exit: + return error; +} + +template <> otError PlatTcp::Process(Arg aArgs[]) +{ + otError error = OT_ERROR_NONE; + + VerifyOrExit(aArgs[0].IsEmpty(), error = OT_ERROR_INVALID_ARGS); + mConnection.Abort(); + +exit: + return error; +} + +otError PlatTcp::Process(Arg aArgs[]) +{ +#define CmdEntry(aCommandString) {aCommandString, &PlatTcp::Process} + + static constexpr Command kCommands[] = {CmdEntry("abort"), CmdEntry("bind"), CmdEntry("close"), + CmdEntry("connect"), CmdEntry("listener"), CmdEntry("send")}; + + static_assert(BinarySearch::IsSorted(kCommands), "kCommands is not sorted"); + + otError error = OT_ERROR_INVALID_COMMAND; + const Command *command; + + if (aArgs[0].IsEmpty() || (aArgs[0] == "help")) + { + OutputCommandTable(kCommands); + ExitNow(error = aArgs[0].IsEmpty() ? error : OT_ERROR_NONE); + } + + command = BinarySearch::Find(aArgs[0].GetCString(), kCommands); + VerifyOrExit(command != nullptr); + + error = (this->*command->mHandler)(aArgs + 1); + +exit: + return error; +} + +} // namespace Cli +} // namespace ot + +#endif // OPENTHREAD_CONFIG_PLATFORM_TCP_ENABLE && OPENTHREAD_CONFIG_CLI_PLAT_TCP_ENABLE diff --git a/src/cli/cli_plat_tcp.hpp b/src/cli/cli_plat_tcp.hpp new file mode 100644 index 000000000..70a7b73a2 --- /dev/null +++ b/src/cli/cli_plat_tcp.hpp @@ -0,0 +1,122 @@ +/* + * Copyright (c) 2026, The OpenThread Authors. + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * 3. Neither the name of the copyright holder nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ + +/** + * @file + * This file contains definitions for a plat-TCP CLI tool. + */ + +#ifndef OT_CLI_CLI_PLAT_TCP_HPP_ +#define OT_CLI_CLI_PLAT_TCP_HPP_ + +#include "openthread-core-config.h" + +#if OPENTHREAD_CONFIG_PLATFORM_TCP_ENABLE && OPENTHREAD_CONFIG_CLI_PLAT_TCP_ENABLE + +#include "cli/cli_config.h" +#include "cli/cli_utils.hpp" + +#include "net/plat_tcp.hpp" + +namespace ot { +namespace Cli { + +/** + * Implements a CLI-based plat-TCP example. + * + * These CLI commands are intended primarily for testing and evaluation of the `otPlatTcp` API implementations. + */ +class PlatTcp : private Utils +{ +public: + /** + * Constructor + * + * @param[in] aInstance The OpenThread Instance. + * @param[in] aOutputImplementer An `OutputImplementer`. + */ + PlatTcp(otInstance *aInstance, OutputImplementer &aOutputImplementer); + + /** + * Processes a CLI sub-command. + * + * @param[in] aArgs An array of command line arguments. + * + * @retval OT_ERROR_NONE Successfully executed the CLI command. + * @retval OT_ERROR_PENDING The CLI command was successfully started but final result is pending. + * @retval OT_ERROR_INVALID_COMMAND Invalid or unknown CLI command. + * @retval OT_ERROR_INVALID_ARGS Invalid arguments. + * @retval ... Error during execution of the CLI command. + */ + otError Process(Arg aArgs[]); + +private: + using Command = CommandEntry; + + using SockAddr = Ip6::PlatTcp::SockAddr; + + class Listener : public Ip6::PlatTcp::Listener + { + public: + Listener(Instance &aInstance, PlatTcp &aOwner); + + private: + static Ip6::PlatTcp::Connection *Accept(Ip6::PlatTcp::Listener &aListener, const SockAddr &aPeerSockAddr); + + PlatTcp &mOwner; + }; + + class Connection : public Ip6::PlatTcp::Connection + { + public: + Connection(Instance &aInstance, PlatTcp &aOwner); + + private: + static void HandleEvent(Ip6::PlatTcp::Connection &aConnection, Event aEvent); + + PlatTcp &mOwner; + }; + + template otError Process(Arg aArgs[]); + + otError GetSockAddr(Arg aArgs[], SockAddr &aSockAddr); + Connection *Accept(const SockAddr &aPeerSockAddr); + void HandleEvent(Connection::Event aEvent); + + Listener mListener; + Connection mConnection; + SockAddr mLocalSockAddr; + bool mIsBound; +}; + +} // namespace Cli +} // namespace ot + +#endif // OPENTHREAD_CONFIG_PLATFORM_TCP_ENABLE && OPENTHREAD_CONFIG_CLI_PLAT_TCP_ENABLE + +#endif // OT_CLI_CLI_PLAT_TCP_HPP_ diff --git a/src/core/net/plat_tcp.cpp b/src/core/net/plat_tcp.cpp index 4cf32b465..b31a65624 100644 --- a/src/core/net/plat_tcp.cpp +++ b/src/core/net/plat_tcp.cpp @@ -567,6 +567,22 @@ const char *PlatTcp::Connection::EventToString(Event aEvent) return kStrings[aEvent]; } +const char *PlatTcp::Connection::DisconnectReasonToString(DisconnectReason aDisconnectReason) +{ +#define ConnDisconnectReasonMapList(_) \ + _(kDisconnectReasonClosed, "Closed") \ + _(kDisconnectReasonTimeout, "Timeout") \ + _(kDisconnectReasonRefused, "Refused") \ + _(kDisconnectReasonReset, "Reset") \ + _(kDisconnectReasonError, "Error") \ + _(kDisconnectReasonAbort, "Abort") \ + _(kDisconnectReasonNoBufs, "NoBufs") + + DefineEnumStringArray(ConnDisconnectReasonMapList); + + return kStrings[aDisconnectReason]; +} + //--------------------------------------------------------------------------------------------------------------------- // PlatTcp diff --git a/src/core/net/plat_tcp.hpp b/src/core/net/plat_tcp.hpp index 8770d6ba6..0582e05ef 100644 --- a/src/core/net/plat_tcp.hpp +++ b/src/core/net/plat_tcp.hpp @@ -509,6 +509,15 @@ public: */ static const char *EventToString(Event aEvent); + /** + * Converts a `DisconnectReason` to a human-readable string. + * + * @param[in] aReason The disconnect reason. + * + * @returns A string representation of @p aReason. + */ + static const char *DisconnectReasonToString(DisconnectReason aReason); + private: bool Matches(State aState) const { return mState == aState; } void SetState(State aState);