diff --git a/include/openthread/coap_secure.h b/include/openthread/coap_secure.h index 6a96ea3d7..25b56bf40 100644 --- a/include/openthread/coap_secure.h +++ b/include/openthread/coap_secure.h @@ -76,6 +76,15 @@ extern "C" { */ typedef void (*otHandleCoapSecureClientConnect)(bool aConnected, void *aContext); +/** + * Callback function pointer to notify when the CoAP secure agent is automatically stopped due to reaching the maximum + * number of connection attempts. + * + * @param[in] aContext A pointer to arbitrary context information. + * + */ +typedef void (*otCoapSecureAutoStopCallback)(void *aContext); + /** * Starts the CoAP Secure service. * @@ -87,6 +96,26 @@ typedef void (*otHandleCoapSecureClientConnect)(bool aConnected, void *aContext) */ otError otCoapSecureStart(otInstance *aInstance, uint16_t aPort); +/** + * Starts the CoAP secure service and sets the maximum number of allowed connection attempts before stopping the + * agent automatically. + * + * @param[in] aInstance A pointer to an OpenThread instance. + * @param[in] aPort The local UDP port to bind to. + * @param[in] aMaxAttempts Maximum number of allowed connection request attempts. Zero indicates no limit. + * @param[in] aCallback Callback to notify if max number of attempts has reached and agent is stopped. + * @param[in] aContext A pointer to arbitrary context to use with @p aCallback. + * + * @retval OT_ERROR_NONE Successfully started the CoAP agent. + * @retval OT_ERROR_ALREADY Already started. + * + */ +otError otCoapSecureStartWithMaxConnAttempts(otInstance *aInstance, + uint16_t aPort, + uint16_t aMaxAttempts, + otCoapSecureAutoStopCallback aCallback, + void *aContext); + /** * Stops the CoAP Secure server. * @@ -230,6 +259,17 @@ bool otCoapSecureIsConnected(otInstance *aInstance); */ bool otCoapSecureIsConnectionActive(otInstance *aInstance); +/** + * Indicates whether or not the DTLS session is closed. + * + * @param[in] aInstance A pointer to an OpenThread instance. + * + * @retval TRUE The DTLS session is closed. + * @retval FALSE The DTLS session is not closed. + * + */ +bool otCoapSecureIsClosed(otInstance *aInstance); + /** * Sends a CoAP request block-wise over secure DTLS connection. * diff --git a/include/openthread/instance.h b/include/openthread/instance.h index 47de8daef..f94151c36 100644 --- a/include/openthread/instance.h +++ b/include/openthread/instance.h @@ -53,7 +53,7 @@ extern "C" { * @note This number versions both OpenThread platform and user APIs. * */ -#define OPENTHREAD_API_VERSION (391) +#define OPENTHREAD_API_VERSION (392) /** * @addtogroup api-instance diff --git a/src/cli/README_COAPS.md b/src/cli/README_COAPS.md index c8c91fa62..6316fb496 100644 --- a/src/cli/README_COAPS.md +++ b/src/cli/README_COAPS.md @@ -87,6 +87,9 @@ coaps response sent - [delete](#delete-uri-path-type-payload) - [disconnect](#disconnect) - [get](#get-uri-path-type) +- [isclosed](#isclosed) +- [isconnactive](#isconnactive) +- [isconnected](#isconnected) - [post](#post-uri-path-type-payload) - [psk](#psk-psk-pskid) - [put](#put-uri-path-type-payload) @@ -102,11 +105,13 @@ coaps response sent ```bash > coaps help -help connect delete disconnect get +isclosed +isconnactive +isconnected post psk put @@ -251,6 +256,36 @@ Stops the application coaps service. Done ``` +### isconnected + +Indicates whether or not the CoAP secure service is connected. + +```bash +> coaps isconnected +yes +Done +``` + +### isconnactive + +Indicates whether or not the CoAP secure service connection is active (already connected or establishing a connection). + +```bash +> coaps isconnactive +yes +Done +``` + +### isclosed + +Indicates whether or not the CoAP secure service is closed. + +```bash +> coaps isclosed +no +Done +``` + ### x509 Set DTLS ciphersuite to `TLS_ECDHE_ECDSA_WITH_AES_128_CCM_8`. diff --git a/src/cli/cli_coap_secure.cpp b/src/cli/cli_coap_secure.cpp index 518e9075d..e22352ff2 100644 --- a/src/cli/cli_coap_secure.cpp +++ b/src/cli/cli_coap_secure.cpp @@ -149,8 +149,9 @@ exit: template <> otError CoapSecure::Process(Arg aArgs[]) { - otError error = OT_ERROR_NONE; - bool verifyPeerCert = true; + otError error = OT_ERROR_NONE; + bool verifyPeerCert = true; + uint16_t maxConnAttempts = 0; if (!aArgs[0].IsEmpty()) { @@ -158,9 +159,13 @@ template <> otError CoapSecure::Process(Arg aArgs[]) { verifyPeerCert = false; } + else if (aArgs[0] == "true") + { + verifyPeerCert = true; + } else { - VerifyOrExit(aArgs[0] == "true", error = OT_ERROR_INVALID_ARGS); + SuccessOrExit(error = aArgs[0].ParseAsUint16(maxConnAttempts)); } } @@ -171,7 +176,8 @@ template <> otError CoapSecure::Process(Arg aArgs[]) otCoapSecureSetDefaultHandler(GetInstancePtr(), &CoapSecure::DefaultHandler, this); #endif - error = otCoapSecureStart(GetInstancePtr(), OT_DEFAULT_COAP_SECURE_PORT); + error = otCoapSecureStartWithMaxConnAttempts(GetInstancePtr(), OT_DEFAULT_COAP_SECURE_PORT, maxConnAttempts, + nullptr, nullptr); exit: return error; @@ -200,6 +206,32 @@ template <> otError CoapSecure::Process(Arg aArgs[]) return OT_ERROR_NONE; } +template <> otError CoapSecure::Process(Arg aArgs[]) +{ + return ProcessIsRequest(aArgs, otCoapSecureIsClosed); +} + +template <> otError CoapSecure::Process(Arg aArgs[]) +{ + return ProcessIsRequest(aArgs, otCoapSecureIsConnected); +} + +template <> otError CoapSecure::Process(Arg aArgs[]) +{ + return ProcessIsRequest(aArgs, otCoapSecureIsConnectionActive); +} + +otError CoapSecure::ProcessIsRequest(Arg aArgs[], bool (*IsChecker)(otInstance *)) +{ + otError error = OT_ERROR_NONE; + + VerifyOrExit(aArgs[0].IsEmpty(), error = OT_ERROR_INVALID_ARGS); + OutputLine("%s", IsChecker(GetInstancePtr()) ? "yes" : "no"); + +exit: + return error; +} + template <> otError CoapSecure::Process(Arg aArgs[]) { return ProcessRequest(aArgs, OT_COAP_CODE_GET); } template <> otError CoapSecure::Process(Arg aArgs[]) { return ProcessRequest(aArgs, OT_COAP_CODE_POST); } @@ -442,11 +474,13 @@ otError CoapSecure::Process(Arg aArgs[]) } static constexpr Command kCommands[] = { - CmdEntry("connect"), CmdEntry("delete"), CmdEntry("disconnect"), CmdEntry("get"), CmdEntry("post"), + CmdEntry("connect"), CmdEntry("delete"), CmdEntry("disconnect"), CmdEntry("get"), + CmdEntry("isclosed"), CmdEntry("isconnactive"), CmdEntry("isconnected"), CmdEntry("post"), #ifdef MBEDTLS_KEY_EXCHANGE_PSK_ENABLED CmdEntry("psk"), #endif - CmdEntry("put"), CmdEntry("resource"), CmdEntry("set"), CmdEntry("start"), CmdEntry("stop"), + CmdEntry("put"), CmdEntry("resource"), CmdEntry("set"), CmdEntry("start"), + CmdEntry("stop"), #ifdef MBEDTLS_KEY_EXCHANGE_ECDHE_ECDSA_ENABLED CmdEntry("x509"), #endif diff --git a/src/cli/cli_coap_secure.hpp b/src/cli/cli_coap_secure.hpp index dd26ebf6c..a09dfed02 100644 --- a/src/cli/cli_coap_secure.hpp +++ b/src/cli/cli_coap_secure.hpp @@ -103,6 +103,7 @@ private: template otError Process(Arg aArgs[]); otError ProcessRequest(Arg aArgs[], otCoapCode aCoapCode); + otError ProcessIsRequest(Arg aArgs[], bool (*IsChecker)(otInstance *)); void Stop(void); diff --git a/src/core/api/coap_secure_api.cpp b/src/core/api/coap_secure_api.cpp index 2ba31a262..e5ac4c014 100644 --- a/src/core/api/coap_secure_api.cpp +++ b/src/core/api/coap_secure_api.cpp @@ -50,6 +50,15 @@ otError otCoapSecureStart(otInstance *aInstance, uint16_t aPort) return AsCoreType(aInstance).GetApplicationCoapSecure().Start(aPort); } +otError otCoapSecureStartWithMaxConnAttempts(otInstance *aInstance, + uint16_t aPort, + uint16_t aMaxAttempts, + otCoapSecureAutoStopCallback aCallback, + void *aContext) +{ + return AsCoreType(aInstance).GetApplicationCoapSecure().Start(aPort, aMaxAttempts, aCallback, aContext); +} + #ifdef MBEDTLS_KEY_EXCHANGE_ECDHE_ECDSA_ENABLED void otCoapSecureSetCertificate(otInstance *aInstance, const uint8_t *aX509Cert, @@ -122,6 +131,8 @@ bool otCoapSecureIsConnectionActive(otInstance *aInstance) return AsCoreType(aInstance).GetApplicationCoapSecure().IsConnectionActive(); } +bool otCoapSecureIsClosed(otInstance *aInstance) { return AsCoreType(aInstance).GetApplicationCoapSecure().IsClosed(); } + void otCoapSecureStop(otInstance *aInstance) { AsCoreType(aInstance).GetApplicationCoapSecure().Stop(); } #if OPENTHREAD_CONFIG_COAP_BLOCKWISE_TRANSFER_ENABLE diff --git a/src/core/coap/coap_secure.hpp b/src/core/coap/coap_secure.hpp index 546152305..0565ef562 100644 --- a/src/core/coap/coap_secure.hpp +++ b/src/core/coap/coap_secure.hpp @@ -65,10 +65,8 @@ public: * Callback to notify when the agent is automatically stopped due to reaching the maximum number of connection * attempts. * - * @param[in] aContext A pointer to arbitrary context information. - * */ - typedef void (*AutoStopCallback)(void *aContext); + typedef otCoapSecureAutoStopCallback AutoStopCallback; /** * Initializes the object. @@ -165,6 +163,15 @@ public: */ bool IsConnected(void) const { return mDtls.IsConnected(); } + /** + * Indicates whether or not the DTLS session is closed. + * + * @retval TRUE The DTLS session is closed + * @retval FALSE The DTLS session is not closed. + * + */ + bool IsClosed(void) const { return mDtls.IsClosed(); } + /** * Stops the DTLS connection. * diff --git a/src/core/meshcop/secure_transport.cpp b/src/core/meshcop/secure_transport.cpp index 8db0a9fe2..acb7b5d96 100644 --- a/src/core/meshcop/secure_transport.cpp +++ b/src/core/meshcop/secure_transport.cpp @@ -1053,9 +1053,18 @@ void SecureTransport::HandleTimer(void) } else if (IsStateCloseNotify()) { - SetState(kStateOpen); - mTimer.Stop(); - mConnectedCallback.InvokeIfSet(false); + if ((mMaxConnectionAttempts > 0) && (mRemainingConnectionAttempts == 0)) + { + Close(); + mConnectedCallback.InvokeIfSet(false); + mAutoCloseCallback.InvokeIfSet(); + } + else + { + SetState(kStateOpen); + mTimer.Stop(); + mConnectedCallback.InvokeIfSet(false); + } } } diff --git a/src/core/meshcop/secure_transport.hpp b/src/core/meshcop/secure_transport.hpp index 229fdab69..4641ef537 100644 --- a/src/core/meshcop/secure_transport.hpp +++ b/src/core/meshcop/secure_transport.hpp @@ -234,6 +234,15 @@ public: */ bool IsConnected(void) const { return mState == kStateConnected; } + /** + * Indicates whether or not the session is closed. + * + * @retval TRUE The session is closed. + * @retval FALSE The session is not closed. + * + */ + bool IsClosed(void) const { return mState == kStateClosed; } + /** * Disconnects the session. * diff --git a/tests/toranj/cli/test-026-coaps-conn-limit.py b/tests/toranj/cli/test-026-coaps-conn-limit.py new file mode 100755 index 000000000..6684dbe00 --- /dev/null +++ b/tests/toranj/cli/test-026-coaps-conn-limit.py @@ -0,0 +1,104 @@ +#!/usr/bin/env python3 +# +# Copyright (c) 2024, 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. + +from cli import verify +from cli import verify_within +import cli +import time + +# ----------------------------------------------------------------------------------------------------------------------- +# Test description: +# +# Validate CoaP secure agent related to the maximum number of allowed connection attempts before the socket is a +# automatically closed and the agent is stopped. +# +# +# Network topology +# +# r1 ---- r2 +# + +test_name = __file__[:-3] if __file__.endswith('.py') else __file__ +print('-' * 120) +print('Starting \'{}\''.format(test_name)) + +# ----------------------------------------------------------------------------------------------------------------------- +# Creating `cli.Node` instances + +speedup = 40 +cli.Node.set_time_speedup_factor(speedup) + +r1 = cli.Node() +r2 = cli.Node() + +# ----------------------------------------------------------------------------------------------------------------------- +# Form topology + +r1.form('coaps-conn-lmt') +r2.join(r1) + +verify(r1.get_state() == 'leader') +verify(r2.get_state() == 'router') + +# ----------------------------------------------------------------------------------------------------------------------- +# Test Implementation + +r1.cli('coaps psk RIGHT 1234') + +# Set max connection attempts to 3 +r1.cli('coaps start 3') + +r1_mleid = r1.get_mleid_ip_addr() + +r2.cli('coaps psk WRONG 1234') +r2.cli('coaps start') + +# First connection attempt +r2.cli('coaps connect', r1_mleid) +time.sleep(0.1) +verify(r1.cli('coaps isconnactive')[-1] == 'no') +verify(r1.cli('coaps isclosed')[-1] == 'no') + +# Second connection attempt +r2.cli('coaps connect', r1_mleid) +time.sleep(0.1) +verify(r1.cli('coaps isconnactive')[-1] == 'no') +verify(r1.cli('coaps isclosed')[-1] == 'no') + +# Third connection attempt +r2.cli('coaps connect', r1_mleid) +time.sleep(0.1) +verify(r1.cli('coaps isconnactive')[-1] == 'no') +verify(r1.cli('coaps isclosed')[-1] == 'yes') + +# ----------------------------------------------------------------------------------------------------------------------- +# Test finished + +cli.Node.finalize_all_nodes() + +print('\'{}\' passed.'.format(test_name)) diff --git a/tests/toranj/openthread-core-toranj-config-simulation.h b/tests/toranj/openthread-core-toranj-config-simulation.h index 8a1063c5c..facfce9de 100644 --- a/tests/toranj/openthread-core-toranj-config-simulation.h +++ b/tests/toranj/openthread-core-toranj-config-simulation.h @@ -45,6 +45,10 @@ #endif +#define OPENTHREAD_CONFIG_COAP_API_ENABLE 1 + +#define OPENTHREAD_CONFIG_COAP_SECURE_API_ENABLE 1 + #define OPENTHREAD_CONFIG_PLATFORM_USEC_TIMER_ENABLE 0 #define OPENTHREAD_CONFIG_PLATFORM_FLASH_API_ENABLE 1 diff --git a/tests/toranj/start.sh b/tests/toranj/start.sh index 3e07304e6..742a0498a 100755 --- a/tests/toranj/start.sh +++ b/tests/toranj/start.sh @@ -190,6 +190,7 @@ if [ "$TORANJ_CLI" = 1 ]; then run cli/test-023-mesh-diag.py run cli/test-024-mle-adv-imax-change.py run cli/test-025-mesh-local-prefix-change.py + run cli/test-026-coaps-conn-limit.py run cli/test-400-srp-client-server.py run cli/test-601-channel-manager-channel-change.py # Skip the "channel-select" test on a TREL only radio link, since it