diff --git a/Android.mk b/Android.mk index b340bea93..1de97361e 100644 --- a/Android.mk +++ b/Android.mk @@ -283,6 +283,7 @@ LOCAL_SRC_FILES := \ src/core/net/dhcp6_client.cpp \ src/core/net/dhcp6_server.cpp \ src/core/net/dns_client.cpp \ + src/core/net/dns_dso.cpp \ src/core/net/dns_types.cpp \ src/core/net/dnssd_server.cpp \ src/core/net/icmp6.cpp \ diff --git a/etc/cmake/options.cmake b/etc/cmake/options.cmake index fc1a39bec..ca65b09b2 100644 --- a/etc/cmake/options.cmake +++ b/etc/cmake/options.cmake @@ -179,6 +179,11 @@ if(OT_DNS_CLIENT) target_compile_definitions(ot-config INTERFACE "OPENTHREAD_CONFIG_DNS_CLIENT_ENABLE=1") endif() +option(OT_DNS_DSO "enable DNS Stateful Operations (DSO) support") +if(OT_DNS_DSO) + target_compile_definitions(ot-config INTERFACE "OPENTHREAD_CONFIG_DNS_DSO_ENABLE=1") +endif() + option(OT_DNSSD_SERVER "enable DNS-SD server support") if(OT_DNSSD_SERVER) target_compile_definitions(ot-config INTERFACE "OPENTHREAD_CONFIG_DNSSD_SERVER_ENABLE=1") diff --git a/examples/Makefile-simulation b/examples/Makefile-simulation index 0df78dc68..f16ce8973 100644 --- a/examples/Makefile-simulation +++ b/examples/Makefile-simulation @@ -51,6 +51,7 @@ DHCP6_CLIENT ?= 1 DHCP6_SERVER ?= 1 DIAGNOSTIC ?= 1 DNS_CLIENT ?= 1 +DNS_DSO ?= 1 DNSSD_SERVER ?= 1 ECDSA ?= 1 HISTORY_TRACKER ?= 1 diff --git a/examples/README.md b/examples/README.md index 9a1861dcf..2cad314f8 100644 --- a/examples/README.md +++ b/examples/README.md @@ -32,6 +32,7 @@ This page lists the available common switches with description. Unless stated ot | DEBUG_UART | not implemented | Enables the Debug UART platform feature. | | DEBUG_UART_LOG | not implemented | Enables the log output for the debug UART. Requires OPENTHREAD_CONFIG_ENABLE_DEBUG_UART to be enabled. | | DNS_CLIENT | OT_DNS_CLIENT | Enables support for DNS client. Enable this switch on a device that sends a DNS query for AAAA (IPv6) record. | +| DNS_DSO | OT_DNS_DSO | Enables support for DNS Stateful Operations (DSO). | | DNSSD_SERVER | OT_DNSSD_SERVER | Enables support for DNS-SD server. DNS-SD server use service information from local SRP server to resolve DNS-SD query questions. | | DUA | OT_DUA | Enables the Domain Unicast Address feature for Thread 1.2. | | DYNAMIC_LOG_LEVEL | not implemented | Enables the dynamic log level feature. Enable this switch if OpenThread log level is required to be set at runtime. See [Logging guide](https://openthread.io/guides/build/logs) to learn more. | diff --git a/examples/common-switches.mk b/examples/common-switches.mk index 33687fecc..f63fcbc7c 100644 --- a/examples/common-switches.mk +++ b/examples/common-switches.mk @@ -51,6 +51,7 @@ DIAGNOSTIC ?= 0 DISABLE_DOC ?= 0 DISABLE_TOOLS ?= 0 DNS_CLIENT ?= 0 +DNS_DSO ?= 0 DNSSD_SERVER ?= 0 DUA ?= 0 DYNAMIC_LOG_LEVEL ?= 0 @@ -197,6 +198,10 @@ ifeq ($(DNS_CLIENT),1) COMMONCFLAGS += -DOPENTHREAD_CONFIG_DNS_CLIENT_ENABLE=1 endif +ifeq ($(DNS_DSO),1) +COMMONCFLAGS += -DOPENTHREAD_CONFIG_DNS_DSO_ENABLE=1 +endif + ifeq ($(DNSSD_SERVER),1) COMMONCFLAGS += -DOPENTHREAD_CONFIG_DNSSD_SERVER_ENABLE=1 endif diff --git a/examples/platforms/simulation/CMakeLists.txt b/examples/platforms/simulation/CMakeLists.txt index 869a1ae61..386003141 100644 --- a/examples/platforms/simulation/CMakeLists.txt +++ b/examples/platforms/simulation/CMakeLists.txt @@ -61,6 +61,7 @@ add_library(openthread-simulation alarm.c crypto.c diag.c + dso_transport.c entropy.c flash.c infra_if.c diff --git a/examples/platforms/simulation/Makefile.am b/examples/platforms/simulation/Makefile.am index cdcad7a6d..227e2ae27 100644 --- a/examples/platforms/simulation/Makefile.am +++ b/examples/platforms/simulation/Makefile.am @@ -43,6 +43,7 @@ PLATFORM_SOURCES = \ alarm.c \ crypto.c \ diag.c \ + dso_transport.c \ entropy.c \ flash.c \ infra_if.c \ diff --git a/examples/platforms/simulation/dso_transport.c b/examples/platforms/simulation/dso_transport.c new file mode 100644 index 000000000..f5107310a --- /dev/null +++ b/examples/platforms/simulation/dso_transport.c @@ -0,0 +1,59 @@ +/* + * Copyright (c) 2021, 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 "platform-simulation.h" + +#include + +#if OPENTHREAD_CONFIG_DNS_DSO_ENABLE + +void otPlatDsoEnableListening(otInstance *aInstance, bool aEnable) +{ + OT_UNUSED_VARIABLE(aInstance); + OT_UNUSED_VARIABLE(aEnable); +} + +void otPlatDsoConnect(otPlatDsoConnection *aConnection, const otSockAddr *aPeerSockAddr) +{ + OT_UNUSED_VARIABLE(aConnection); + OT_UNUSED_VARIABLE(aPeerSockAddr); +} + +void otPlatDsoSend(otPlatDsoConnection *aConnection, otMessage *aMessage) +{ + OT_UNUSED_VARIABLE(aConnection); + OT_UNUSED_VARIABLE(aMessage); +} + +void otPlatDsoDisconnect(otPlatDsoConnection *aConnection, otPlatDsoDisconnectMode aMode) +{ + OT_UNUSED_VARIABLE(aConnection); + OT_UNUSED_VARIABLE(aMode); +} + +#endif // #if OPENTHREAD_CONFIG_DNS_DSO_ENABLE diff --git a/include/Makefile.am b/include/Makefile.am index 2d7273a2d..6b4f16635 100644 --- a/include/Makefile.am +++ b/include/Makefile.am @@ -99,6 +99,7 @@ ot_platform_headers = \ openthread/platform/crypto.h \ openthread/platform/debug_uart.h \ openthread/platform/diag.h \ + openthread/platform/dso_transport.h \ openthread/platform/entropy.h \ openthread/platform/flash.h \ openthread/platform/infra_if.h \ diff --git a/include/openthread/BUILD.gn b/include/openthread/BUILD.gn index 089c941f4..6307afbcc 100644 --- a/include/openthread/BUILD.gn +++ b/include/openthread/BUILD.gn @@ -102,6 +102,7 @@ source_set("openthread") { "platform/crypto.h", "platform/debug_uart.h", "platform/diag.h", + "platform/dso_transport.h", "platform/entropy.h", "platform/flash.h", "platform/infra_if.h", diff --git a/include/openthread/instance.h b/include/openthread/instance.h index 175c65a87..c908508b1 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 (183) +#define OPENTHREAD_API_VERSION (184) /** * @addtogroup api-instance diff --git a/include/openthread/platform/dso_transport.h b/include/openthread/platform/dso_transport.h new file mode 100644 index 000000000..348ee828a --- /dev/null +++ b/include/openthread/platform/dso_transport.h @@ -0,0 +1,202 @@ +/* + * Copyright (c) 2021, The OpenThread Authors. + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * 3. Neither the name of the copyright holder nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ + +/** + * @file + * @brief + * This file includes the platform abstraction for DNS Stateful Operations (DSO) transport. + */ + +#ifndef OPENTHREAD_PLATFORM_DSO_TRANSPORT_H_ +#define OPENTHREAD_PLATFORM_DSO_TRANSPORT_H_ + +#include + +#include +#include +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * This structure represents a DSO connection. + * + * It is an opaque struct (the platform implementation only deals with pointers to this struct). + * + */ +typedef struct otPlatDsoConnection otPlatDsoConnection; + +/** + * This function can be used by DSO platform implementation to get the the OpenThread instance associated with a + * connection instance. + * + * @param[in] aConnection A pointer to the DSO connection. + * + * @returns A pointer to the `otInstance`. + * + */ +extern otInstance *otPlatDsoGetInstance(otPlatDsoConnection *aConnection); + +/** + * This function starts or stops listening for incoming connection requests on transport layer. + * + * For DNS-over-TLS, the transport layer MUST listen on port 853 and follow RFC 7858. + * + * While listening is enabled, if a connection request is received, the `otPlatDsoAccept()` callback MUST be called. + * + * @param[in] aInstance The OpenThread instance. + * @param[in] aEnable TRUE to start listening, FALSE to stop listening. + * + */ +void otPlatDsoEnableListening(otInstance *aInstance, bool aEnable); + +/** + * This function is a callback from the DSO platform to indicate an incoming connection request when listening is + * enabled. + * + * This function determines whether or not to accept the connection request. It returns a non-null `otPlatDsoConnection` + * pointer if the request is to be accepted, or `NULL` if the request is to be rejected. + * + * If a non-null connection pointer is returned, the platform layer MUST continue establishing the connection with the + * peer. The platform reports the outcome by invoking `otPlatDsoHandleConnected()` callback on success or + * `otPlatDsoHandleDisconnected()` callback on failure. + * + * @param[in] aInstance The OpenThread instance. + * @param[in] aPeerSockAddr The socket address (IPv6 address and port number) of the peer requesting connection. + * + * @returns A pointer to the `otPlatDsoConnection` to use if to accept, or `NULL` if to reject. + * + */ +extern otPlatDsoConnection *otPlatDsoAccept(otInstance *aInstance, const otSockAddr *aPeerSockAddr); + +/** + * This function requests the platform layer to initiate establishing a connection with a peer. + * + * The platform reports the outcome by invoking `otPlatDsoHandleConnected()` callback on success or + * `otPlatDsoHandleDisconnected()` callback (on failure). + * + * @param[in] aConnection The connection. + * @param[in] aPeerSockAddr The socket address (IPv6 address and port number) of the peer to connect to. + * + */ +void otPlatDsoConnect(otPlatDsoConnection *aConnection, const otSockAddr *aPeerSockAddr); + +/** + * This function is a callback from the platform layer to indicate that a connection is successfully established. + * + * It MUST be called either after accepting an incoming connection (`otPlatDsoAccept`) or after a `otPlatDsoConnect()` + * call. + * + * Only after this callback, the connection can be used to send and receive messages. + * + * @param[in] aConnection The connection. + * + */ +extern void otPlatDsoHandleConnected(otPlatDsoConnection *aConnection); + +/** + * This function sends a DSO message to the peer on a connection. + * + * This function is used only after the connection is successfully established (after `otPlatDsoHandleConnected()` + * callback). + * + * This function passes the ownership of the @p aMessage to the DSO platform layer, and the platform implementation is + * expected to free the message once it is no longer needed. + * + * The @p aMessage contains the DNS message (starting with DNS header). Note that it does not contain the the length + * field that is needed when sending over TLS/TCP transport. The platform layer MUST therefore include the length + * field when passing the message to TLS/TCP layer. + * + * @param[in] aConnection The connection to send on. + * @param[in] aMessage The message to send. + * + */ +void otPlatDsoSend(otPlatDsoConnection *aConnection, otMessage *aMessage); + +/** + * This function is a callback from the platform layer to indicate that a DNS message was received over a connection. + * + * The platform MUST call this function only after the connection is successfully established (after callback + * `otPlatDsoHandleConnected()` is invoked). + * + * This function passes the ownership of the @p aMessage from the DSO platform layer to OpenThread. OpenThread will + * free the message when no longer needed. + * + * The @p aMessage MUST contain the DNS message (starting with DNS header) and not include the length field that may + * be included in TCP/TLS exchange. + * + * @param[in] aConnection The connection on which the message was received. + * @param[in] aMessage The received message. + * + */ +extern void otPlatDsoHandleReceive(otPlatDsoConnection *aConnection, otMessage *aMessage); + +/** + * This enumeration defines disconnect modes. + * + */ +typedef enum +{ + OT_PLAT_DSO_DISCONNECT_MODE_GRACEFULLY_CLOSE, ///< Gracefully close the connection. + OT_PLAT_DSO_DISCONNECT_MODE_FORCIBLY_ABORT, ///< Forcibly abort the connection. +} otPlatDsoDisconnectMode; + +/** + * This function requests a connection to be disconnected. + * + * After calling this function, the DSO platform implementation MUST NOT maintain `aConnection` pointer (platform + * MUST NOT call any callbacks using this `Connection` pointer anymore). In particular, calling `otPlatDsoDisconnect()` + * MUST NOT trigger the callback `otPlatDsoHandleDisconnected()`. + * + * @param[in] aConnection The connection to disconnect + * @param[in] aMode The disconnect mode (close gracefully or forcibly abort). + * + */ +void otPlatDsoDisconnect(otPlatDsoConnection *aConnection, otPlatDsoDisconnectMode aMode); + +/** + * This function is a callback from the platform layer to indicate that peer closed/aborted the connection or the + * connection establishment failed (e.g., peer rejected a connection request). + * + * After calling this function, the DSO platform implementation MUST NOT maintain `aConnection` pointer (platform + * MUST NOT call any callbacks using this `Connection` pointer anymore). + * + * @param[in] aConnection The connection which disconnected. + * @param[in] aMode The disconnect mode (closed gracefully or forcibly aborted). + * + */ +extern void otPlatDsoHandleDisconnected(otPlatDsoConnection *aConnection, otPlatDsoDisconnectMode aMode); + +#ifdef __cplusplus +} // extern "C" +#endif + +#endif // OPENTHREAD_PLATFORM_DSO_TRANSPORT_H_ diff --git a/script/check-scan-build b/script/check-scan-build index 7ae01d8c6..3b5331b1f 100755 --- a/script/check-scan-build +++ b/script/check-scan-build @@ -51,6 +51,7 @@ readonly OT_BUILD_OPTIONS=( "-DOT_DHCP6_SERVER=ON" "-DOT_DIAGNOSTIC=ON" "-DOT_DNS_CLIENT=ON" + "-DOT_DNS_DSO=ON" "-DOT_ECDSA=ON" "-DOT_EXTERNAL_MBEDTLS=external" "-DOT_IP6_FRAGM=ON" diff --git a/script/check-simulation-build-autotools b/script/check-simulation-build-autotools index 2280dc5c2..5428b1c9d 100755 --- a/script/check-simulation-build-autotools +++ b/script/check-simulation-build-autotools @@ -54,6 +54,7 @@ build_all_features() "-DOPENTHREAD_CONFIG_DHCP6_SERVER_ENABLE=1" "-DOPENTHREAD_CONFIG_DIAG_ENABLE=1" "-DOPENTHREAD_CONFIG_DNS_CLIENT_ENABLE=1" + "-DOPENTHREAD_CONFIG_DNS_DSO_ENABLE=1" "-DOPENTHREAD_CONFIG_ECDSA_ENABLE=1" "-DOPENTHREAD_CONFIG_HEAP_EXTERNAL_ENABLE=1" "-DOPENTHREAD_CONFIG_HISTORY_TRACKER_ENABLE=1" diff --git a/script/make-pretty b/script/make-pretty index 06477d875..199b6aad7 100755 --- a/script/make-pretty +++ b/script/make-pretty @@ -96,6 +96,7 @@ readonly OT_CLANG_TIDY_BUILD_OPTS=( '-DOT_DHCP6_SERVER=ON' '-DOT_DIAGNOSTIC=ON' '-DOT_DNS_CLIENT=ON' + '-DOT_DNS_DSO=ON' '-DOT_DNSSD_SERVER=ON' '-DOT_DUA=ON' '-DOT_MLR=ON' diff --git a/script/test b/script/test index 9007e596a..5aacf0512 100755 --- a/script/test +++ b/script/test @@ -60,6 +60,7 @@ build_simulation() "-DBUILD_TESTING=ON" "-DOT_ANYCAST_LOCATOR=ON" "-DOT_DNS_CLIENT=ON" + "-DOT_DNS_DSO=ON" "-DOT_DNSSD_SERVER=ON" "-DOT_ECDSA=ON" "-DOT_EXTERNAL_HEAP=ON" diff --git a/src/core/BUILD.gn b/src/core/BUILD.gn index a7969045b..99c7818ac 100644 --- a/src/core/BUILD.gn +++ b/src/core/BUILD.gn @@ -526,6 +526,8 @@ openthread_core_files = [ "net/dhcp6_server.hpp", "net/dns_client.cpp", "net/dns_client.hpp", + "net/dns_dso.cpp", + "net/dns_dso.hpp", "net/dns_types.cpp", "net/dns_types.hpp", "net/dnssd_server.cpp", @@ -743,6 +745,7 @@ source_set("libopenthread_core_config") { "config/dhcp6_server.h", "config/diag.h", "config/dns_client.h", + "config/dns_dso.h", "config/dnssd_server.h", "config/dtls.h", "config/history_tracker.h", diff --git a/src/core/CMakeLists.txt b/src/core/CMakeLists.txt index 16090bacd..2e1d2d6ba 100644 --- a/src/core/CMakeLists.txt +++ b/src/core/CMakeLists.txt @@ -156,6 +156,7 @@ set(COMMON_SOURCES net/dhcp6_client.cpp net/dhcp6_server.cpp net/dns_client.cpp + net/dns_dso.cpp net/dns_types.cpp net/dnssd_server.cpp net/icmp6.cpp diff --git a/src/core/Makefile.am b/src/core/Makefile.am index a3cba67c5..bd37bcf41 100644 --- a/src/core/Makefile.am +++ b/src/core/Makefile.am @@ -246,6 +246,7 @@ SOURCES_COMMON = \ net/dhcp6_client.cpp \ net/dhcp6_server.cpp \ net/dns_client.cpp \ + net/dns_dso.cpp \ net/dns_types.cpp \ net/dnssd_server.cpp \ net/icmp6.cpp \ @@ -479,6 +480,7 @@ HEADERS_COMMON = \ config/dhcp6_server.h \ config/diag.h \ config/dns_client.h \ + config/dns_dso.h \ config/dnssd_server.h \ config/dtls.h \ config/history_tracker.h \ @@ -543,6 +545,7 @@ HEADERS_COMMON = \ net/dhcp6_client.hpp \ net/dhcp6_server.hpp \ net/dns_client.hpp \ + net/dns_dso.hpp \ net/dns_types.hpp \ net/dnssd_server.hpp \ net/icmp6.hpp \ diff --git a/src/core/common/instance.hpp b/src/core/common/instance.hpp index 2757ad302..fa1b26f79 100644 --- a/src/core/common/instance.hpp +++ b/src/core/common/instance.hpp @@ -746,6 +746,13 @@ template <> inline Dns::ServiceDiscovery::Server &Instance::Get(void) } #endif +#if OPENTHREAD_CONFIG_DNS_DSO_ENABLE +template <> inline Dns::Dso &Instance::Get(void) +{ + return mThreadNetif.mDnsDso; +} +#endif + #if OPENTHREAD_FTD || OPENTHREAD_CONFIG_TMF_NETWORK_DIAG_MTD_ENABLE template <> inline NetworkDiagnostic::NetworkDiagnostic &Instance::Get(void) { diff --git a/src/core/common/time.hpp b/src/core/common/time.hpp index b214cb36b..bff16f78f 100644 --- a/src/core/common/time.hpp +++ b/src/core/common/time.hpp @@ -240,18 +240,22 @@ public: /** * This static method converts a given number of seconds to milliseconds. * + * @param[in] aSeconds The seconds value to convert to milliseconds. + * * @returns The number of milliseconds. * */ - static uint32_t SecToMsec(uint32_t aSeconds) { return aSeconds * 1000u; } + static uint32_t constexpr SecToMsec(uint32_t aSeconds) { return aSeconds * 1000u; } /** * This static method converts a given number of milliseconds to seconds. * + * @param[in] aMilliseconds The milliseconds value to convert to seconds. + * * @returns The number of seconds. * */ - static uint32_t MsecToSec(uint32_t aMilliseconds) { return aMilliseconds / 1000u; } + static uint32_t constexpr MsecToSec(uint32_t aMilliseconds) { return aMilliseconds / 1000u; } private: static constexpr uint32_t kDistantFuture = (1UL << 31); diff --git a/src/core/config/dns_dso.h b/src/core/config/dns_dso.h new file mode 100644 index 000000000..65a7171d4 --- /dev/null +++ b/src/core/config/dns_dso.h @@ -0,0 +1,78 @@ +/* + * Copyright (c) 2021, The OpenThread Authors. + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * 3. Neither the name of the copyright holder nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ + +/** + * @file + * This file includes compile-time configurations for the DNS Stateful Operations (DSO). + * + */ + +#ifndef CONFIG_DNS_DSO_H_ +#define CONFIG_DNS_DSO_H_ + +/** + * @def OPENTHREAD_CONFIG_DNS_DSO_ENABLE + * + * Define to 1 to enable DSO support. + * + */ +#ifndef OPENTHREAD_CONFIG_DNS_DSO_ENABLE +#define OPENTHREAD_CONFIG_DNS_DSO_ENABLE 0 +#endif + +/** + * @def OPENTHREAD_CONFIG_DNS_DSO_CONNECTING_TIMEOUT + * + * Specifies the maximum time (in msec) waiting for a connection to be established by DSO platform layer. + * + */ +#ifndef OPENTHREAD_CONFIG_DNS_DSO_CONNECTING_TIMEOUT +#define OPENTHREAD_CONFIG_DNS_DSO_CONNECTING_TIMEOUT (45 * 1000) +#endif + +/** + * @def OPENTHREAD_CONFIG_DNS_DSO_RESPONSE_TIMEOUT + * + * Specifies the maximum time (in msec) waiting for a response to a request. + * + */ +#ifndef OPENTHREAD_CONFIG_DNS_DSO_RESPONSE_TIMEOUT +#define OPENTHREAD_CONFIG_DNS_DSO_RESPONSE_TIMEOUT (30 * 1000) +#endif + +/** + * @def OPENTHREAD_CONFIG_DNS_DSO_MAX_PENDING_REQUESTS + * + * Specifies the maximum number of pending requests per DSO session. + * + */ +#ifndef OPENTHREAD_CONFIG_DNS_DSO_MAX_PENDING_REQUESTS +#define OPENTHREAD_CONFIG_DNS_DSO_MAX_PENDING_REQUESTS 3 +#endif + +#endif // CONFIG_DNS_DSO_H_ diff --git a/src/core/net/dns_dso.cpp b/src/core/net/dns_dso.cpp new file mode 100644 index 000000000..06712a53d --- /dev/null +++ b/src/core/net/dns_dso.cpp @@ -0,0 +1,1535 @@ +/* + * Copyright (c) 2021, 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 "dns_dso.hpp" + +#if OPENTHREAD_CONFIG_DNS_DSO_ENABLE + +#include "common/as_core_type.hpp" +#include "common/code_utils.hpp" +#include "common/debug.hpp" +#include "common/instance.hpp" +#include "common/locator_getters.hpp" +#include "common/logging.hpp" +#include "common/random.hpp" + +/** + * @file + * This file implements the DNS Stateful Operations (DSO) per RFC 8490. + */ + +namespace ot { +namespace Dns { + +//--------------------------------------------------------------------------------------------------------------------- +// otPlatDso transport callbacks + +extern "C" otInstance *otPlatDsoGetInstance(otPlatDsoConnection *aConnection) +{ + return &AsCoreType(aConnection).GetInstance(); +} + +extern "C" otPlatDsoConnection *otPlatDsoAccept(otInstance *aInstance, const otSockAddr *aPeerSockAddr) +{ + return AsCoreType(aInstance).Get().AcceptConnection(AsCoreType(aPeerSockAddr)); +} + +extern "C" void otPlatDsoHandleConnected(otPlatDsoConnection *aConnection) +{ + AsCoreType(aConnection).HandleConnected(); +} + +extern "C" void otPlatDsoHandleReceive(otPlatDsoConnection *aConnection, otMessage *aMessage) +{ + AsCoreType(aConnection).HandleReceive(AsCoreType(aMessage)); +} + +extern "C" void otPlatDsoHandleDisconnected(otPlatDsoConnection *aConnection, otPlatDsoDisconnectMode aMode) +{ + AsCoreType(aConnection).HandleDisconnected(MapEnum(aMode)); +} + +//--------------------------------------------------------------------------------------------------------------------- +// Dso::Connection + +Dso::Connection::Connection(Instance & aInstance, + const Ip6::SockAddr &aPeerSockAddr, + Callbacks & aCallbacks, + uint32_t aInactivityTimeout, + uint32_t aKeepAliveInterval) + : InstanceLocator(aInstance) + , mNext(nullptr) + , mCallbacks(aCallbacks) + , mPeerSockAddr(aPeerSockAddr) + , mState(kStateDisconnected) + , mIsServer(false) + , mInactivity(aInactivityTimeout) + , mKeepAlive(aKeepAliveInterval) +{ + OT_ASSERT(aKeepAliveInterval >= kMinKeepAliveInterval); + Init(/* aIsServer */ false); +} + +void Dso::Connection::Init(bool aIsServer) +{ + mNextMessageId = 1; + mIsServer = aIsServer; + mStateDidChange = false; + mLongLivedOperation = false; + mRetryDelay = 0; + mRetryDelayErrorCode = Dns::Header::kResponseSuccess; + mDisconnectReason = kReasonUnknown; +} + +void Dso::Connection::SetState(State aState) +{ + VerifyOrExit(mState != aState); + + otLogInfoDns("[dso] State: %s -> %s on connection with %s", StateToString(mState), StateToString(aState), + mPeerSockAddr.ToString().AsCString()); + + mState = aState; + mStateDidChange = true; + +exit: + return; +} + +void Dso::Connection::SignalAnyStateChange(void) +{ + VerifyOrExit(mStateDidChange); + mStateDidChange = false; + + switch (mState) + { + case kStateDisconnected: + mCallbacks.mHandleDisconnected(*this); + break; + + case kStateConnectedButSessionless: + mCallbacks.mHandleConnected(*this); + break; + + case kStateSessionEstablished: + mCallbacks.mHandleSessionEstablished(*this); + break; + + case kStateConnecting: + case kStateEstablishingSession: + break; + }; + +exit: + return; +} + +Message *Dso::Connection::NewMessage(void) +{ + return Get().Allocate(Message::kTypeOther, sizeof(Dns::Header), + Message::Settings(Message::kPriorityNormal)); +} + +void Dso::Connection::Connect(void) +{ + OT_ASSERT(mState == kStateDisconnected); + + Init(/* aIsServer */ false); + Get().mClientConnections.Push(*this); + MarkAsConnecting(); + otPlatDsoConnect(this, &mPeerSockAddr); +} + +void Dso::Connection::Accept(void) +{ + OT_ASSERT(mState == kStateDisconnected); + + Init(/* aIsServer */ true); + Get().mServerConnections.Push(*this); + MarkAsConnecting(); +} + +void Dso::Connection::MarkAsConnecting(void) +{ + SetState(kStateConnecting); + + // While in `kStateConnecting` state we use the `mKeepAlive` to + // track the `kConnectingTimeout` (if connection is not established + // within the timeout, we consider it as failure and close it). + + mKeepAlive.SetExpirationTime(TimerMilli::GetNow() + kConnectingTimeout); + Get().mTimer.FireAtIfEarlier(mKeepAlive.GetExpirationTime()); + + // Wait for `HandleConnected()` or `HandleDisconnected()` callbacks + // or timeout. +} + +void Dso::Connection::HandleConnected(void) +{ + OT_ASSERT(mState == kStateConnecting); + + SetState(kStateConnectedButSessionless); + ResetTimeouts(/* aIsKeepAliveMessage */ false); + + SignalAnyStateChange(); +} + +void Dso::Connection::Disconnect(DisconnectMode aMode, DisconnectReason aReason) +{ + VerifyOrExit(mState != kStateDisconnected); + + mDisconnectReason = aReason; + MarkAsDisconnected(); + + otPlatDsoDisconnect(this, MapEnum(aMode)); + +exit: + return; +} + +void Dso::Connection::HandleDisconnected(DisconnectMode aMode) +{ + VerifyOrExit(mState != kStateDisconnected); + + if (mState == kStateConnecting) + { + mDisconnectReason = kReasonFailedToConnect; + } + else + { + switch (aMode) + { + case kGracefullyClose: + mDisconnectReason = kReasonPeerClosed; + break; + + case kForciblyAbort: + mDisconnectReason = kReasonPeerAborted; + } + } + + MarkAsDisconnected(); + SignalAnyStateChange(); + +exit: + return; +} + +void Dso::Connection::MarkAsDisconnected(void) +{ + if (IsClient()) + { + IgnoreError(Get().mClientConnections.Remove(*this)); + } + else + { + IgnoreError(Get().mServerConnections.Remove(*this)); + } + + mPendingRequests.Clear(); + SetState(kStateDisconnected); + + otLogInfoDns("[dso] Disconnect reason: %s", DisconnectReasonToString(mDisconnectReason)); +} + +void Dso::Connection::MarkSessionEstablished(void) +{ + switch (mState) + { + case kStateConnectedButSessionless: + case kStateEstablishingSession: + case kStateSessionEstablished: + break; + + case kStateDisconnected: + case kStateConnecting: + OT_ASSERT(false); + } + + SetState(kStateSessionEstablished); +} + +Error Dso::Connection::SendRequestMessage(Message &aMessage, MessageId &aMessageId, uint32_t aResponseTimeout) +{ + return SendMessage(aMessage, kRequestMessage, aMessageId, Dns::Header::kResponseSuccess, aResponseTimeout); +} + +Error Dso::Connection::SendUnidirectionalMessage(Message &aMessage) +{ + MessageId messageId = 0; + + return SendMessage(aMessage, kUnidirectionalMessage, messageId); +} + +Error Dso::Connection::SendResponseMessage(Message &aMessage, MessageId aResponseId) +{ + return SendMessage(aMessage, kResponseMessage, aResponseId); +} + +void Dso::Connection::SetLongLivedOperation(bool aLongLivedOperation) +{ + VerifyOrExit(mLongLivedOperation != aLongLivedOperation); + + mLongLivedOperation = aLongLivedOperation; + + otLogInfoDns("[dso] Long-lived operation %s", mLongLivedOperation ? "started" : "stopped"); + + if (!mLongLivedOperation) + { + TimeMilli now = TimerMilli::GetNow(); + TimeMilli nextTime; + + nextTime = GetNextFireTime(now); + + if (nextTime != now.GetDistantFuture()) + { + Get().mTimer.FireAtIfEarlier(nextTime); + } + } + +exit: + return; +} + +Error Dso::Connection::SendRetryDelayMessage(uint32_t aDelay, Dns::Header::Response aResponseCode) +{ + Error error = kErrorNone; + Message * message = nullptr; + RetryDelayTlv retryDelayTlv; + MessageId messageId; + + switch (mState) + { + case kStateSessionEstablished: + OT_ASSERT(IsServer()); + break; + + case kStateConnectedButSessionless: + case kStateEstablishingSession: + case kStateDisconnected: + case kStateConnecting: + OT_ASSERT(false); + } + + message = NewMessage(); + VerifyOrExit(message != nullptr, error = kErrorNoBufs); + + retryDelayTlv.Init(); + retryDelayTlv.SetRetryDelay(aDelay); + SuccessOrExit(error = message->Append(retryDelayTlv)); + error = SendMessage(*message, kUnidirectionalMessage, messageId, aResponseCode); + +exit: + FreeMessageOnError(message, error); + return error; +} + +Error Dso::Connection::SetTimeouts(uint32_t aInactivityTimeout, uint32_t aKeepAliveInterval) +{ + Error error = kErrorNone; + + VerifyOrExit(aKeepAliveInterval >= kMinKeepAliveInterval, error = kErrorInvalidArgs); + + // If acting as server, the timeout values are the ones we grant + // to a connecting clients. If acting as client, the timeout + // values are what to request when sending Keep Alive message. + // If in `kStateDisconnected` we set both (since we don't know + // yet whether we are going to connect as client or server). + + if ((mState == kStateDisconnected) || IsServer()) + { + mKeepAlive.SetInterval(aKeepAliveInterval); + AdjustInactivityTimeout(aInactivityTimeout); + } + + if ((mState == kStateDisconnected) || IsClient()) + { + mKeepAlive.SetRequestInterval(aKeepAliveInterval); + mInactivity.SetRequestInterval(aInactivityTimeout); + } + + switch (mState) + { + case kStateDisconnected: + case kStateConnecting: + break; + + case kStateConnectedButSessionless: + case kStateEstablishingSession: + if (IsServer()) + { + break; + } + + OT_FALL_THROUGH; + + case kStateSessionEstablished: + error = SendKeepAliveMessage(); + } + +exit: + return error; +} + +Error Dso::Connection::SendKeepAliveMessage(void) +{ + return SendKeepAliveMessage(IsServer() ? kUnidirectionalMessage : kRequestMessage, 0); +} + +Error Dso::Connection::SendKeepAliveMessage(MessageType aMessageType, MessageId aResponseId) +{ + // Sends a Keep Alive message of a given type. This is a common + // method used by both client and server. `aResponseId` is + // applicable and used only when the message type is + // `kResponseMessage`. + + Error error = kErrorNone; + Message * message = nullptr; + KeepAliveTlv keepAliveTlv; + + switch (mState) + { + case kStateConnectedButSessionless: + case kStateEstablishingSession: + if (IsServer()) + { + // While session is being established, server is only allowed + // to send a Keep Alive response to a request from client. + OT_ASSERT(aMessageType == kResponseMessage); + } + break; + + case kStateSessionEstablished: + break; + + case kStateDisconnected: + case kStateConnecting: + OT_ASSERT(false); + } + + // Server can send Keep Alive response (to a request from client) + // or a unidirectional Keep Alive message. Client can send + // KeepAlive request message. + + if (IsServer()) + { + if (aMessageType == kResponseMessage) + { + OT_ASSERT(aResponseId != 0); + } + else + { + OT_ASSERT(aMessageType == kUnidirectionalMessage); + } + } + else + { + OT_ASSERT(aMessageType == kRequestMessage); + } + + message = NewMessage(); + VerifyOrExit(message != nullptr, error = kErrorNoBufs); + + keepAliveTlv.Init(); + + if (IsServer()) + { + keepAliveTlv.SetInactivityTimeout(mInactivity.GetInterval()); + keepAliveTlv.SetKeepAliveInterval(mKeepAlive.GetInterval()); + } + else + { + keepAliveTlv.SetInactivityTimeout(mInactivity.GetRequestInterval()); + keepAliveTlv.SetKeepAliveInterval(mKeepAlive.GetRequestInterval()); + } + + SuccessOrExit(error = message->Append(keepAliveTlv)); + + error = SendMessage(*message, aMessageType, aResponseId); + +exit: + FreeMessageOnError(message, error); + return error; +} + +Error Dso::Connection::SendMessage(Message & aMessage, + MessageType aMessageType, + MessageId & aMessageId, + Dns::Header::Response aResponseCode, + uint32_t aResponseTimeout) +{ + Error error = kErrorNone; + Tlv::Type primaryTlvType = Tlv::kReservedType; + Dns::Header header; + + switch (mState) + { + case kStateConnectedButSessionless: + // To establish session, client MUST send a request message. + // Server is not allowed to send any messages. Unidirectional + // messages are not allowed before session is established. + OT_ASSERT(IsClient()); + OT_ASSERT(aMessageType == kRequestMessage); + break; + + case kStateEstablishingSession: + // During session establishment, client is allowed to send + // additional request messages, server is only allowed to + // send response. + if (IsClient()) + { + OT_ASSERT(aMessageType == kRequestMessage); + } + else + { + OT_ASSERT(aMessageType == kResponseMessage); + } + break; + + case kStateSessionEstablished: + // All message types are allowed. + break; + + case kStateDisconnected: + case kStateConnecting: + OT_ASSERT(false); + } + + // A DSO request or unidirectional message MUST contain at + // least one TLV. The first TLV is the "Primary TLV" and + // determines the nature of the operation being performed. + // A DSO response message may contain no TLVs, or may contain + // one or more TLVs. Response Primary TLV(s) MUST appear first + // in a DSO response message. + + aMessage.SetOffset(0); + IgnoreError(ReadPrimaryTlv(aMessage, primaryTlvType)); + + switch (aMessageType) + { + case kResponseMessage: + break; + case kRequestMessage: + case kUnidirectionalMessage: + OT_ASSERT(primaryTlvType != Tlv::kReservedType); + } + + // `header` is cleared from its constructor call so all fields + // start as zero. + + switch (aMessageType) + { + case kRequestMessage: + header.SetType(Dns::Header::kTypeQuery); + aMessageId = mNextMessageId; + break; + + case kResponseMessage: + header.SetType(Dns::Header::kTypeResponse); + break; + + case kUnidirectionalMessage: + header.SetType(Dns::Header::kTypeQuery); + aMessageId = 0; + break; + } + + header.SetMessageId(aMessageId); + header.SetQueryType(Dns::Header::kQueryTypeDso); + header.SetResponseCode(aResponseCode); + SuccessOrExit(error = aMessage.Prepend(header)); + + SuccessOrExit(error = AppendPadding(aMessage)); + + // Update `mPendingRequests` list with the new request info + + if (aMessageType == kRequestMessage) + { + SuccessOrExit( + error = mPendingRequests.Add(mNextMessageId, primaryTlvType, TimerMilli::GetNow() + aResponseTimeout)); + + if (++mNextMessageId == 0) + { + mNextMessageId = 1; + } + } + + otLogInfoDns("[dso] Sending %s message with id %u to %s", MessageTypeToString(aMessageType), aMessageId, + mPeerSockAddr.ToString().AsCString()); + + switch (mState) + { + case kStateConnectedButSessionless: + // On client we transition from "connected" state to + // "establishing session" state on successfully sending a + // request message. + if (IsClient()) + { + SetState(kStateEstablishingSession); + } + break; + + case kStateEstablishingSession: + // On server we transition from "establishing session" state + // to "established" on sending a response with success + // response code. + if (IsServer() && (aResponseCode == Dns::Header::kResponseSuccess)) + { + SetState(kStateSessionEstablished); + } + + default: + break; + } + + ResetTimeouts(/* aIsKeepAliveMessage*/ (primaryTlvType == KeepAliveTlv::kType)); + + otPlatDsoSend(this, &aMessage); + + // Signal any state changes. This is done at the very end when the + // `SendMessage()` is fully processed (all state and local + // variables are updated) to ensure that we do not have any + // reentrancy issues (e.g., if the callback signalling state + // change triggers another tx). + + SignalAnyStateChange(); + +exit: + return error; +} + +Error Dso::Connection::AppendPadding(Message &aMessage) +{ + // This method appends Encryption Padding TLV to a DSO message. + // It uses the padding policy "Random-Block-Length Padding" from + // RFC 8467. + + static const uint16_t kBlockLengths[] = {8, 11, 17, 21}; + + Error error = kErrorNone; + uint16_t blockLength; + EncryptionPaddingTlv paddingTlv; + + // We pick a random block length. The random selection can be + // based on a "weak" source of randomness (so the use of + // `NonCrypto` is fine). We add padding to the message such + // that its padded length is a multiple of the chosen block + // length. + + blockLength = kBlockLengths[Random::NonCrypto::GetUint8InRange(0, OT_ARRAY_LENGTH(kBlockLengths))]; + + paddingTlv.Init((blockLength - ((aMessage.GetLength() + sizeof(Tlv)) % blockLength)) % blockLength); + + SuccessOrExit(error = aMessage.Append(paddingTlv)); + + for (uint16_t len = paddingTlv.GetLength(); len > 0; len--) + { + SuccessOrExit(error = aMessage.Append(0)); + } + +exit: + return error; +} + +void Dso::Connection::HandleReceive(Message &aMessage) +{ + Error error = kErrorAbort; + Tlv::Type primaryTlvType = Tlv::kReservedType; + Dns::Header header; + + SuccessOrExit(aMessage.Read(0, header)); + + if (header.GetQueryType() != Dns::Header::kQueryTypeDso) + { + if (header.GetType() == Dns::Header::kTypeQuery) + { + SendErrorResponse(header, Dns::Header::kResponseNotImplemented); + error = kErrorNone; + } + + ExitNow(); + } + + switch (mState) + { + case kStateConnectedButSessionless: + // After connection is established, client should initiate + // establishing session (by sending a request). So no rx is + // allowed before this. On server, we allow rx of a request + // message only. + VerifyOrExit(IsServer() && (header.GetType() == Dns::Header::kTypeQuery) && (header.GetMessageId() != 0)); + break; + + case kStateEstablishingSession: + // Unidirectional message are allowed after session is + // established. While session is being established, on client, + // we allow rx on response message. On server we can rx + // request or response. + + VerifyOrExit(header.GetMessageId() != 0); + + if (IsClient()) + { + VerifyOrExit(header.GetType() == Dns::Header::kTypeResponse); + } + break; + + case kStateSessionEstablished: + // All message types are allowed. + break; + + case kStateDisconnected: + case kStateConnecting: + ExitNow(); + } + + // All count fields MUST be set to zero in the header. + VerifyOrExit((header.GetQuestionCount() == 0) && (header.GetAnswerCount() == 0) && + (header.GetAuthorityRecordCount() == 0) && (header.GetAdditionalRecordCount() == 0)); + + aMessage.SetOffset(sizeof(header)); + + switch (ReadPrimaryTlv(aMessage, primaryTlvType)) + { + case kErrorNone: + VerifyOrExit(primaryTlvType != Tlv::kReservedType); + break; + + case kErrorNotFound: + // The `primaryTlvType` is set to `Tlv::kReservedType` + // (value zero) to indicate that there is no primary TLV. + break; + + default: + ExitNow(); + } + + switch (header.GetType()) + { + case Dns::Header::kTypeQuery: + error = ProcessRequestOrUnidirectionalMessage(header, aMessage, primaryTlvType); + break; + + case Dns::Header::kTypeResponse: + error = ProcessResponseMessage(header, aMessage, primaryTlvType); + break; + } + +exit: + aMessage.Free(); + + if (error == kErrorNone) + { + ResetTimeouts(/* aIsKeepAliveMessage */ (primaryTlvType == KeepAliveTlv::kType)); + } + else + { + Disconnect(kForciblyAbort, kReasonPeerMisbehavior); + } + + // We signal any state change at the very end when the received + // message is fully processed (all state and local variables are + // updated) to ensure that we do not have any reentrancy issues + // (e.g., if a `Connection` method happens to be called from the + // callback). + + SignalAnyStateChange(); +} + +Error Dso::Connection::ReadPrimaryTlv(const Message &aMessage, Tlv::Type &aPrimaryTlvType) const +{ + // Read and validate the primary TLV (first TLV after the header). + // The `aMessage.GetOffset()` must point to the first TLV. If no + // TLV then `kErrorNotFound` is returned. If TLV in message is not + // well-formed `kErrorParse` is returned. The read TLV type is + // returned in `aPrimaryTlvType` (set to `Tlv::kReservedType` + // (value zero) when `kErrorNotFound`). + + Error error = kErrorNotFound; + Tlv tlv; + + aPrimaryTlvType = Tlv::kReservedType; + + SuccessOrExit(aMessage.Read(aMessage.GetOffset(), tlv)); + VerifyOrExit(aMessage.GetOffset() + tlv.GetSize() <= aMessage.GetLength(), error = kErrorParse); + aPrimaryTlvType = tlv.GetType(); + error = kErrorNone; + +exit: + return error; +} + +Error Dso::Connection::ProcessRequestOrUnidirectionalMessage(const Dns::Header &aHeader, + const Message & aMessage, + Tlv::Type aPrimaryTlvType) +{ + Error error = kErrorAbort; + + if (IsServer() && (mState == kStateConnectedButSessionless)) + { + SetState(kStateEstablishingSession); + } + + // A DSO request or unidirectional message MUST contain at + // least one TLV which is the "Primary TLV" and determines + // the nature of the operation being performed. + + switch (aPrimaryTlvType) + { + case KeepAliveTlv::kType: + error = ProcessKeepAliveMessage(aHeader, aMessage); + break; + + case RetryDelayTlv::kType: + error = ProcessRetryDelayMessage(aHeader, aMessage); + break; + + case Tlv::kReservedType: + case EncryptionPaddingTlv::kType: + // Misbehavior by peer. + break; + + default: + if (aHeader.GetMessageId() == 0) + { + otLogInfoDns("[dso] Received unidirectional message from %s", mPeerSockAddr.ToString().AsCString()); + + error = mCallbacks.mProcessUnidirectionalMessage(*this, aMessage, aPrimaryTlvType); + } + else + { + MessageId messageId = aHeader.GetMessageId(); + + otLogInfoDns("[dso] Received request message with id %u from %s", messageId, + mPeerSockAddr.ToString().AsCString()); + + error = mCallbacks.mProcessRequestMessage(*this, messageId, aMessage, aPrimaryTlvType); + + // `kErrorNotFound` indicates that TLV type is not known. + + if (error == kErrorNotFound) + { + SendErrorResponse(aHeader, Dns::Header::kDsoTypeNotImplemented); + error = kErrorNone; + } + } + break; + } + + return error; +} + +Error Dso::Connection::ProcessResponseMessage(const Dns::Header &aHeader, + const Message & aMessage, + Tlv::Type aPrimaryTlvType) +{ + Error error = kErrorAbort; + Tlv::Type requestPrimaryTlvType; + + // If a client or server receives a response where the message + // ID is zero, or is any other value that does not match the + // message ID of any of its outstanding operations, this is a + // fatal error and the recipient MUST forcibly abort the + // connection immediately. + + VerifyOrExit(aHeader.GetMessageId() != 0); + VerifyOrExit(mPendingRequests.Contains(aHeader.GetMessageId(), requestPrimaryTlvType)); + + // If the response has no error and contains a primary TLV, it + // MUST match the request primary TLV. + + if ((aHeader.GetResponseCode() == Dns::Header::kResponseSuccess) && (aPrimaryTlvType != Tlv::kReservedType)) + { + VerifyOrExit(aPrimaryTlvType == requestPrimaryTlvType); + } + + mPendingRequests.Remove(aHeader.GetMessageId()); + + switch (requestPrimaryTlvType) + { + case KeepAliveTlv::kType: + SuccessOrExit(error = ProcessKeepAliveMessage(aHeader, aMessage)); + break; + + default: + SuccessOrExit(error = mCallbacks.mProcessResponseMessage(*this, aHeader, aMessage, aPrimaryTlvType, + requestPrimaryTlvType)); + break; + } + + // DSO session is established when client sends a request message + // and receives a response from server with no error code. + + if (IsClient() && (mState == kStateEstablishingSession) && + (aHeader.GetResponseCode() == Dns::Header::kResponseSuccess)) + { + SetState(kStateSessionEstablished); + } + +exit: + return error; +} + +Error Dso::Connection::ProcessKeepAliveMessage(const Dns::Header &aHeader, const Message &aMessage) +{ + Error error = kErrorAbort; + uint16_t offset = aMessage.GetOffset(); + Tlv tlv; + KeepAliveTlv keepAliveTlv; + + if (aHeader.GetType() == Dns::Header::kTypeResponse) + { + // A Keep Alive response message is allowed on a client from a sever. + + VerifyOrExit(IsClient()); + + if (aHeader.GetResponseCode() != Dns::Header::kResponseSuccess) + { + // We got an error response code from server for our + // Keep Alive request message. If this happens while + // establishing the DSO session, it indicates that server + // does not support DSO, so we close the connection. If + // this happens while session is already established, it + // is a misbehavior (fatal error) by server. + + if (mState == kStateEstablishingSession) + { + Disconnect(kGracefullyClose, kReasonPeerDoesNotSupportDso); + error = kErrorNone; + } + + ExitNow(); + } + } + + // Parse and validate the Keep Alive Message + + SuccessOrExit(aMessage.Read(offset, keepAliveTlv)); + offset += keepAliveTlv.GetSize(); + + VerifyOrExit((keepAliveTlv.GetType() == KeepAliveTlv::kType) && keepAliveTlv.IsValid()); + + // Keep Alive message MUST contain only one Keep Alive TLV. + + while (offset < aMessage.GetLength()) + { + SuccessOrExit(aMessage.Read(offset, tlv)); + offset += tlv.GetSize(); + + VerifyOrExit((tlv.GetType() != KeepAliveTlv::kType) && (tlv.GetType() != RetryDelayTlv::kType)); + } + + VerifyOrExit(offset == aMessage.GetLength()); + + if (aHeader.GetType() == Dns::Header::kTypeQuery) + { + if (IsServer()) + { + // Received a Keep Alive message from client. It MUST + // be a request message (not unidirectional). We prepare + // and send a Keep Alive response. + + VerifyOrExit(aHeader.GetMessageId() != 0); + + otLogInfoDns("[dso] Received KeepAlive request message from client %s", + mPeerSockAddr.ToString().AsCString()); + + IgnoreError(SendKeepAliveMessage(kResponseMessage, aHeader.GetMessageId())); + error = kErrorNone; + ExitNow(); + } + + // Received a Keep Alive message on client from server. Server + // Keep Alive message MUST be unidirectional (message ID + // zero). + + VerifyOrExit(aHeader.GetMessageId() == 0); + } + + otLogInfoDns("[dso] Received Keep Alive %s message from server %s", + (aHeader.GetMessageId() == 0) ? "unidirectional" : "response", mPeerSockAddr.ToString().AsCString()); + + // Receiving a Keep Alive interval value from server less than the + // minimum (ten seconds) is a fatal error and client MUST then + // abort the connection. + + VerifyOrExit(keepAliveTlv.GetKeepAliveInterval() >= kMinKeepAliveInterval); + + // Update the timeout intervals on the connection from + // the new values we got from the server. The receive + // of the Keep Alive message does not itself reset the + // inactivity timer. So we use `AdjustInactivityTimeout` + // which takes into account the time elapsed since the + // last activity. + + AdjustInactivityTimeout(keepAliveTlv.GetInactivityTimeout()); + mKeepAlive.SetInterval(keepAliveTlv.GetKeepAliveInterval()); + + otLogInfoDns("[dso] Timeouts Inactivity:%u, KeepAlive:%u", mInactivity.GetInterval(), mKeepAlive.GetInterval()); + + error = kErrorNone; + +exit: + return error; +} + +Error Dso::Connection::ProcessRetryDelayMessage(const Dns::Header &aHeader, const Message &aMessage) + +{ + Error error = kErrorAbort; + RetryDelayTlv retryDelayTlv; + + // Retry Delay TLV can be used as the Primary TLV only in + // a unidirectional message sent from server to client. + // It is used by the server to instruct the client to + // close the session and its underlying connection, and not + // to reconnect for the indicated time interval. + + VerifyOrExit(IsClient() && (aHeader.GetMessageId() == 0)); + + SuccessOrExit(aMessage.Read(aMessage.GetOffset(), retryDelayTlv)); + VerifyOrExit(retryDelayTlv.IsValid()); + + mRetryDelayErrorCode = aHeader.GetResponseCode(); + mRetryDelay = retryDelayTlv.GetRetryDelay(); + + otLogInfoDns("[dso] Received Retry Delay message from server %s", mPeerSockAddr.ToString().AsCString()); + otLogInfoDns("[dso] RetryDelay:%u ms, ResponseCode:%d", mRetryDelay, mRetryDelayErrorCode); + + Disconnect(kGracefullyClose, kReasonServerRetryDelayRequest); + +exit: + return error; +} + +void Dso::Connection::SendErrorResponse(const Dns::Header &aHeader, Dns::Header::Response aResponseCode) +{ + Message * response = NewMessage(); + Dns::Header header; + + VerifyOrExit(response != nullptr); + + header.SetMessageId(aHeader.GetMessageId()); + header.SetType(Dns::Header::kTypeResponse); + header.SetQueryType(aHeader.GetQueryType()); + header.SetResponseCode(aResponseCode); + + SuccessOrExit(response->Prepend(header)); + + otPlatDsoSend(this, response); + response = nullptr; + +exit: + FreeMessage(response); +} + +void Dso::Connection::AdjustInactivityTimeout(uint32_t aNewTimeout) +{ + // This method sets the inactivity timeout interval to a new value + // and updates the expiration time based on the new timeout value. + // + // On client, it is called on receiving a Keep Alive response or + // unidirectional message from server. Note that the receive of + // the Keep Alive message does not itself reset the inactivity + // timer. So the time elapsed since the last activity should be + // taken into account with the new inactivity timeout value. + // + // On server this method is called from `SetTimeouts()` when a new + // inactivity timeout value is set. + + TimeMilli now = TimerMilli::GetNow(); + TimeMilli start; + TimeMilli newExpiration; + + if (mState == kStateDisconnected) + { + mInactivity.SetInterval(aNewTimeout); + ExitNow(); + } + + VerifyOrExit(aNewTimeout != mInactivity.GetInterval()); + + // Calculate the start time (i.e., the last time inactivity timer + // was cleared). If the previous inactivity time is set to + // `kInfinite` value (`IsUsed()` returns `false`) then + // `GetExpirationTime()` returns the start time. Otherwise, we + // calculate it going back from the current expiration time with + // the current wait interval. + + if (!mInactivity.IsUsed()) + { + start = mInactivity.GetExpirationTime(); + } + else if (IsClient()) + { + start = mInactivity.GetExpirationTime() - mInactivity.GetInterval(); + } + else + { + start = mInactivity.GetExpirationTime() - CalculateServerInactivityWaitTime(); + } + + mInactivity.SetInterval(aNewTimeout); + + if (!mInactivity.IsUsed()) + { + newExpiration = start; + } + else if (IsClient()) + { + newExpiration = start + aNewTimeout; + + if (newExpiration < now) + { + newExpiration = now; + } + } + else + { + newExpiration = start + CalculateServerInactivityWaitTime(); + + if (newExpiration < now) + { + // If the server abruptly reduces the inactivity timeout + // such that current elapsed time is already more than + // twice the new inactivity timeout, then the client is + // immediately considered delinquent (server can forcibly + // abort the connection). So to give the client time to + // close the connection gracefully, the server SHOULD + // give the client an additional grace period of either + // five seconds or one quarter of the new inactivity + // timeout, whichever is greater [RFC 8490 - 7.1.1]. + + newExpiration = now + OT_MAX(kMinServerInactivityWaitTime, aNewTimeout / 4); + } + } + + mInactivity.SetExpirationTime(newExpiration); + +exit: + return; +} + +uint32_t Dso::Connection::CalculateServerInactivityWaitTime(void) const +{ + // A server will abort an idle session after five seconds + // (`kMinServerInactivityWaitTime`) or twice the inactivity + // timeout value, whichever is greater [RFC 8490 - 6.4.1]. + + OT_ASSERT(mInactivity.IsUsed()); + + return OT_MAX(mInactivity.GetInterval() * 2, kMinServerInactivityWaitTime); +} + +void Dso::Connection::ResetTimeouts(bool aIsKeepAliveMessage) +{ + TimeMilli now = TimerMilli::GetNow(); + TimeMilli nextTime; + + // At both servers and clients, the generation or reception of any + // complete DNS message resets both timers for that DSO + // session, with the one exception being that a DSO Keep Alive + // message resets only the keep alive timer, not the inactivity + // timeout timer [RFC 8490 - 6.3] + + if (mKeepAlive.IsUsed()) + { + // On client, we wait for the Keep Alive interval but on server + // we wait for twice the interval before considering Keep Alive + // timeout. + // + // Note that we limit the interval to `Timeout::kMaxInterval` + // (which is ~12 days). This max limit ensures that even twice + // the interval is less than max OpenThread timer duration so + // that the expiration time calculations below stay within the + // `TimerMilli` range. + + mKeepAlive.SetExpirationTime(now + mKeepAlive.GetInterval() * (IsServer() ? 2 : 1)); + } + + if (!aIsKeepAliveMessage) + { + if (mInactivity.IsUsed()) + { + mInactivity.SetExpirationTime( + now + (IsServer() ? CalculateServerInactivityWaitTime() : mInactivity.GetInterval())); + } + else + { + // When Inactivity timeout is not used (i.e., interval is set + // to the special `kInfinite` value), we still need to track + // the time so that if/when later the inactivity interval + // gets changed, we can adjust the remaining time correctly + // from `AdjustInactivityTimeout()`. In this case, we just + // track the current time as "expiration time". + + mInactivity.SetExpirationTime(now); + } + } + + nextTime = GetNextFireTime(now); + + if (nextTime != now.GetDistantFuture()) + { + Get().mTimer.FireAtIfEarlier(nextTime); + } +} + +TimeMilli Dso::Connection::GetNextFireTime(TimeMilli aNow) const +{ + TimeMilli nextTime = aNow.GetDistantFuture(); + + switch (mState) + { + case kStateDisconnected: + break; + + case kStateConnecting: + // While in `kStateConnecting`, Keep Alive timer is + // used for `kConnectingTimeout`. + VerifyOrExit(mKeepAlive.GetExpirationTime() > aNow, nextTime = aNow); + nextTime = mKeepAlive.GetExpirationTime(); + break; + + case kStateConnectedButSessionless: + case kStateEstablishingSession: + case kStateSessionEstablished: + nextTime = OT_MIN(nextTime, mPendingRequests.GetNextFireTime(aNow)); + + if (mKeepAlive.IsUsed()) + { + VerifyOrExit(mKeepAlive.GetExpirationTime() > aNow, nextTime = aNow); + nextTime = OT_MIN(nextTime, mKeepAlive.GetExpirationTime()); + } + + if (mInactivity.IsUsed() && mPendingRequests.IsEmpty() && !mLongLivedOperation) + { + // An operation being active on a DSO Session includes + // a request message waiting for a response, or an + // active long-lived operation. + + VerifyOrExit(mInactivity.GetExpirationTime() > aNow, nextTime = aNow); + nextTime = OT_MIN(nextTime, mInactivity.GetExpirationTime()); + } + + break; + } + +exit: + return nextTime; +} + +void Dso::Connection::HandleTimer(TimeMilli aNow, TimeMilli &aNextTime) +{ + switch (mState) + { + case kStateDisconnected: + break; + + case kStateConnecting: + if (mKeepAlive.IsExpired(aNow)) + { + Disconnect(kGracefullyClose, kReasonFailedToConnect); + } + break; + + case kStateConnectedButSessionless: + case kStateEstablishingSession: + case kStateSessionEstablished: + if (mPendingRequests.HasAnyTimedOut(aNow)) + { + // If server sends no response to a request, client + // waits for 30 seconds (`kResponseTimeout`) after which + // client MUST forcibly abort the connection. + Disconnect(kForciblyAbort, kReasonResponseTimeout); + ExitNow(); + } + + // The inactivity timer is kept clear, while an operation is + // active on the session (which includes a request waiting for + // response or an active long-lived operation). + + if (mInactivity.IsUsed() && mPendingRequests.IsEmpty() && !mLongLivedOperation && mInactivity.IsExpired(aNow)) + { + // On client, if the inactivity timeout is reached, the + // connection is closed gracefully. On server, if too much + // time (`CalculateServerInactivityWaitTime()`, i.e., five + // seconds or twice the current inactivity timeout interval, + // whichever is grater) elapses server MUST consider the + // client delinquent and MUST forcibly abort the connection. + + Disconnect(IsClient() ? kGracefullyClose : kForciblyAbort, kReasonInactivityTimeout); + ExitNow(); + } + + if (mKeepAlive.IsUsed() && mKeepAlive.IsExpired(aNow)) + { + // On client, if the Keep Alive interval elapses without any + // DNS messages being sent or received, the client MUST take + // action and send a DSO Keep Alive message. + // + // On server, if twice the Keep Alive interval value elapses + // without any messages being sent or received, the server + // considers the client delinquent and aborts the connection. + + if (IsClient()) + { + IgnoreError(SendKeepAliveMessage()); + } + else + { + Disconnect(kForciblyAbort, kReasonKeepAliveTimeout); + ExitNow(); + } + } + break; + } + +exit: + aNextTime = OT_MIN(aNextTime, GetNextFireTime(aNow)); + SignalAnyStateChange(); +} + +const char *Dso::Connection::StateToString(State aState) +{ + static const char *const kStateStrings[] = { + "Disconnected", // (0) kStateDisconnected, + "Connecting", // (1) kStateConnecting, + "ConnectedButSessionless", // (2) kStateConnectedButSessionless, + "EstablishingSession", // (3) kStateEstablishingSession, + "SessionEstablished", // (4) kStateSessionEstablished, + }; + + static_assert(0 == kStateDisconnected, "kStateDisconnected value is incorrect"); + static_assert(1 == kStateConnecting, "kStateConnecting value is incorrect"); + static_assert(2 == kStateConnectedButSessionless, "kStateConnectedButSessionless value is incorrect"); + static_assert(3 == kStateEstablishingSession, "kStateEstablishingSession value is incorrect"); + static_assert(4 == kStateSessionEstablished, "kStateSessionEstablished value is incorrect"); + + return kStateStrings[aState]; +} + +const char *Dso::Connection::MessageTypeToString(MessageType aMessageType) +{ + static const char *const kMessageTypeStrings[] = { + "Request", // (0) kRequestMessage + "Response", // (1) kResponseMessage + "Unidirectional", // (2) kUnidirectionalMessage + }; + + static_assert(0 == kRequestMessage, "kRequestMessage value is incorrect"); + static_assert(1 == kResponseMessage, "kResponseMessage value is incorrect"); + static_assert(2 == kUnidirectionalMessage, "kUnidirectionalMessage value is incorrect"); + + return kMessageTypeStrings[aMessageType]; +} + +const char *Dso::Connection::DisconnectReasonToString(DisconnectReason aReason) +{ + static const char *const kDisconnectReasonStrings[] = { + "FailedToConnect", // (0) kReasonFailedToConnect + "ResponseTimeout", // (1) kReasonResponseTimeout + "PeerDoesNotSupportDso", // (2) kReasonPeerDoesNotSupportDso + "PeerClosed", // (3) kReasonPeerClosed + "PeerAborted", // (4) kReasonPeerAborted + "InactivityTimeout", // (5) kReasonInactivityTimeout + "KeepAliveTimeout", // (6) kReasonKeepAliveTimeout + "ServerRetryDelayRequest", // (7) kReasonServerRetryDelayRequest + "PeerMisbehavior", // (8) kReasonPeerMisbehavior + "Unknown", // (9) kReasonUnknown + }; + + static_assert(0 == kReasonFailedToConnect, "kReasonFailedToConnect value is incorrect"); + static_assert(1 == kReasonResponseTimeout, "kReasonResponseTimeout value is incorrect"); + static_assert(2 == kReasonPeerDoesNotSupportDso, "kReasonPeerDoesNotSupportDso value is incorrect"); + static_assert(3 == kReasonPeerClosed, "kReasonPeerClosed value is incorrect"); + static_assert(4 == kReasonPeerAborted, "kReasonPeerAborted value is incorrect"); + static_assert(5 == kReasonInactivityTimeout, "kReasonInactivityTimeout value is incorrect"); + static_assert(6 == kReasonKeepAliveTimeout, "kReasonKeepAliveTimeout value is incorrect"); + static_assert(7 == kReasonServerRetryDelayRequest, "kReasonServerRetryDelayRequest value is incorrect"); + static_assert(8 == kReasonPeerMisbehavior, "kReasonPeerMisbehavior value is incorrect"); + static_assert(9 == kReasonUnknown, "kReasonUnknown value is incorrect"); + + return kDisconnectReasonStrings[aReason]; +} + +//--------------------------------------------------------------------------------------------------------------------- +// Dso::Connection::PendingRequests + +bool Dso::Connection::PendingRequests::Contains(MessageId aMessageId, Tlv::Type &aPrimaryTlvType) const +{ + bool contains = true; + const Entry *entry = mRequests.FindMatching(aMessageId); + + VerifyOrExit(entry != nullptr, contains = false); + aPrimaryTlvType = entry->mPrimaryTlvType; + +exit: + return contains; +} + +Error Dso::Connection::PendingRequests::Add(MessageId aMessageId, Tlv::Type aPrimaryTlvType, TimeMilli aResponseTimeout) +{ + Error error = kErrorNone; + Entry *entry = mRequests.PushBack(); + + VerifyOrExit(entry != nullptr, error = kErrorNoBufs); + entry->mMessageId = aMessageId; + entry->mPrimaryTlvType = aPrimaryTlvType; + entry->mTimeout = aResponseTimeout; + +exit: + return error; +} + +void Dso::Connection::PendingRequests::Remove(MessageId aMessageId) +{ + Entry *entry = mRequests.FindMatching(aMessageId); + Entry *lastEntry; + + VerifyOrExit(entry != nullptr); + + // Remove last entry from the `mRequests` array, if it is not the + // `entry` we want to remove, replace `entry` with `lastEntry. + + lastEntry = mRequests.PopBack(); + VerifyOrExit(lastEntry != entry); + *entry = *lastEntry; + +exit: + return; +} + +bool Dso::Connection::PendingRequests::HasAnyTimedOut(TimeMilli aNow) const +{ + bool timedOut = false; + + for (const Entry &entry : mRequests) + { + if (entry.mTimeout <= aNow) + { + timedOut = true; + break; + } + } + + return timedOut; +} + +TimeMilli Dso::Connection::PendingRequests::GetNextFireTime(TimeMilli aNow) const +{ + TimeMilli nextTime = aNow.GetDistantFuture(); + + for (const Entry &entry : mRequests) + { + VerifyOrExit(entry.mTimeout > aNow, nextTime = aNow); + nextTime = OT_MIN(entry.mTimeout, nextTime); + } + +exit: + return nextTime; +} + +//--------------------------------------------------------------------------------------------------------------------- +// Dso + +Dso::Dso(Instance &aInstance) + : InstanceLocator(aInstance) + , mAcceptHandler(nullptr) + , mTimer(aInstance, HandleTimer) +{ +} + +void Dso::StartListening(AcceptHandler aAcceptHandler) +{ + mAcceptHandler = aAcceptHandler; + otPlatDsoEnableListening(&GetInstance(), true); +} + +void Dso::StopListening(void) +{ + otPlatDsoEnableListening(&GetInstance(), false); +} + +Dso::Connection *Dso::FindClientConnection(const Ip6::SockAddr &aPeerSockAddr) +{ + return mClientConnections.FindMatching(aPeerSockAddr); +} + +Dso::Connection *Dso::FindServerConnection(const Ip6::SockAddr &aPeerSockAddr) +{ + return mServerConnections.FindMatching(aPeerSockAddr); +} + +Dso::Connection *Dso::AcceptConnection(const Ip6::SockAddr &aPeerSockAddr) +{ + Connection *connection = nullptr; + + VerifyOrExit(mAcceptHandler != nullptr); + connection = mAcceptHandler(GetInstance(), aPeerSockAddr); + + VerifyOrExit(connection != nullptr); + connection->Accept(); + +exit: + return connection; +} + +void Dso::HandleTimer(Timer &aTimer) +{ + aTimer.Get().HandleTimer(); +} + +void Dso::HandleTimer(void) +{ + TimeMilli now = TimerMilli::GetNow(); + TimeMilli nextTime = now.GetDistantFuture(); + Connection *conn; + Connection *next; + + for (conn = mClientConnections.GetHead(); conn != nullptr; conn = next) + { + next = conn->GetNext(); + conn->HandleTimer(now, nextTime); + } + + for (conn = mServerConnections.GetHead(); conn != nullptr; conn = next) + { + next = conn->GetNext(); + conn->HandleTimer(now, nextTime); + } + + if (nextTime != now.GetDistantFuture()) + { + mTimer.FireAtIfEarlier(nextTime); + } +} + +} // namespace Dns +} // namespace ot + +#endif // OPENTHREAD_CONFIG_DNS_DSO_ENABLE diff --git a/src/core/net/dns_dso.hpp b/src/core/net/dns_dso.hpp new file mode 100644 index 000000000..a8346c7e7 --- /dev/null +++ b/src/core/net/dns_dso.hpp @@ -0,0 +1,973 @@ +/* + * Copyright (c) 2021, 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 DNS_DSO_HPP_ +#define DNS_DSO_HPP_ + +#include "openthread-core-config.h" + +#if OPENTHREAD_CONFIG_DNS_DSO_ENABLE + +#include + +#include "common/array.hpp" +#include "common/as_core_type.hpp" +#include "common/const_cast.hpp" +#include "common/encoding.hpp" +#include "common/linked_list.hpp" +#include "common/locator.hpp" +#include "common/message.hpp" +#include "common/non_copyable.hpp" +#include "common/timer.hpp" +#include "net/dns_types.hpp" +#include "net/socket.hpp" + +/** + * @file + * This file includes definitions for the DNS Stateful Operations (DSO) per RFC-8490. + */ + +struct otPlatDsoConnection +{ +}; + +namespace ot { +namespace Dns { + +using ot::Encoding::BigEndian::HostSwap16; +using ot::Encoding::BigEndian::HostSwap32; + +extern "C" otPlatDsoConnection *otPlatDsoAccept(otInstance *aInstance, const otSockAddr *aPeerSockAddr); + +extern "C" void otPlatDsoHandleConnected(otPlatDsoConnection *aConnection); +extern "C" void otPlatDsoHandleReceive(otPlatDsoConnection *aConnection, otMessage *aMessage); +extern "C" void otPlatDsoHandleDisconnected(otPlatDsoConnection *aConnection, otPlatDsoDisconnectMode aMode); + +/** + * This class implements DNS Stateful Operations (DSO). + * + */ +class Dso : public InstanceLocator, private NonCopyable +{ + friend otPlatDsoConnection *otPlatDsoAccept(otInstance *aInstance, const otSockAddr *aPeerSockAddr); + +public: + /** + * Infinite Keep Alive or Inactivity timeout value. + * + * This value can be used for either Keep Alive or Inactivity timeout interval. It practically disables the + * timeout. + * + */ + static constexpr uint32_t kInfiniteTimeout = 0xffffffff; + + /** + * Default Keep Alive or Inactivity timeout value (in msec). + * + * On a new DSO session, if no explicit DSO Keep Alive message exchange has taken place, the default value for both + * timeouts is 15 seconds [RFC 8490 - 6.2]. + * + */ + static constexpr uint32_t kDefaultTimeout = TimeMilli::SecToMsec(15); + + /** + * The minimum allowed Keep Alive interval (in msec). + * + * Any value less than ten seconds is invalid [RFC 8490 - 6.5.2]. + * + */ + static constexpr uint32_t kMinKeepAliveInterval = TimeMilli::SecToMsec(10); + + /** + * The maximum wait time for a DSO response to a DSO request (in msec). + * + */ + static constexpr uint32_t kResponseTimeout = OPENTHREAD_CONFIG_DNS_DSO_RESPONSE_TIMEOUT; + + /** + * The maximum wait time for a connection to be established (in msec). + * + */ + static constexpr uint32_t kConnectingTimeout = OPENTHREAD_CONFIG_DNS_DSO_CONNECTING_TIMEOUT; + + /** + * The minimum Inactivity wait time on a server before closing a connection. + * + * A server will abort an idle session after five seconds or twice the inactivity timeout value, whichever is + * greater [RFC 8490 - 6.4.1]. + * + */ + static constexpr uint32_t kMinServerInactivityWaitTime = TimeMilli::SecToMsec(5); + + /** + * This class represents a DSO TLV. + * + */ + OT_TOOL_PACKED_BEGIN + class Tlv + { + public: + typedef uint16_t Type; ///< DSO TLV type. + + static constexpr Type kReservedType = 0; ///< Reserved TLV type. + static constexpr Type kKeepAliveType = 1; ///< Keep Alive TLV type. + static constexpr Type kRetryDelayType = 2; ///< Retry Delay TLV type. + static constexpr Type kEncryptionPaddingType = 3; ///< Encryption Padding TLV type. + + /** + * This method initializes the `Tlv` instance with a given type and length. + * + * @param[in] aType The TLV type. + * @param[in] aLength The TLV length. + * + */ + void Init(Type aType, uint16_t aLength) + { + mType = HostSwap16(aType); + mLength = HostSwap16(aLength); + } + + /** + * This method gets the TLV type. + * + * @returns The TLV type. + * + */ + Type GetType(void) const { return HostSwap16(mType); } + + /** + * This method gets the TLV length. + * + * @returns The TLV length (in bytes). + * + */ + uint16_t GetLength(void) const { return HostSwap16(mLength); } + + /** + * This method returns the total size of the TLV (including the type and length fields). + * + * @returns The total size (number of bytes) of the TLV. + * + */ + uint32_t GetSize(void) const { return sizeof(Tlv) + static_cast(GetLength()); } + + private: + Type mType; + uint16_t mLength; + } OT_TOOL_PACKED_END; + + /** + * This class represents a DSO connection to a peer. + * + */ + class Connection : public otPlatDsoConnection, + public InstanceLocator, + public LinkedListEntry, + private NonCopyable + { + friend class Dso; + friend class LinkedList; + friend class LinkedListEntry; + friend void otPlatDsoHandleConnected(otPlatDsoConnection *aConnection); + friend void otPlatDsoHandleReceive(otPlatDsoConnection *aConnection, otMessage *aMessage); + friend void otPlatDsoHandleDisconnected(otPlatDsoConnection *aConnection, otPlatDsoDisconnectMode aMode); + + public: + typedef uint16_t MessageId; ///< This type represents a DSO Message Identifier. + + /** + * This enumeration defines the `Connection` states. + * + */ + enum State : uint8_t + { + kStateDisconnected, ///< Disconnected. + kStateConnecting, ///< Connecting to peer. + kStateConnectedButSessionless, ///< Connected but DSO session is not yet established. + kStateEstablishingSession, ///< Establishing DSO session. + kStateSessionEstablished, ///< DSO session is established. + }; + + /** + * This enumeration defines the disconnect modes. + * + */ + enum DisconnectMode : uint8_t + { + kGracefullyClose = OT_PLAT_DSO_DISCONNECT_MODE_GRACEFULLY_CLOSE, ///< Close the connection gracefully. + kForciblyAbort = OT_PLAT_DSO_DISCONNECT_MODE_FORCIBLY_ABORT, ///< Forcibly abort the connection. + }; + + /** + * This enumeration defines the disconnect reason. + * + */ + enum DisconnectReason : uint8_t + { + kReasonFailedToConnect, ///< Failed to connect (e.g., peer did not accept or timed out). + kReasonResponseTimeout, ///< Response timeout (no response from peer after `kResponseTimeout`). + kReasonPeerDoesNotSupportDso, ///< Peer does not support DSO. + kReasonPeerClosed, ///< Peer closed the connection gracefully. + kReasonPeerAborted, ///< Peer forcibly aborted the connection. + kReasonInactivityTimeout, ///< Connection closed or aborted due to Inactivity timeout. + kReasonKeepAliveTimeout, ///< Connection closed due to Keep Alive timeout. + kReasonServerRetryDelayRequest, ///< Connection closed due to server requesting retry delay. + kReasonPeerMisbehavior, ///< Aborted due to peer misbehavior (fatal error). + kReasonUnknown, ///< Unknown reason. + }; + + /** + * This class defines the callback functions used by a `Connection`. + * + */ + class Callbacks + { + friend class Connection; + + public: + /** + * This callback signals that the connection is established (entering `kStateConnectedButSessionless`). + * + * On a client, this callback can be used to send the first DSO request to start establishing the session. + * + * @param[in] aConnection A reference to the connection. + * + */ + typedef void (&HandleConnected)(Connection &aConnection); + + /** + * This callback signals that the DSO session is established (entering `kStateSessionEstablished`). + * + * @param[in] aConnection A reference to the connection. + * + */ + typedef void (&HandleSessionEstablished)(Connection &aConnection); + + /** + * This callback signals that the DSO session is disconnected by DSO module or the peer. + * + * After this callback is invoked the DSO module will no longer track the related `Connection` instance. It + * can be reclaimed by the caller, e.g., freed if it was heap allocated. + * + * The `Connection::GetDisconnectReason()` can be used the get the disconnect reason. + * + * @param[in] aConnection A reference to the connection. + * + */ + typedef void (&HandleDisconnected)(Connection &aConnection); + + /** + * This callback requests processing of a received DSO request message. + * + * If the processing is successful a response can be sent using `Connection::SendResponseMessage()` method. + * + * Note that @p aMessage is a `const` so the ownership of the message is not passed in this callback. + * The message will be freed by the `Connection` after returning from this callback, so if it needs to be + * persisted the callback implementation needs to create its own copy. + * + * The offset in @p aMessage is set to point to the start of the DSO TLVs. DSO module only reads and + * validates the first TLV (primary TLV) from the message. It is up to the callback implementation to parse + * and validate the rest of the TLVs in the message. + * + * @param[in] aConnection A reference to the connection. + * @param[in] aMessageId The message ID of the received request. + * @param[in] aMessage The received message. Message offset is set to the start of the TLVs. + * @param[in] aPrimaryTlvType The primary TLV type. + * + * @retval kErrorSuccess The request message was processed successfully. + * @retval kErrorNotFound The @p aPrimaryTlvType is not known (not supported). This error triggers a DNS + * response with error code 11 "DSO TLV TYPE not implemented" to be sent. + * @retval kErrorAbort Fatal error (misbehavior by peer). This triggers aborting of the connection. + * + */ + typedef Error (&ProcessRequestMessage)(Connection & aConnection, + MessageId aMessageId, + const Message &aMessage, + Tlv::Type aPrimaryTlvType); + + /** + * This callback requests processing of a received DSO unidirectional message. + * + * Similar to `ProcessRequestMessage()` the ownership of @p aMessage is not passed in this callback. + * + * The offset in @p aMessage is set to point to the start of the DSO TLVs. DSO module only reads and + * validates the first TLV (primary TLV) from the message. It is up to the callback implementation to parse + * and validate the rest of the TLVs in the message. + * + * @param[in] aConnection A reference to the connection. + * @param[in] aMessage The received message. Message offset is set to the start of the TLVs. + * @param[in] aPrimaryTlvType The primary TLV type. + * + * @retval kErrorSuccess The unidirectional message was processed successfully. + * @retval kErrorAbort Fatal error (misbehavior by peer). This triggers aborting of the connection. If + * @p aPrimaryTlvType is not known in a unidirectional message, it is a fatal error. + * + */ + typedef Error (&ProcessUnidirectionalMessage)(Connection & aConnection, + const Message &aMessage, + Tlv::Type aPrimaryTlvType); + + /** + * This callback requests processing of a received DSO response message. + * + * Before invoking this callback, the `Connection` implementation already verifies that: + * + * (1) this response is for a pending previously sent request (based on the message ID), + * (2) if no error response code in DNS @p aHeader and the response contains a response primary TLV, the + * the response primary TLV matches the request primary TLV. + * + * Similar to `ProcessRequestMessage()` the ownership of @p aMessage is not passed in this callback. + * + * The offset in @p aMessage is set to point to the start of the DSO TLVs. DSO module only reads and + * validates the first TLV (primary TLV) from the message. It is up to the callback implementation to parse + * and validate the rest of the TLVs in the message. + * + * @param[in] aConnection A reference to the connection. + * @param[in] aHeader The DNS header of the received response. + * @param[in] aMessage The received message. Message offset is set to the start of the TLVs. + * @param[in] aResponseTlvType The primary TLV type in the response message, or `Tlv::kReservedType` if + * the response contains no TLV. + * @param[in] aRequestTlvType The primary TLV type of the corresponding request message. + * + * @retval kErrorSuccess The message was processed successfully. + * @retval kErrorAbort Fatal error (misbehavior by peer). This triggers aborting of the connection. + * + */ + typedef Error (&ProcessResponseMessage)(Connection & aConnection, + const Dns::Header &aHeader, + const Message & aMessage, + Tlv::Type aResponseTlvType, + Tlv::Type aRequestTlvType); + /** + * This constructor initializes a `Callbacks` object setting all the callback functions. + * + * @param[in] aHandleConnected The `HandleConnected` callback. + * @param[in] aHandleSessionEstablished The `HandleSessionEstablished` callback. + * @param[in] aHandleDisconnected The `HandleDisconnected` callback. + * @param[in] aProcessRequestMessage The `ProcessRequestMessage` callback. + * @param[in] aProcessUnidirectionalMessage The `ProcessUnidirectionalMessage` callback. + * @param[in] aProcessResponseMessage The `ProcessResponseMessage` callback. + * + */ + Callbacks(HandleConnected aHandleConnected, + HandleSessionEstablished aHandleSessionEstablished, + HandleDisconnected aHandleDisconnected, + ProcessRequestMessage aProcessRequestMessage, + ProcessUnidirectionalMessage aProcessUnidirectionalMessage, + ProcessResponseMessage aProcessResponseMessage) + : mHandleConnected(aHandleConnected) + , mHandleSessionEstablished(aHandleSessionEstablished) + , mHandleDisconnected(aHandleDisconnected) + , mProcessRequestMessage(aProcessRequestMessage) + , mProcessUnidirectionalMessage(aProcessUnidirectionalMessage) + , mProcessResponseMessage(aProcessResponseMessage) + { + } + + private: + HandleConnected mHandleConnected; + HandleSessionEstablished mHandleSessionEstablished; + HandleDisconnected mHandleDisconnected; + ProcessRequestMessage mProcessRequestMessage; + ProcessUnidirectionalMessage mProcessUnidirectionalMessage; + ProcessResponseMessage mProcessResponseMessage; + }; + + /** + * This constructor initializes a `Connection` instance. + * + * The `kDefaultTimeout` will be used for @p aInactivityTimeout and @p aKeepAliveInterval. The + * @p aKeepAliveInterval MUST NOT be less than `kMinKeepAliveInterval`. + * + * @param[in] aInstance The OpenThread instance. + * @param[in] aPeerSockAddr The peer socket address. + * @param[in] aCallbacks A reference to the `Callbacks` instance used by the `Connection`. + * @param[in] aInactivityTimeout The Inactivity timeout interval (in msec). + * @param[in] aKeepAliveInterval The Keep Alive timeout interval (in msec). + * + */ + Connection(Instance & aInstance, + const Ip6::SockAddr &aPeerSockAddr, + Callbacks & aCallbacks, + uint32_t aInactivityTimeout = kDefaultTimeout, + uint32_t aKeepAliveInterval = kDefaultTimeout); + + /** + * This method gets the current state of the `Connection`. + * + * @returns The `Connection` state. + * + */ + State GetState(void) const { return mState; } + + /** + * This method returns the `Connection` peer socket address. + * + * @returns The peer socket address. + * + */ + const Ip6::SockAddr &GetPeerSockAddr(void) const { return mPeerSockAddr; } + + /** + * This method indicates whether or not the device is acting as a DSO server on this `Connection`. + * + * Server is the software entity with a listening socket, awaiting incoming connection requests. + * + * @retval TRUE Device is acting as a server on this connection. + * @retval FALSE Device is acting as a client on this connection. + * + */ + bool IsServer(void) const { return mIsServer; } + + /** + * This method indicates whether or not the device is acting as a DSO client on this `Connection`. + * + * Client is the software entity that initiates a connection to the server's listening socket. + * + * @retval TRUE Device is acting as a client on this connection. + * @retval FALSE Device is acting as a server on this connection. + * + */ + bool IsClient(void) const { return !mIsServer; } + + /** + * This method allocates a new DSO message. + * + * @returns A pointer to the allocated message or `nullptr` if out of message buffers. + * + */ + Message *NewMessage(void); + + /** + * This method requests the device to initiate a connection (connect as a client) to the peer (acting as a + * server). + * + * This method MUST be called when `Connection` is `kStateDisconnected` state. + * + * After calling `Connect()`, either + * - `Callbacks::HandleConnected()` is invoked when connection is successfully established, or + * - `Callbacks::HandleDisconnected()` is invoked if the connection cannot be established (e.g., peer does not + * accept it or we time out waiting for it). The disconnect reason is set to `kReasonFailedToConnect`. + * + * Calling `Connect()` passes the control and ownership of the `Connection` instance to the DSO module (which + * adds the `Connection` into a list of client connections - see `Dso::FindClientConnection()`). The ownership + * is passed back to the caller when the `Connection` gets disconnected, i.e., when either + * - the user requests a disconnect by an explicit call to `Disconnect()` method, or, + * - when `HandleDisconnected()` callback is invoked (after its is closed by DSO module itself or by peer). + * + */ + void Connect(void); + + /** + * This method requests the connection to be disconnected. + * + * Note that calling `Disconnect()` does not trigger the `Callbacks::HandleDisconnected()` to be invoked (as + * this callback is used when DSO module itself or the peer disconnects the connections). + * + * After the call to `Disconnect()` the caller can take back ownership of the `Connection` (e.g., can free the + * `Connection` instance if it was heap allocated). + * + * @param[in] aMode Determines whether to close the connection gracefully or forcibly abort the connection. + * @param[in] aReason The disconnect reason. + * + */ + void Disconnect(DisconnectMode aMode, DisconnectReason aReason); + + /** + * This method returns the last disconnect reason. + * + * @returns The last disconnect reason. + * + */ + DisconnectReason GetDisconnectReason(void) const { return mDisconnectReason; } + + /** + * This method implicitly marks the DSO session as established (set state to `kStateSessionEstablished`). + * + * This method MUST be called when `Connection is in `kStateConnectedButSessionless` state. + * + * The DSO module itself will mark the session as established after the first successful DSO message exchange + * (sending a request message from client and receiving a response from server). + * + * This method is intended for implicit DSO session establishment where it may be known in advance by some + * external means that both client and server support DSO and then the session may be established as soon as + * the connection is established. + * + */ + void MarkSessionEstablished(void); + + /** + * This method sends a DSO request message. + * + * This method MUST be called when `Connection` is in certain states depending on whether it is acting as a + * client or server: + * - On client, a request message can be sent after connection is established (`kStateConnectedButSessionless`). + * The first request is used to establish the DSO session. While in `kStateEstablishingSession` or + * `kStateSessionEstablished` other DSO request messages can be sent to the server. + * - On server, a request can be sent only after DSO session is established (`kStateSessionEstablished`). + * + * The prepared message needs to contain the DSO TLVs. The DNS header will be added by the DSO module itself. + * Also there is no need to append the "Encryption Padding TLV" to the message as it will be added by the DSO + * module before sending the message to the transport layer. + * + * On success (when this method returns `kErrorNone`) it takes the ownership of the @p aMessage. On failure the + * caller still owns the message and may need to free it. + * + * @param[in] aMessage The DSO request message to send. + * @param[out] aMessageId A reference to output the message ID used for the transmission (may be used by + * the caller to track the response from `Callbacks::ProcessResponseMessage()`). + * @param[in] aResponseTimeout The response timeout in msec (default value is `kResponseTimeout`) + * + * @retval kErrorNone Successfully sent the DSO request message and updated @p aMessageId. + * @retval kErrorNoBufs Failed to allocate new buffer to prepare the message (append header or padding). + * + */ + Error SendRequestMessage(Message & aMessage, + MessageId &aMessageId, + uint32_t aResponseTimeout = kResponseTimeout); + + /** + * This method sends a DSO unidirectional message. + * + * This method MUST be called when session is established (in `kStateSessionEstablished` state). + * + * Similar to `SendRequestMessage()` method, only TLV(s) need to be included in the message. The DNS header and + * Encryption Padding TLV will be added by the DSO module. + * + * On success (when this method returns `kErrorNone`) it takes the ownership of the @p aMessage. On failure the + * caller still owns the message and may need to free it. + * + * @param[in] aMessage The DSO unidirectional message to send. + * + * @retval kErrorNone Successfully sent the DSO message. + * @retval kErrorNoBufs Failed to allocate new buffer to prepare the message (append header or padding). + * + */ + Error SendUnidirectionalMessage(Message &aMessage); + + /** + * This method sends a DSO response message for a received request message. + * + * Similar to `SendRequestMessage()` method, only TLV(s) need to be included in the message. The DNS header and + * Encryption Padding TLV will be added by DSO module. + * + * On success (when this method returns `kErrorNone`) it takes the ownership of the @p aMessage. On failure the + * caller still owns the message and may need to free it. + * + * @param[in] aMessage The DSO response message to send. + * @param[in] aResponseId The message ID to use for the response. + * + * @retval kErrorNone Successfully sent the DSO response message. + * @retval kErrorNoBufs Failed to allocate new buffer to prepare the message (append header or padding). + * + */ + Error SendResponseMessage(Message &aMessage, MessageId aResponseId); + + /** + * This method returns the Keep Alive timeout interval (in msec). + * + * On client, this indicates the value granted by server, on server the value to grant. + * + * @returns The keep alive timeout interval (in msec). + * + */ + uint32_t GetKeepAliveInterval(void) const { return mKeepAlive.GetInterval(); } + + /** + * This method returns the Inactivity timeout interval (in msec). + * + * On client, this indicates the value granted by server, on server the value to grant. + * + * @returns The inactivity timeout interval (in msec). + * + */ + uint32_t GetInactivityTimeout(void) const { return mInactivity.GetInterval(); } + + /** + * This method sends a Keep Alive message. + * + * This method MUST be called when `Connection` is in certain states depending on whether it is acting as a + * client or server: + * - On client, it can be called in any state after the connection is established. Sending Keep Alive message + * can be used to initiate establishing DSO session. + * - On server, it can be used only after session is established (`kStateSessionEstablished`). + * + * On a client, the Keep Alive message is sent as a request message. On server it is sent as a unidirectional + * message. + * + * @retval kErrorNone Successfully prepared and sent a Keep Alive message. + * @retval kErrorNoBufs Failed to allocate message to send. + * + */ + Error SendKeepAliveMessage(void); + + /** + * This method sets the Inactivity and Keep Alive timeout intervals. + * + * On client, the specified timeout intervals are used in Keep Alive request message, i.e., they are the values + * that client would wish to get. On server, the given timeout intervals specify the values that server would + * grant to a client upon receiving a Keep Alive request from it. + * + * This method can be called in any `Connection` state. If current state allows, calling this method will also + * trigger sending of a Keep Alive message (as if `SendKeepAliveMessage()` is also called). For states which + * trigger the tx, see `SendKeepAliveMessage()`. + * + * The special value `kInfiniteTimeout` can be used for either Inactivity or Keep Alive interval which disables + * the corresponding timer. The Keep Alive interval should be larger than or equal to minimum + * `kMinKeepAliveInterval`, otherwise `kErrorInvalidArgs` is returned. + * + * @param[in] aInactivityTimeout The Inactivity timeout (in msec). + * @param[in] aKeepAliveTimeout The Keep Alive timeout (in msec). + * + * @retval kErrorNone Successfully set the timeouts and sent a Keep Alive message. + * @retval kErrorInvalidArgs The given timeouts are not valid. + * @retval kErrorNoBufs Failed to allocate message to send. + * + */ + Error SetTimeouts(uint32_t aInactivityTimeout, uint32_t aKeepAliveInterval); + + /** + * This method enables/disables long-lived operation on the session. + * + * When a long-lived operation is active, the Inactivity timeout is always cleared, i.e., the DSO session stays + * connected even if no messages are exchanged. + * + * @param[in] aLongLivedOperation A boolean indicating whether or not a long-lived operation is active. + * + */ + void SetLongLivedOperation(bool aLongLivedOperation); + + /** + * This method sends a unidirectional Retry Delay message from server to client. + * + * This method MUST be used on a server only and when DSO session is already established, i.e., in state + * `kStateSessionEstablished`. It sends a unidirectional Retry Delay message to client requesting it to close + * the connection and not connect again for at least the specified delay amount. + * + * Note that calling `SendRetryDelayMessage()` does not by itself close the connection on server side. It is + * up to the user of the DSO module to implement a wait time delay before deciding to close/abort the connection + * from server side, in case the client does not close it upon receiving the Retry Delay message. + * + * @param[in] aDelay The retry delay interval (in msec). + * @param[in] aResponseCode The DNS RCODE to include in the Retry Delay message. + * + * @retval kErrorNone Successfully prepared and sent a Retry Delay message to client. + * @retval kErrorNoBufs Failed to allocate message to send. + * + */ + Error SendRetryDelayMessage(uint32_t aDelay, + Dns::Header::Response aResponseCode = Dns::Header::kResponseSuccess); + + /** + * This method returns the requested retry delay interval (in msec) by server. + * + * This method MUST be used after a `HandleDisconnected()` callback with `kReasonServerRetryDelayRequest` + * + * @returns The retry delay interval requested by server. + * + */ + uint32_t GetRetryDelay(void) const { return mRetryDelay; } + + /** + * This method returns the DNS error code in the last retry delay message received on client from server. + * + * This method MUST be used after a `HandleDisconnected()` callback with `kReasonServerRetryDelayRequest` + * + * @returns The DNS error code in the last Retry Delay message received on client from server. + * + */ + Dns::Header::Response GetRetryDelayErrorCode(void) const { return mRetryDelayErrorCode; } + + private: + enum MessageType : uint8_t + { + kRequestMessage, + kResponseMessage, + kUnidirectionalMessage, + }; + + // Info about pending request messages (message ID, primary TLV type, and response timeout). + class PendingRequests + { + public: + static constexpr uint8_t kMaxPendingRequests = OPENTHREAD_CONFIG_DNS_DSO_MAX_PENDING_REQUESTS; + + void Clear(void) { mRequests.Clear(); } + bool IsEmpty(void) const { return mRequests.IsEmpty(); } + bool Contains(MessageId aMessageId, Tlv::Type &aPrimaryTlvType) const; + Error Add(MessageId aMessageId, Tlv::Type aPrimaryTlvType, TimeMilli aResponseTimeout); + void Remove(MessageId aMessageId); + bool HasAnyTimedOut(TimeMilli aNow) const; + TimeMilli GetNextFireTime(TimeMilli aNow) const; + + private: + struct Entry + { + bool Matches(MessageId aMessageId) const { return mMessageId == aMessageId; } + + MessageId mMessageId; + Tlv::Type mPrimaryTlvType; + TimeMilli mTimeout; // Latest time by which a response is expected. + }; + + Array mRequests; + }; + + // Inactivity or KeepAlive timeout + class Timeout + { + public: + static constexpr uint32_t kInfinite = kInfiniteTimeout; + static constexpr uint32_t kDefault = kDefaultTimeout; + + explicit Timeout(uint32_t aInterval) + : mInterval(aInterval) + , mRequest(aInterval) + { + } + + // On client, timeout value granted by server. On server, value to grant. + uint32_t GetInterval(void) const { return mInterval; } + void SetInterval(uint32_t aInterval) { mInterval = LimitInterval(aInterval); } + + // On client, timeout value to request. Not used on server. + uint32_t GetRequestInterval(void) const { return mRequest; } + void SetRequestInterval(uint32_t aInterval) { mRequest = LimitInterval(aInterval); } + + TimeMilli GetExpirationTime(void) const { return mExpirationTime; } + void SetExpirationTime(TimeMilli aTime) { mExpirationTime = aTime; } + + bool IsUsed(void) const { return (mInterval != kInfinite); } + bool IsExpired(TimeMilli aNow) const { return (mExpirationTime <= aNow); } + + private: + static constexpr uint32_t kMaxInterval = TimerMilli::kMaxDelay / 2; + + uint32_t LimitInterval(uint32_t aInterval) const + { + // If it is not infinite, limit the interval to `kMaxInterval`. + // The max limit ensures that even twice the interval is less + // than max OpenThread timer duration. + return (aInterval == kInfinite) ? aInterval : OT_MIN(aInterval, kMaxInterval); + } + + uint32_t mInterval; + uint32_t mRequest; + TimeMilli mExpirationTime; + }; + + void Init(bool aIsServer); + void SetState(State aState); + void SignalAnyStateChange(void); + void Accept(void); + void MarkAsConnecting(void); + void HandleConnected(void); + void HandleDisconnected(DisconnectMode aMode); + void MarkAsDisconnected(void); + + Error SendKeepAliveMessage(MessageType aMessageType, MessageId aResponseId); + Error SendMessage(Message & aMessage, + MessageType aMessageType, + MessageId & aMessageId, + Dns::Header::Response aResponseCode = Dns::Header::kResponseSuccess, + uint32_t aResponseTimeout = kResponseTimeout); + void HandleReceive(Message &aMessage); + Error ReadPrimaryTlv(const Message &aMessage, Tlv::Type &aPrimaryTlvType) const; + Error ProcessRequestOrUnidirectionalMessage(const Dns::Header &aHeader, + const Message & aMessage, + Tlv::Type aPrimaryTlvType); + Error ProcessResponseMessage(const Dns::Header &aHeader, const Message &aMessage, Tlv::Type aPrimaryTlvType); + Error ProcessKeepAliveMessage(const Dns::Header &aHeader, const Message &aMessage); + Error ProcessRetryDelayMessage(const Dns::Header &aHeader, const Message &aMessage); + void SendErrorResponse(const Dns::Header &aHeader, Dns::Header::Response aResponseCode); + Error AppendPadding(Message &aMessage); + + void AdjustInactivityTimeout(uint32_t aNewTimeout); + uint32_t CalculateServerInactivityWaitTime(void) const; + void ResetTimeouts(bool aIsKeepAliveMessage); + TimeMilli GetNextFireTime(TimeMilli aNow) const; + void HandleTimer(TimeMilli aNow, TimeMilli &aNextTime); + + bool Matches(const Ip6::SockAddr &aPeerSockAddr) const { return mPeerSockAddr == aPeerSockAddr; } + + static const char *StateToString(State aState); + static const char *MessageTypeToString(MessageType aMessageType); + static const char *DisconnectReasonToString(DisconnectReason aReason); + + Connection * mNext; + Callbacks & mCallbacks; + Ip6::SockAddr mPeerSockAddr; + State mState; + MessageId mNextMessageId; + PendingRequests mPendingRequests; + bool mIsServer : 1; + bool mStateDidChange : 1; + bool mLongLivedOperation : 1; + Timeout mInactivity; + Timeout mKeepAlive; + uint32_t mRetryDelay; + Dns::Header::Response mRetryDelayErrorCode; + DisconnectReason mDisconnectReason; + }; + + /** + * This callback function is used by DSO module to determine whether or not to accept a connection request from a + * peer. + * + * The function MUST return a non-null `Connection` pointer if the request is to be accepted. The returned + * `Connection` instance MUST be in `kStateDisconnected`. The DSO module will take the ownership of the `Connection` + * instance (adds it into a list of server connections - see `FindServerConnection()`). The ownership is passed + * back to the caller when the `Connection` gets disconnected, i.e., when either the user requests a disconnect by + * an explicit call to the method `Connection::Disconnect()`, or, if `HandleDisconnected()` callback is invoked + * (after connection is closed by the DSO module itself or by the peer). + * + * @param[in] aInstance The OpenThread instance. + * @param[in] aPeerSockAddr The peer socket address. + * + * @returns A pointer to the `Connection` to use if to accept, or `nullptr` if to reject the connection request. + * + */ + typedef Connection *(*AcceptHandler)(Instance &aInstance, const Ip6::SockAddr &aPeerSockAddr); + + /** + * This constructor initializes the `Dso` module. + * + */ + explicit Dso(Instance &aInstance); + + /** + * This method starts listening for DSO connection requests from peers. + * + * Once a connection request (from a peer) is received, the `Dso` module will invoke the `AcceptHandler` to + * determine whether to accept or reject the request. + * + * @param[in] aAcceptHandler Accept handler callback. + * + */ + void StartListening(AcceptHandler aAcceptHandler); + + /** + * This method stops listening for DSO connection requests from peers. + * + */ + void StopListening(void); + + /** + * This method finds a client `Connection` instance (being currently managed by the `Dso` module) matching a given + * peer socket address. + * + * @param[in] aPeerSockAddr The peer socket address. + * + * @returns A pointer to the matching `Connection` or `nullptr` if no match is found. + * + */ + Connection *FindClientConnection(const Ip6::SockAddr &aPeerSockAddr); + + /** + * This method finds a server `Connection` instance (being currently managed by the `Dso` module) matching a given + * peer socket address. + * + * @param[in] aPeerSockAddr The peer socket address. + * + * @returns A pointer to the matching `Connection` or `nullptr` if no match is found. + * + */ + Connection *FindServerConnection(const Ip6::SockAddr &aPeerSockAddr); + +private: + OT_TOOL_PACKED_BEGIN + class KeepAliveTlv : public Tlv + { + public: + static constexpr Type kType = kKeepAliveType; + + void Init(void) { Tlv::Init(kType, sizeof(*this) - sizeof(Tlv)); } + + bool IsValid(void) const { return GetSize() >= sizeof(*this); } + + uint32_t GetInactivityTimeout(void) const { return HostSwap32(mInactivityTimeout); } + void SetInactivityTimeout(uint32_t aTimeout) { mInactivityTimeout = HostSwap32(aTimeout); } + + uint32_t GetKeepAliveInterval(void) const { return HostSwap32(mKeepAliveInterval); } + void SetKeepAliveInterval(uint32_t aInterval) { mKeepAliveInterval = HostSwap32(aInterval); } + + private: + uint32_t mInactivityTimeout; // In msec + uint32_t mKeepAliveInterval; // In msec + } OT_TOOL_PACKED_END; + + OT_TOOL_PACKED_BEGIN + class RetryDelayTlv : public Tlv + { + public: + static constexpr Type kType = kRetryDelayType; + + void Init(void) { Tlv::Init(kType, sizeof(*this) - sizeof(Tlv)); } + + bool IsValid(void) const { return GetSize() >= sizeof(*this); } + + uint32_t GetRetryDelay(void) const { return HostSwap32(mRetryDelay); } + void SetRetryDelay(uint32_t aDelay) { mRetryDelay = HostSwap32(aDelay); } + + private: + uint32_t mRetryDelay; + } OT_TOOL_PACKED_END; + + OT_TOOL_PACKED_BEGIN + class EncryptionPaddingTlv : public Tlv + { + public: + static constexpr Type kType = kEncryptionPaddingType; + + void Init(uint16_t aPaddingLength) { Tlv::Init(kType, aPaddingLength); } + + private: + // Value is padding bytes (zero) based on the length. + } OT_TOOL_PACKED_END; + + Connection *AcceptConnection(const Ip6::SockAddr &aPeerSockAddr); + + static void HandleTimer(Timer &aTimer); + void HandleTimer(void); + + AcceptHandler mAcceptHandler; + LinkedList mClientConnections; + LinkedList mServerConnections; + TimerMilli mTimer; +}; + +} // namespace Dns + +DefineCoreType(otPlatDsoConnection, Dns::Dso::Connection); +DefineMapEnum(otPlatDsoDisconnectMode, Dns::Dso::Connection::DisconnectMode); + +} // namespace ot + +#endif // OPENTHREAD_CONFIG_DNS_DSO_ENABLE + +#endif // DNS_DSO_HPP_ diff --git a/src/core/net/dns_types.cpp b/src/core/net/dns_types.cpp index aa2ae54c2..3862061ec 100644 --- a/src/core/net/dns_types.cpp +++ b/src/core/net/dns_types.cpp @@ -77,6 +77,7 @@ Error Header::ResponseCodeToError(Response aResponse) break; case kResponseNotImplemented: // Server does not support the query type (OpCode). + case kDsoTypeNotImplemented: // DSO TLV type is not implemented. error = kErrorNotImplemented; break; diff --git a/src/core/net/dns_types.hpp b/src/core/net/dns_types.hpp index eac939ae6..33b3f9d5e 100644 --- a/src/core/net/dns_types.hpp +++ b/src/core/net/dns_types.hpp @@ -151,7 +151,8 @@ public: kQueryTypeInverse = 1, kQueryTypeStatus = 2, kQueryTypeNotify = 4, - kQueryTypeUpdate = 5 + kQueryTypeUpdate = 5, + kQueryTypeDso = 6, }; /** @@ -273,6 +274,7 @@ public: kResponseRecordNotExists = 8, ///< Some RRset that ought to exist, does not exist. kResponseNotAuth = 9, ///< Service is not authoritative for zone. kResponseNotZone = 10, ///< A name is not in the zone. + kDsoTypeNotImplemented = 11, ///< DSO TLV TYPE is not implemented. kResponseBadName = 20, ///< Bad name. kResponseBadAlg = 21, ///< Bad algorithm. kResponseBadTruncation = 22, ///< Bad truncation. @@ -312,6 +314,7 @@ public: * - kResponseRecordNotExists (8) : Some RRset that ought to exist, does not exist -> kErrorNotFound * - kResponseNotAuth (9) : Service is not authoritative for zone -> kErrorSecurity * - kResponseNotZone (10) : A name is not in the zone -> kErrorParse + * - kDsoTypeNotImplemented (11) : DSO TLV Type is not implemented -> kErrorNotImplemented * - kResponseBadName (20) : Bad name -> kErrorParse * - kResponseBadAlg (21) : Bad algorithm -> kErrorSecurity * - kResponseBadTruncation (22) : Bad truncation -> kErrorParse diff --git a/src/core/openthread-core-config.h b/src/core/openthread-core-config.h index d5f10fc6c..09d581001 100644 --- a/src/core/openthread-core-config.h +++ b/src/core/openthread-core-config.h @@ -70,6 +70,7 @@ #include "config/dhcp6_server.h" #include "config/diag.h" #include "config/dns_client.h" +#include "config/dns_dso.h" #include "config/dnssd_server.h" #include "config/dtls.h" #include "config/history_tracker.h" diff --git a/src/core/thread/thread_netif.cpp b/src/core/thread/thread_netif.cpp index b698e34c2..f36469a05 100644 --- a/src/core/thread/thread_netif.cpp +++ b/src/core/thread/thread_netif.cpp @@ -74,6 +74,9 @@ ThreadNetif::ThreadNetif(Instance &aInstance) #if OPENTHREAD_CONFIG_DNSSD_SERVER_ENABLE , mDnssdServer(aInstance) #endif +#if OPENTHREAD_CONFIG_DNS_DSO_ENABLE + , mDnsDso(aInstance) +#endif #if OPENTHREAD_CONFIG_SNTP_CLIENT_ENABLE , mSntpClient(aInstance) #endif diff --git a/src/core/thread/thread_netif.hpp b/src/core/thread/thread_netif.hpp index ffc55c4d5..77496b4fc 100644 --- a/src/core/thread/thread_netif.hpp +++ b/src/core/thread/thread_netif.hpp @@ -52,6 +52,7 @@ #include "net/dhcp6_client.hpp" #include "net/dhcp6_server.hpp" #include "net/dns_client.hpp" +#include "net/dns_dso.hpp" #include "net/dnssd_server.hpp" #include "net/ip6_filter.hpp" #include "net/nd_agent.hpp" @@ -190,6 +191,9 @@ private: #if OPENTHREAD_CONFIG_DNSSD_SERVER_ENABLE Dns::ServiceDiscovery::Server mDnssdServer; #endif +#if OPENTHREAD_CONFIG_DNS_DSO_ENABLE + Dns::Dso mDnsDso; +#endif #if OPENTHREAD_CONFIG_SNTP_CLIENT_ENABLE Sntp::Client mSntpClient; #endif diff --git a/tests/toranj/openthread-core-toranj-config-simulation.h b/tests/toranj/openthread-core-toranj-config-simulation.h index ea97044b1..09d925d08 100644 --- a/tests/toranj/openthread-core-toranj-config-simulation.h +++ b/tests/toranj/openthread-core-toranj-config-simulation.h @@ -67,4 +67,12 @@ */ #define OPENTHREAD_CONFIG_LOG_OUTPUT OPENTHREAD_CONFIG_LOG_OUTPUT_APP +/** + * @def OPENTHREAD_CONFIG_DNS_DSO_ENABLE + * + * Define to 1 to enable DSO support. + * + */ +#define OPENTHREAD_CONFIG_DNS_DSO_ENABLE 1 + #endif /* OPENTHREAD_CORE_TORANJ_CONFIG_SIMULATION_H_ */ diff --git a/tests/unit/CMakeLists.txt b/tests/unit/CMakeLists.txt index 4e549aa00..1b2827fac 100644 --- a/tests/unit/CMakeLists.txt +++ b/tests/unit/CMakeLists.txt @@ -255,6 +255,28 @@ target_link_libraries(ot-test-dns add_test(NAME ot-test-dns COMMAND ot-test-dns) +add_executable(ot-test-dso + test_dso.cpp +) + +target_include_directories(ot-test-dso + PRIVATE + ${COMMON_INCLUDES} +) + +target_compile_options(ot-test-dso + PRIVATE + ${COMMON_COMPILE_OPTIONS} +) + +target_link_libraries(ot-test-dso + PRIVATE + ${COMMON_LIBS} +) + +add_test(NAME ot-test-dso COMMAND ot-test-dso) + + add_executable(ot-test-ecdsa test_ecdsa.cpp ) diff --git a/tests/unit/Makefile.am b/tests/unit/Makefile.am index c15f90cca..14da7fff3 100644 --- a/tests/unit/Makefile.am +++ b/tests/unit/Makefile.am @@ -117,6 +117,7 @@ check_PROGRAMS += \ ot-test-cmd-line-parser \ ot-test-data \ ot-test-dns \ + ot-test-dso \ ot-test-ecdsa \ ot-test-flash \ ot-test-heap \ @@ -209,6 +210,9 @@ ot_test_data_SOURCES = $(COMMON_SOURCES) test_data.cpp ot_test_dns_LDADD = $(COMMON_LDADD) ot_test_dns_SOURCES = $(COMMON_SOURCES) test_dns.cpp +ot_test_dso_LDADD = $(COMMON_LDADD) +ot_test_dso_SOURCES = $(COMMON_SOURCES) test_dso.cpp + ot_test_ecdsa_LDADD = $(COMMON_LDADD) ot_test_ecdsa_SOURCES = $(COMMON_SOURCES) test_ecdsa.cpp diff --git a/tests/unit/test_dso.cpp b/tests/unit/test_dso.cpp new file mode 100644 index 000000000..7b878e8c9 --- /dev/null +++ b/tests/unit/test_dso.cpp @@ -0,0 +1,1240 @@ +/* + * Copyright (c) 2021, 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 "test_platform.h" +#include "test_util.hpp" + +#include "common/arg_macros.hpp" +#include "common/array.hpp" +#include "common/as_core_type.hpp" +#include "common/instance.hpp" +#include "net/dns_dso.hpp" + +#if OPENTHREAD_CONFIG_DNS_DSO_ENABLE + +extern "C" { + +static uint32_t sNow = 0; +static uint32_t sAlarmTime; +static bool sAlarmOn = false; +static otInstance *sInstance; + +// Logs a message and adds current time (sNow) as "::." +#define Log(...) \ + printf("%02u:%02u:%02u.%03u " OT_FIRST_ARG(__VA_ARGS__) "\n", (sNow / 36000000), (sNow / 60000) % 60, \ + (sNow / 1000) % 60, sNow % 1000 OT_REST_ARGS(__VA_ARGS__)) + +void otPlatAlarmMilliStop(otInstance *) +{ + sAlarmOn = false; +} + +void otPlatAlarmMilliStartAt(otInstance *, uint32_t aT0, uint32_t aDt) +{ + sAlarmOn = true; + sAlarmTime = aT0 + aDt; + + Log(" otPlatAlarmMilliStartAt(time:%u.%03u, dt:%u.%03u)", sAlarmTime / 1000, sAlarmTime % 1000, + (sAlarmTime - sNow) / 1000, (sAlarmTime - sNow) % 1000); +} + +uint32_t otPlatAlarmMilliGetNow(void) +{ + return sNow; +} + +} // extern "C" + +void AdvanceTime(uint32_t aDuration) +{ + uint32_t time = sNow + aDuration; + + Log(" AdvanceTime for %u.%03u", aDuration / 1000, aDuration % 1000); + + while (sAlarmTime <= time) + { + sNow = sAlarmTime; + otPlatAlarmMilliFired(sInstance); + } + + sNow = time; +} + +namespace ot { +namespace Dns { + +OT_TOOL_PACKED_BEGIN +class TestTlv : public Dso::Tlv +{ +public: + static constexpr Type kType = 0xf800; + + void Init(uint8_t aValue) + { + Tlv::Init(kType, sizeof(*this) - sizeof(Tlv)); + mValue = aValue; + } + + bool IsValid(void) const { return GetSize() >= sizeof(*this); } + uint8_t GetValue(void) const { return mValue; } + +private: + uint8_t mValue; + +} OT_TOOL_PACKED_END; + +extern "C" void otPlatDsoSend(otPlatDsoConnection *aConnection, otMessage *aMessage); + +class Connection : public Dso::Connection +{ + friend void otPlatDsoSend(otPlatDsoConnection *aConnection, otMessage *aMessage); + +public: + explicit Connection(Instance & aInstance, + const char * aName, + const Ip6::SockAddr &aLocalSockAddr, + const Ip6::SockAddr &aPeerSockAddr) + : Dso::Connection(aInstance, aPeerSockAddr, sCallbacks) + , mName(aName) + , mLocalSockAddr(aLocalSockAddr) + { + ClearTestFlags(); + } + + const char * GetName(void) const { return mName; } + const Ip6::SockAddr &GetLocalSockAddr(void) const { return mLocalSockAddr; } + + void ClearTestFlags(void) + { + mDidGetConnectedSignal = false; + mDidGetSessionEstablishedSignal = false; + mDidGetDisconnectSignal = false; + mDidSendMessage = false; + mDidReceiveMessage = false; + mDidProcessRequest = false; + mDidProcessUnidirectional = false; + mDidProcessResponse = false; + } + + bool DidGetConnectedSignal(void) const { return mDidGetConnectedSignal; } + bool DidGetSessionEstablishedSignal(void) const { return mDidGetSessionEstablishedSignal; } + bool DidGetDisconnectSignal(void) const { return mDidGetDisconnectSignal; } + bool DidSendMessage(void) const { return mDidSendMessage; } + bool DidReceiveMessage(void) const { return mDidReceiveMessage; } + bool DidProcessRequest(void) const { return mDidProcessRequest; } + bool DidProcessUnidirectional(void) const { return mDidProcessUnidirectional; } + bool DidProcessResponse(void) const { return mDidProcessResponse; } + + uint8_t GetLastRxTestTlvValue(void) const { return mLastRxTestTlvValue; } + Dns::Header::Response GetLastRxResponseCode(void) const { return mLastRxResponseCode; } + + void SendTestRequestMessage(uint8_t aValue = 0, uint32_t aResponseTimeout = Dso::kResponseTimeout) + { + MessageId messageId; + + mLastTxTestTlvValue = aValue; + SuccessOrQuit(SendRequestMessage(PrepareTestMessage(aValue), messageId, aResponseTimeout)); + } + + void SendTestUnidirectionalMessage(uint8_t aValue = 0) + { + mLastTxTestTlvValue = aValue; + SuccessOrQuit(SendUnidirectionalMessage(PrepareTestMessage(aValue))); + } + +private: + Message &PrepareTestMessage(uint8_t aValue) + { + TestTlv testTlv; + Message *message = NewMessage(); + + VerifyOrQuit(message != nullptr); + testTlv.Init(aValue); + SuccessOrQuit(message->Append(testTlv)); + + return *message; + } + + void ParseTestMessage(const Message &aMessage) + { + TestTlv testTlv; + Dso::Tlv tlv; + uint16_t offset = aMessage.GetOffset(); + + // Test message MUST only contain Test TLV and Encryption + // Padding TLV. + + SuccessOrQuit(aMessage.Read(offset, testTlv)); + VerifyOrQuit(testTlv.GetType() == TestTlv::kType); + VerifyOrQuit(testTlv.IsValid()); + offset += testTlv.GetSize(); + mLastRxTestTlvValue = testTlv.GetValue(); + + SuccessOrQuit(aMessage.Read(offset, tlv)); + VerifyOrQuit(tlv.GetType() == Dso::Tlv::kEncryptionPaddingType); + offset += tlv.GetSize(); + + VerifyOrQuit(offset == aMessage.GetLength()); + } + + void SendTestResponseMessage(MessageId aResponseId, uint8_t aValue) + { + mLastTxTestTlvValue = aValue; + SuccessOrQuit(SendResponseMessage(PrepareTestMessage(aValue), aResponseId)); + } + + //--------------------------------------------------------------------- + // Callback methods + + void HandleConnected(void) { mDidGetConnectedSignal = true; } + void HandleSessionEstablished(void) { mDidGetSessionEstablishedSignal = true; } + void HandleDisconnected(void) { mDidGetDisconnectSignal = true; } + + Error ProcessRequestMessage(MessageId aMessageId, const Message &aMessage, Dso::Tlv::Type aPrimaryTlvType) + { + Error error = kErrorNone; + + Log(" ProcessRequestMessage(primaryTlv:0x%04x) on %s", aPrimaryTlvType, mName); + mDidProcessRequest = true; + + VerifyOrExit(aPrimaryTlvType == TestTlv::kType, error = kErrorNotFound); + ParseTestMessage(aMessage); + SendTestResponseMessage(aMessageId, mLastRxTestTlvValue); + + exit: + return error; + } + + Error ProcessUnidirectionalMessage(const Message &aMessage, Dso::Tlv::Type aPrimaryTlvType) + { + Log(" ProcessUnidirectionalMessage(primaryTlv:0x%04x) on %s", aPrimaryTlvType, mName); + mDidProcessUnidirectional = true; + + if (aPrimaryTlvType == TestTlv::kType) + { + ParseTestMessage(aMessage); + } + + return kErrorNone; + } + + Error ProcessResponseMessage(const Dns::Header &aHeader, + const Message & aMessage, + Dso::Tlv::Type aResponseTlvType, + Dso::Tlv::Type aRequestTlvType) + { + Error error = kErrorNone; + + mDidProcessResponse = true; + mLastRxResponseCode = aHeader.GetResponseCode(); + Log(" ProcessResponseMessage(responseTlv:0x%04x) on %s (response-Code:%u) ", aResponseTlvType, mName, + mLastRxResponseCode); + + VerifyOrExit(mLastRxResponseCode == Dns::Header::kResponseSuccess); + + // During test we only expect a Test TLV response with + // a matching TLV value to what was sent last. + + VerifyOrQuit(aResponseTlvType == TestTlv::kType); + VerifyOrQuit(aRequestTlvType == TestTlv::kType); + ParseTestMessage(aMessage); + VerifyOrQuit(mLastRxTestTlvValue == mLastTxTestTlvValue); + + exit: + return error; + } + + static void HandleConnected(Dso::Connection &aConnection) + { + static_cast(aConnection).HandleConnected(); + } + + static void HandleSessionEstablished(Dso::Connection &aConnection) + { + static_cast(aConnection).HandleSessionEstablished(); + } + + static void HandleDisconnected(Dso::Connection &aConnection) + { + static_cast(aConnection).HandleDisconnected(); + } + + static Error ProcessRequestMessage(Dso::Connection &aConnection, + MessageId aMessageId, + const Message & aMessage, + Dso::Tlv::Type aPrimaryTlvType) + { + return static_cast(aConnection).ProcessRequestMessage(aMessageId, aMessage, aPrimaryTlvType); + } + + static Error ProcessUnidirectionalMessage(Dso::Connection &aConnection, + const Message & aMessage, + Dso::Tlv::Type aPrimaryTlvType) + { + return static_cast(aConnection).ProcessUnidirectionalMessage(aMessage, aPrimaryTlvType); + } + + static Error ProcessResponseMessage(Dso::Connection & aConnection, + const Dns::Header &aHeader, + const Message & aMessage, + Dso::Tlv::Type aResponseTlvType, + Dso::Tlv::Type aRequestTlvType) + { + return static_cast(aConnection) + .ProcessResponseMessage(aHeader, aMessage, aResponseTlvType, aRequestTlvType); + } + + const char * mName; + Ip6::SockAddr mLocalSockAddr; + bool mDidGetConnectedSignal; + bool mDidGetSessionEstablishedSignal; + bool mDidGetDisconnectSignal; + bool mDidSendMessage; + bool mDidReceiveMessage; + bool mDidProcessRequest; + bool mDidProcessUnidirectional; + bool mDidProcessResponse; + uint8_t mLastTxTestTlvValue; + uint8_t mLastRxTestTlvValue; + Dns::Header::Response mLastRxResponseCode; + + static Callbacks sCallbacks; +}; + +Dso::Connection::Callbacks Connection::sCallbacks(Connection::HandleConnected, + Connection::HandleSessionEstablished, + Connection::HandleDisconnected, + Connection::ProcessRequestMessage, + Connection::ProcessUnidirectionalMessage, + Connection::ProcessResponseMessage); + +static constexpr uint16_t kMaxConnections = 5; + +static Array sConnections; + +static Connection *FindPeerConnection(const Connection &aConnetion) +{ + Connection *peerConn = nullptr; + + for (Connection *conn : sConnections) + { + if (conn->GetLocalSockAddr() == aConnetion.GetPeerSockAddr()) + { + peerConn = conn; + break; + } + } + + return peerConn; +} + +extern "C" { + +static bool sDsoListening = false; + +// This test flag indicates whether the `otPlatDso` API should +// forward a sent message to the peer connection. It can be set to +// `false` to drop the messages to test timeout behaviors on the +// peer. +static bool sTestDsoForwardMessageToPeer = true; + +// This test flag indicate whether when disconnecting a connection +// (using `otPlatDsoDisconnect()` to signal the peer connection about +// the disconnect. Default behavior is set to `true`. It can be set +// to `false` to test certain timeout behavior on peer side. +static bool sTestDsoSignalDisconnectToPeer = true; + +void otPlatDsoEnableListening(otInstance *, bool aEnable) +{ + Log(" otPlatDsoEnableListening(%s)", aEnable ? "true" : "false"); + sDsoListening = aEnable; +} + +void otPlatDsoConnect(otPlatDsoConnection *aConnection, const otSockAddr *aPeerSockAddr) +{ + Connection & conn = *static_cast(aConnection); + Connection * peerConn = nullptr; + const Ip6::SockAddr &peerSockAddr = AsCoreType(aPeerSockAddr); + + Log(" otPlatDsoConnect(%s, aPeer:0x%04x)", conn.GetName(), peerSockAddr.GetPort()); + + VerifyOrQuit(conn.GetPeerSockAddr() == peerSockAddr); + VerifyOrQuit(conn.GetState() == Connection::kStateConnecting); + + if (!sDsoListening) + { + Log(" Server is not listening"); + ExitNow(); + } + + peerConn = static_cast(otPlatDsoAccept(otPlatDsoGetInstance(aConnection), aPeerSockAddr)); + + if (peerConn == nullptr) + { + Log(" Request rejected"); + ExitNow(); + } + + Log(" Request accepted"); + VerifyOrQuit(peerConn->GetState() == Connection::kStateConnecting); + + Log(" Signalling `Connected` on peer connection (%s)", peerConn->GetName()); + otPlatDsoHandleConnected(peerConn); + + Log(" Signalling `Connected` on connection (%s)", conn.GetName()); + otPlatDsoHandleConnected(aConnection); + +exit: + return; +} + +void otPlatDsoSend(otPlatDsoConnection *aConnection, otMessage *aMessage) +{ + Connection &conn = *static_cast(aConnection); + Connection *peerConn = nullptr; + + Log(" otPlatDsoSend(%s), message-len:%u", conn.GetName(), AsCoreType(aMessage).GetLength()); + + VerifyOrQuit(conn.GetState() != Connection::kStateDisconnected); + VerifyOrQuit(conn.GetState() != Connection::kStateConnecting); + conn.mDidSendMessage = true; + + if (sTestDsoForwardMessageToPeer) + { + peerConn = FindPeerConnection(conn); + VerifyOrQuit(peerConn != nullptr); + + VerifyOrQuit(peerConn->GetState() != Connection::kStateDisconnected); + VerifyOrQuit(peerConn->GetState() != Connection::kStateConnecting); + + Log(" Sending the message to peer connection (%s)", peerConn->GetName()); + + peerConn->mDidReceiveMessage = true; + otPlatDsoHandleReceive(peerConn, aMessage); + } + else + { + Log(" Dropping the message"); + } +} + +void otPlatDsoDisconnect(otPlatDsoConnection *aConnection, otPlatDsoDisconnectMode aMode) +{ + Connection &conn = *static_cast(aConnection); + Connection *peerConn = nullptr; + + Log(" otPlatDsoDisconnect(%s, mode:%s)", conn.GetName(), + (aMode == OT_PLAT_DSO_DISCONNECT_MODE_GRACEFULLY_CLOSE) ? "close" : "abort"); + + VerifyOrQuit(conn.GetState() == Connection::kStateDisconnected); + + if (sTestDsoSignalDisconnectToPeer) + { + peerConn = FindPeerConnection(conn); + + if (peerConn == nullptr) + { + Log(" No peer connection found"); + } + else if (peerConn->GetState() == Connection::kStateDisconnected) + { + Log(" Peer connection (%s) already disconnected", peerConn->GetName()); + } + else + { + Log(" Signaling `Disconnected` on peer connection (%s)", peerConn->GetName()); + otPlatDsoHandleDisconnected(peerConn, aMode); + } + } +} + +} // extern "C" + +Dso::Connection *AcceptConnection(Instance &aInstance, const Ip6::SockAddr &aPeerSockAddr) +{ + OT_UNUSED_VARIABLE(aInstance); + + Connection *rval = nullptr; + + Log(" AcceptConnection(peer:0x%04x)", aPeerSockAddr.GetPort()); + + for (Connection *conn : sConnections) + { + if (conn->GetLocalSockAddr() == aPeerSockAddr) + { + VerifyOrQuit(conn->GetState() == Connection::kStateDisconnected); + rval = conn; + break; + } + } + + if (rval != nullptr) + { + Log(" Accepting and returning connection %s", rval->GetName()); + } + else + { + Log(" Rejecting"); + } + + return rval; +} + +static constexpr uint8_t kKeepAliveTestIterations = 3; + +static void VerifyKeepAliveExchange(Connection &aClientConn, + Connection &aServerConn, + uint32_t aKeepAliveInterval, + uint8_t aNumIterations = kKeepAliveTestIterations) +{ + for (uint8_t n = 0; n < aNumIterations; n++) + { + Log("- - - - - - - - - - - - - - - - - - - - - - - - - - - - - -"); + Log("Test Keep Alive message exchange, iter %d", n + 1); + + aClientConn.ClearTestFlags(); + aServerConn.ClearTestFlags(); + + AdvanceTime(aKeepAliveInterval - 1); + VerifyOrQuit(!aClientConn.DidSendMessage()); + VerifyOrQuit(!aServerConn.DidReceiveMessage()); + Log("No message before keep alive timeout"); + + AdvanceTime(1); + VerifyOrQuit(aClientConn.DidSendMessage()); + VerifyOrQuit(aServerConn.DidReceiveMessage()); + Log("KeepAlive message exchanged after keep alive time elapses"); + } +} + +void TestDso(void) +{ + static constexpr uint16_t kPortA = 0xaaaa; + static constexpr uint16_t kPortB = 0xbbbb; + + static constexpr Dso::Tlv::Type kUnknownTlvType = 0xf801; + + static constexpr uint32_t kRetryDelayInterval = TimeMilli::SecToMsec(3600); + static constexpr uint32_t kLongResponseTimeout = Dso::kResponseTimeout + TimeMilli::SecToMsec(17); + + Instance & instance = *static_cast(testInitInstance()); + Ip6::SockAddr serverSockAddr(kPortA); + Ip6::SockAddr clientSockAddr(kPortB); + Connection serverConn(instance, "serverConn", serverSockAddr, clientSockAddr); + Connection clientConn(instance, "clinetConn", clientSockAddr, serverSockAddr); + Message * message; + Dso::Tlv tlv; + Connection::MessageId messageId; + + sNow = 0; + sInstance = &instance; + + SuccessOrQuit(sConnections.PushBack(&serverConn)); + SuccessOrQuit(sConnections.PushBack(&clientConn)); + + VerifyOrQuit(serverConn.GetPeerSockAddr() == clientSockAddr); + VerifyOrQuit(clientConn.GetPeerSockAddr() == serverSockAddr); + + VerifyOrQuit(serverConn.GetState() == Connection::kStateDisconnected); + VerifyOrQuit(clientConn.GetState() == Connection::kStateDisconnected); + + instance.Get().StartListening(AcceptConnection); + + VerifyOrQuit(instance.Get().FindClientConnection(clientSockAddr) == nullptr); + VerifyOrQuit(instance.Get().FindServerConnection(clientSockAddr) == nullptr); + VerifyOrQuit(instance.Get().FindClientConnection(serverSockAddr) == nullptr); + VerifyOrQuit(instance.Get().FindServerConnection(serverSockAddr) == nullptr); + + Log("-------------------------------------------------------------------------------------------"); + Log("Connect from client to server"); + + clientConn.Connect(); + + VerifyOrQuit(clientConn.GetState() == Connection::kStateConnectedButSessionless); + VerifyOrQuit(serverConn.GetState() == Connection::kStateConnectedButSessionless); + + VerifyOrQuit(clientConn.IsClient()); + VerifyOrQuit(!clientConn.IsServer()); + + VerifyOrQuit(!serverConn.IsClient()); + VerifyOrQuit(serverConn.IsServer()); + + // Note that we find connection with a peer address + VerifyOrQuit(instance.Get().FindClientConnection(serverSockAddr) == &clientConn); + VerifyOrQuit(instance.Get().FindServerConnection(serverSockAddr) == nullptr); + VerifyOrQuit(instance.Get().FindClientConnection(clientSockAddr) == nullptr); + VerifyOrQuit(instance.Get().FindServerConnection(clientSockAddr) == &serverConn); + + VerifyOrQuit(clientConn.DidGetConnectedSignal()); + VerifyOrQuit(!clientConn.DidGetSessionEstablishedSignal()); + VerifyOrQuit(!clientConn.DidGetDisconnectSignal()); + + VerifyOrQuit(serverConn.DidGetConnectedSignal()); + VerifyOrQuit(!serverConn.DidGetSessionEstablishedSignal()); + VerifyOrQuit(!serverConn.DidGetDisconnectSignal()); + + Log("- - - - - - - - - - - - - - - - - - - - - - - - - - - - - -"); + Log("Send keep alive message to establish connection"); + + clientConn.ClearTestFlags(); + serverConn.ClearTestFlags(); + + SuccessOrQuit(clientConn.SendKeepAliveMessage()); + + VerifyOrQuit(clientConn.GetState() == Connection::kStateSessionEstablished); + VerifyOrQuit(serverConn.GetState() == Connection::kStateSessionEstablished); + + VerifyOrQuit(!clientConn.DidGetConnectedSignal()); + VerifyOrQuit(clientConn.DidGetSessionEstablishedSignal()); + VerifyOrQuit(!clientConn.DidGetDisconnectSignal()); + + VerifyOrQuit(!serverConn.DidGetConnectedSignal()); + VerifyOrQuit(serverConn.DidGetSessionEstablishedSignal()); + VerifyOrQuit(!serverConn.DidGetDisconnectSignal()); + + VerifyOrQuit(clientConn.GetKeepAliveInterval() == Dso::kDefaultTimeout); + VerifyOrQuit(clientConn.GetInactivityTimeout() == Dso::kDefaultTimeout); + VerifyOrQuit(serverConn.GetKeepAliveInterval() == Dso::kDefaultTimeout); + VerifyOrQuit(serverConn.GetInactivityTimeout() == Dso::kDefaultTimeout); + + Log("- - - - - - - - - - - - - - - - - - - - - - - - - - - - - -"); + Log("Close connection"); + + clientConn.ClearTestFlags(); + serverConn.ClearTestFlags(); + + clientConn.Disconnect(Connection::kGracefullyClose, Connection::kReasonInactivityTimeout); + + VerifyOrQuit(clientConn.GetState() == Connection::kStateDisconnected); + VerifyOrQuit(clientConn.GetDisconnectReason() == Connection::kReasonInactivityTimeout); + + VerifyOrQuit(serverConn.GetState() == Connection::kStateDisconnected); + VerifyOrQuit(serverConn.GetDisconnectReason() == Connection::kReasonPeerClosed); + + VerifyOrQuit(!clientConn.DidGetConnectedSignal()); + VerifyOrQuit(!clientConn.DidGetSessionEstablishedSignal()); + VerifyOrQuit(!clientConn.DidGetDisconnectSignal()); + + VerifyOrQuit(!serverConn.DidGetConnectedSignal()); + VerifyOrQuit(!serverConn.DidGetSessionEstablishedSignal()); + VerifyOrQuit(serverConn.DidGetDisconnectSignal()); + + VerifyOrQuit(instance.Get().FindClientConnection(clientSockAddr) == nullptr); + VerifyOrQuit(instance.Get().FindServerConnection(clientSockAddr) == nullptr); + VerifyOrQuit(instance.Get().FindClientConnection(serverSockAddr) == nullptr); + VerifyOrQuit(instance.Get().FindServerConnection(serverSockAddr) == nullptr); + + Log("-------------------------------------------------------------------------------------------"); + Log("Connection timeout when server is not listening"); + + instance.Get().StopListening(); + + clientConn.ClearTestFlags(); + + clientConn.Connect(); + VerifyOrQuit(clientConn.GetState() == Connection::kStateConnecting); + VerifyOrQuit(instance.Get().FindClientConnection(serverSockAddr) == &clientConn); + VerifyOrQuit(instance.Get().FindServerConnection(serverSockAddr) == nullptr); + + AdvanceTime(Dso::kConnectingTimeout); + + VerifyOrQuit(clientConn.GetState() == Connection::kStateDisconnected); + VerifyOrQuit(clientConn.GetDisconnectReason() == Connection::kReasonFailedToConnect); + VerifyOrQuit(instance.Get().FindClientConnection(serverSockAddr) == nullptr); + VerifyOrQuit(instance.Get().FindServerConnection(serverSockAddr) == nullptr); + + VerifyOrQuit(!clientConn.DidGetConnectedSignal()); + VerifyOrQuit(!clientConn.DidGetSessionEstablishedSignal()); + VerifyOrQuit(clientConn.DidGetDisconnectSignal()); + + Log("-------------------------------------------------------------------------------------------"); + Log("Keep Alive Timeout behavior"); + + // Keep Alive timeout smaller than min value should be rejected. + VerifyOrQuit(clientConn.SetTimeouts(Dso::kInfiniteTimeout, Dso::kMinKeepAliveInterval - 1) == kErrorInvalidArgs); + + instance.Get().StartListening(AcceptConnection); + SuccessOrQuit(serverConn.SetTimeouts(Dso::kInfiniteTimeout, Dso::kMinKeepAliveInterval)); + + VerifyOrQuit(serverConn.GetKeepAliveInterval() == Dso::kMinKeepAliveInterval); + VerifyOrQuit(serverConn.GetInactivityTimeout() == Dso::kInfiniteTimeout); + + clientConn.Connect(); + SuccessOrQuit(clientConn.SendKeepAliveMessage()); + VerifyOrQuit(clientConn.GetState() == Connection::kStateSessionEstablished); + VerifyOrQuit(serverConn.GetState() == Connection::kStateSessionEstablished); + + VerifyOrQuit(serverConn.GetKeepAliveInterval() == Dso::kMinKeepAliveInterval); + VerifyOrQuit(serverConn.GetInactivityTimeout() == Dso::kInfiniteTimeout); + VerifyOrQuit(clientConn.GetKeepAliveInterval() == Dso::kMinKeepAliveInterval); + VerifyOrQuit(clientConn.GetInactivityTimeout() == Dso::kInfiniteTimeout); + + VerifyKeepAliveExchange(clientConn, serverConn, Dso::kMinKeepAliveInterval); + + Log("- - - - - - - - - - - - - - - - - - - - - - - - - - - - - -"); + Log("Change Keep Alive interval on server"); + + clientConn.ClearTestFlags(); + serverConn.ClearTestFlags(); + + SuccessOrQuit(serverConn.SetTimeouts(Dso::kInfiniteTimeout, Dso::kDefaultTimeout)); + + VerifyOrQuit(serverConn.DidSendMessage()); + VerifyOrQuit(clientConn.DidReceiveMessage()); + VerifyOrQuit(!clientConn.DidSendMessage()); + + VerifyOrQuit(serverConn.GetKeepAliveInterval() == Dso::kDefaultTimeout); + VerifyOrQuit(serverConn.GetInactivityTimeout() == Dso::kInfiniteTimeout); + VerifyOrQuit(clientConn.GetKeepAliveInterval() == Dso::kDefaultTimeout); + VerifyOrQuit(clientConn.GetInactivityTimeout() == Dso::kInfiniteTimeout); + + VerifyKeepAliveExchange(clientConn, serverConn, Dso::kDefaultTimeout); + + Log("- - - - - - - - - - - - - - - - - - - - - - - - - - - - - -"); + Log("Keep Alive timer clear on message send or receive"); + + clientConn.ClearTestFlags(); + serverConn.ClearTestFlags(); + + AdvanceTime(Dso::kDefaultTimeout / 2); + + clientConn.SendTestUnidirectionalMessage(); + VerifyOrQuit(clientConn.DidSendMessage()); + VerifyOrQuit(serverConn.DidReceiveMessage()); + VerifyOrQuit(!serverConn.DidSendMessage()); + VerifyOrQuit(clientConn.GetState() == Connection::kStateSessionEstablished); + VerifyOrQuit(serverConn.GetState() == Connection::kStateSessionEstablished); + Log("Sent unidirectional message (client->server) at half the keep alive interval"); + VerifyKeepAliveExchange(clientConn, serverConn, Dso::kDefaultTimeout, 1); + + clientConn.ClearTestFlags(); + serverConn.ClearTestFlags(); + + AdvanceTime(Dso::kDefaultTimeout / 2); + + serverConn.SendTestUnidirectionalMessage(); + VerifyOrQuit(serverConn.DidSendMessage()); + VerifyOrQuit(clientConn.DidReceiveMessage()); + VerifyOrQuit(!clientConn.DidSendMessage()); + VerifyOrQuit(clientConn.GetState() == Connection::kStateSessionEstablished); + VerifyOrQuit(serverConn.GetState() == Connection::kStateSessionEstablished); + Log("Sent unidirectional message (server->client) at half the keep alive interval"); + VerifyKeepAliveExchange(clientConn, serverConn, Dso::kDefaultTimeout, 1); + + Log("- - - - - - - - - - - - - - - - - - - - - - - - - - - - - -"); + Log("Keep Alive timeout on server"); + + clientConn.ClearTestFlags(); + serverConn.ClearTestFlags(); + + Log("Drop all sent message (drop Keep Alive msg from client->server)"); + sTestDsoForwardMessageToPeer = false; + + AdvanceTime(Dso::kDefaultTimeout); + VerifyOrQuit(clientConn.DidSendMessage()); + VerifyOrQuit(!serverConn.DidReceiveMessage()); + VerifyOrQuit(clientConn.GetState() == Connection::kStateSessionEstablished); + VerifyOrQuit(serverConn.GetState() == Connection::kStateSessionEstablished); + + Log("Sever waits for twice the interval before Keep Alive timeout"); + AdvanceTime(Dso::kDefaultTimeout); + + VerifyOrQuit(serverConn.GetState() == Connection::kStateDisconnected); + VerifyOrQuit(serverConn.GetDisconnectReason() == Connection::kReasonKeepAliveTimeout); + + VerifyOrQuit(clientConn.GetState() == Connection::kStateDisconnected); + VerifyOrQuit(clientConn.GetDisconnectReason() == Connection::kReasonPeerAborted); + Log("Server aborted connection on Keep Alive timeout"); + sTestDsoForwardMessageToPeer = true; + + Log("-------------------------------------------------------------------------------------------"); + Log("Inactivity Timeout behavior"); + + SuccessOrQuit(serverConn.SetTimeouts(Dso::kDefaultTimeout, Dso::kMinKeepAliveInterval)); + + VerifyOrQuit(serverConn.GetKeepAliveInterval() == Dso::kMinKeepAliveInterval); + VerifyOrQuit(serverConn.GetInactivityTimeout() == Dso::kDefaultTimeout); + + clientConn.Connect(); + SuccessOrQuit(clientConn.SendKeepAliveMessage()); + VerifyOrQuit(clientConn.GetState() == Connection::kStateSessionEstablished); + VerifyOrQuit(serverConn.GetState() == Connection::kStateSessionEstablished); + + VerifyOrQuit(serverConn.GetKeepAliveInterval() == Dso::kMinKeepAliveInterval); + VerifyOrQuit(serverConn.GetInactivityTimeout() == Dso::kDefaultTimeout); + VerifyOrQuit(clientConn.GetKeepAliveInterval() == Dso::kMinKeepAliveInterval); + VerifyOrQuit(clientConn.GetInactivityTimeout() == Dso::kDefaultTimeout); + + Log("Sending a unidirectional message should clear inactivity timer"); + AdvanceTime(Dso::kDefaultTimeout / 2); + clientConn.SendTestUnidirectionalMessage(); + + AdvanceTime(Dso::kDefaultTimeout - 1); + VerifyOrQuit(clientConn.GetState() == Connection::kStateSessionEstablished); + VerifyOrQuit(serverConn.GetState() == Connection::kStateSessionEstablished); + Log("Client keeps the connection up to the inactivity timeout"); + + AdvanceTime(1); + VerifyOrQuit(clientConn.GetState() == Connection::kStateDisconnected); + VerifyOrQuit(clientConn.GetDisconnectReason() == Connection::kReasonInactivityTimeout); + VerifyOrQuit(serverConn.GetState() == Connection::kStateDisconnected); + VerifyOrQuit(serverConn.GetDisconnectReason() == Connection::kReasonPeerClosed); + Log("Client closes the connection gracefully on inactivity timeout"); + + Log("- - - - - - - - - - - - - - - - - - - - - - - - - - - - - -"); + Log("Increasing inactivity timeout in middle"); + + clientConn.Connect(); + SuccessOrQuit(clientConn.SendKeepAliveMessage()); + VerifyOrQuit(clientConn.GetState() == Connection::kStateSessionEstablished); + VerifyOrQuit(serverConn.GetState() == Connection::kStateSessionEstablished); + + AdvanceTime(TimeMilli::SecToMsec(10)); + Log("After 10 sec elapses, change the inactivity timeout from 15 to 20 sec"); + SuccessOrQuit(serverConn.SetTimeouts(TimeMilli::SecToMsec(20), Dso::kMinKeepAliveInterval)); + + AdvanceTime(TimeMilli::SecToMsec(10) - 1); + VerifyOrQuit(clientConn.GetState() == Connection::kStateSessionEstablished); + VerifyOrQuit(serverConn.GetState() == Connection::kStateSessionEstablished); + Log("Client keeps the connection up to new 20 sec inactivity timeout"); + + AdvanceTime(1); + VerifyOrQuit(clientConn.GetState() == Connection::kStateDisconnected); + VerifyOrQuit(clientConn.GetDisconnectReason() == Connection::kReasonInactivityTimeout); + VerifyOrQuit(serverConn.GetState() == Connection::kStateDisconnected); + VerifyOrQuit(serverConn.GetDisconnectReason() == Connection::kReasonPeerClosed); + Log("Client closes the connection gracefully on inactivity timeout of 20 sec"); + + Log("- - - - - - - - - - - - - - - - - - - - - - - - - - - - - -"); + Log("Decreasing inactivity timeout in middle"); + + clientConn.Connect(); + SuccessOrQuit(clientConn.SendKeepAliveMessage()); + VerifyOrQuit(clientConn.GetState() == Connection::kStateSessionEstablished); + VerifyOrQuit(serverConn.GetState() == Connection::kStateSessionEstablished); + + AdvanceTime(TimeMilli::SecToMsec(10)); + Log("After 10 sec elapses, change the inactivity timeout from 15 to 10 sec"); + SuccessOrQuit(serverConn.SetTimeouts(TimeMilli::SecToMsec(10), Dso::kMinKeepAliveInterval)); + + AdvanceTime(0); + VerifyOrQuit(clientConn.GetState() == Connection::kStateDisconnected); + VerifyOrQuit(clientConn.GetDisconnectReason() == Connection::kReasonInactivityTimeout); + VerifyOrQuit(serverConn.GetState() == Connection::kStateDisconnected); + VerifyOrQuit(serverConn.GetDisconnectReason() == Connection::kReasonPeerClosed); + Log("Client closes the connection gracefully on new shorter inactivity timeout of 10 sec"); + + Log("- - - - - - - - - - - - - - - - - - - - - - - - - - - - - -"); + Log("Changing inactivity timeout from infinite to finite"); + + SuccessOrQuit(serverConn.SetTimeouts(Dso::kDefaultTimeout, Dso::kInfiniteTimeout)); + clientConn.Connect(); + SuccessOrQuit(clientConn.SendKeepAliveMessage()); + VerifyOrQuit(clientConn.GetState() == Connection::kStateSessionEstablished); + VerifyOrQuit(serverConn.GetState() == Connection::kStateSessionEstablished); + + AdvanceTime(TimeMilli::SecToMsec(6)); + Log("After 6 sec, change the inactivity to infinite"); + SuccessOrQuit(serverConn.SetTimeouts(Dso::kInfiniteTimeout, Dso::kInfiniteTimeout)); + + AdvanceTime(TimeMilli::SecToMsec(4)); + Log("After 4 sec, change the inactivity timeout from infinite to 20 sec"); + SuccessOrQuit(serverConn.SetTimeouts(TimeMilli::SecToMsec(20), Dso::kInfiniteTimeout)); + + AdvanceTime(TimeMilli::SecToMsec(10) - 1); + VerifyOrQuit(clientConn.GetState() == Connection::kStateSessionEstablished); + VerifyOrQuit(serverConn.GetState() == Connection::kStateSessionEstablished); + + AdvanceTime(1); + VerifyOrQuit(clientConn.GetState() == Connection::kStateDisconnected); + VerifyOrQuit(clientConn.GetDisconnectReason() == Connection::kReasonInactivityTimeout); + VerifyOrQuit(serverConn.GetState() == Connection::kStateDisconnected); + VerifyOrQuit(serverConn.GetDisconnectReason() == Connection::kReasonPeerClosed); + Log("Client closes the connection gracefully after 20 sec since last activity"); + + Log("- - - - - - - - - - - - - - - - - - - - - - - - - - - - - -"); + Log("Tracking activity while inactivity timeout is infinite"); + + SuccessOrQuit(serverConn.SetTimeouts(Dso::kInfiniteTimeout, Dso::kInfiniteTimeout)); + clientConn.Connect(); + SuccessOrQuit(clientConn.SendKeepAliveMessage()); + VerifyOrQuit(clientConn.GetState() == Connection::kStateSessionEstablished); + VerifyOrQuit(serverConn.GetState() == Connection::kStateSessionEstablished); + + AdvanceTime(TimeMilli::SecToMsec(7)); + Log("After 7 sec, send a test message, this clears inactivity timer"); + serverConn.SendTestUnidirectionalMessage(); + + AdvanceTime(TimeMilli::SecToMsec(10)); + Log("After 10 sec, change the inactivity timeout from infinite to 15 sec"); + SuccessOrQuit(serverConn.SetTimeouts(TimeMilli::SecToMsec(15), Dso::kInfiniteTimeout)); + + AdvanceTime(TimeMilli::SecToMsec(5) - 1); + VerifyOrQuit(clientConn.GetState() == Connection::kStateSessionEstablished); + VerifyOrQuit(serverConn.GetState() == Connection::kStateSessionEstablished); + + AdvanceTime(1); + VerifyOrQuit(clientConn.GetState() == Connection::kStateDisconnected); + VerifyOrQuit(clientConn.GetDisconnectReason() == Connection::kReasonInactivityTimeout); + VerifyOrQuit(serverConn.GetState() == Connection::kStateDisconnected); + VerifyOrQuit(serverConn.GetDisconnectReason() == Connection::kReasonPeerClosed); + Log("Client closes the connection gracefully after 15 sec since last activity"); + + Log("- - - - - - - - - - - - - - - - - - - - - - - - - - - - - -"); + Log("Inactivity timeout on server"); + + clientConn.Connect(); + SuccessOrQuit(clientConn.SendKeepAliveMessage()); + VerifyOrQuit(clientConn.GetState() == Connection::kStateSessionEstablished); + VerifyOrQuit(serverConn.GetState() == Connection::kStateSessionEstablished); + + SuccessOrQuit(serverConn.SetTimeouts(Dso::kDefaultTimeout, Dso::kInfiniteTimeout)); + + Log("Wait for inactivity timeout and ensure client disconnect"); + Log("Configure test for client not to signal its disconnect to server"); + sTestDsoSignalDisconnectToPeer = false; + + AdvanceTime(Dso::kDefaultTimeout); + VerifyOrQuit(clientConn.GetState() == Connection::kStateDisconnected); + VerifyOrQuit(clientConn.GetDisconnectReason() == Connection::kReasonInactivityTimeout); + VerifyOrQuit(serverConn.GetState() == Connection::kStateSessionEstablished); + sTestDsoSignalDisconnectToPeer = true; + + Log("Server should disconnect after twice the inactivity timeout"); + AdvanceTime(Dso::kDefaultTimeout - 1); + VerifyOrQuit(serverConn.GetState() == Connection::kStateSessionEstablished); + AdvanceTime(1); + VerifyOrQuit(serverConn.GetState() == Connection::kStateDisconnected); + VerifyOrQuit(serverConn.GetDisconnectReason() == Connection::kReasonInactivityTimeout); + + Log("- - - - - - - - - - - - - - - - - - - - - - - - - - - - - -"); + Log("Server reducing inactivity timeout to expired (on server)"); + + clientConn.Connect(); + SuccessOrQuit(clientConn.SendKeepAliveMessage()); + VerifyOrQuit(clientConn.GetState() == Connection::kStateSessionEstablished); + VerifyOrQuit(serverConn.GetState() == Connection::kStateSessionEstablished); + SuccessOrQuit(serverConn.SetTimeouts(Dso::kDefaultTimeout, Dso::kInfiniteTimeout)); + + AdvanceTime(TimeMilli::SecToMsec(10)); + Log("After 11 sec elapses, change the inactivity timeout from 15 to 2 sec"); + SuccessOrQuit(serverConn.SetTimeouts(TimeMilli::SecToMsec(2), Dso::kMinKeepAliveInterval)); + + sTestDsoSignalDisconnectToPeer = false; + AdvanceTime(0); + VerifyOrQuit(clientConn.GetState() == Connection::kStateDisconnected); + VerifyOrQuit(clientConn.GetDisconnectReason() == Connection::kReasonInactivityTimeout); + sTestDsoSignalDisconnectToPeer = true; + Log("Client closes the connection gracefully on expired timeout"); + Log("Configure test for client not to signal its disconnect to server"); + + AdvanceTime(Dso::kMinServerInactivityWaitTime - 1); + VerifyOrQuit(serverConn.GetState() == Connection::kStateSessionEstablished); + AdvanceTime(1); + VerifyOrQuit(serverConn.GetState() == Connection::kStateDisconnected); + VerifyOrQuit(serverConn.GetDisconnectReason() == Connection::kReasonInactivityTimeout); + Log("Server wait for kMinServerInactivityWaitTime (5 sec) before closing on expired timeout"); + + Log("- - - - - - - - - - - - - - - - - - - - - - - - - - - - - -"); + Log("Long-lived operation"); + + clientConn.Connect(); + SuccessOrQuit(clientConn.SendKeepAliveMessage()); + VerifyOrQuit(clientConn.GetState() == Connection::kStateSessionEstablished); + VerifyOrQuit(serverConn.GetState() == Connection::kStateSessionEstablished); + SuccessOrQuit(serverConn.SetTimeouts(Dso::kDefaultTimeout, Dso::kInfiniteTimeout)); + + clientConn.SetLongLivedOperation(true); + serverConn.SetLongLivedOperation(true); + + AdvanceTime(2 * Dso::kDefaultTimeout); + VerifyOrQuit(clientConn.GetState() == Connection::kStateSessionEstablished); + VerifyOrQuit(serverConn.GetState() == Connection::kStateSessionEstablished); + + clientConn.SetLongLivedOperation(false); + AdvanceTime(0); + VerifyOrQuit(clientConn.GetState() == Connection::kStateDisconnected); + VerifyOrQuit(serverConn.GetState() == Connection::kStateDisconnected); + + Log("-------------------------------------------------------------------------------------------"); + Log("Request, response, and unidirectional message exchange"); + + SuccessOrQuit(serverConn.SetTimeouts(Dso::kDefaultTimeout, Dso::kDefaultTimeout)); + clientConn.Connect(); + + VerifyOrQuit(clientConn.GetState() == Connection::kStateConnectedButSessionless); + VerifyOrQuit(serverConn.GetState() == Connection::kStateConnectedButSessionless); + + clientConn.ClearTestFlags(); + serverConn.ClearTestFlags(); + + Log("- - - - - - - - - - - - - - - - - - - - - - - - - - - - - -"); + Log("Establish connection using test message request/response"); + clientConn.SendTestRequestMessage(); + + VerifyOrQuit(clientConn.GetState() == Connection::kStateSessionEstablished); + VerifyOrQuit(serverConn.GetState() == Connection::kStateSessionEstablished); + VerifyOrQuit(serverConn.DidProcessRequest()); + VerifyOrQuit(clientConn.DidProcessResponse()); + + Log("- - - - - - - - - - - - - - - - - - - - - - - - - - - - - -"); + Log("Send unidirectional test message"); + + serverConn.ClearTestFlags(); + clientConn.SendTestUnidirectionalMessage(0x10); + VerifyOrQuit(serverConn.DidProcessUnidirectional()); + VerifyOrQuit(serverConn.GetLastRxTestTlvValue() == 0x10); + + clientConn.ClearTestFlags(); + serverConn.SendTestUnidirectionalMessage(0x20); + VerifyOrQuit(clientConn.DidProcessUnidirectional()); + VerifyOrQuit(clientConn.GetLastRxTestTlvValue() == 0x20); + + Log("- - - - - - - - - - - - - - - - - - - - - - - - - - - - - -"); + Log("Exchange request and response"); + + clientConn.ClearTestFlags(); + serverConn.ClearTestFlags(); + serverConn.SendTestRequestMessage(0x30); + VerifyOrQuit(clientConn.DidProcessRequest()); + VerifyOrQuit(!clientConn.DidProcessResponse()); + VerifyOrQuit(!serverConn.DidProcessRequest()); + VerifyOrQuit(serverConn.DidProcessResponse()); + VerifyOrQuit(serverConn.GetLastRxTestTlvValue() == 0x30); + VerifyOrQuit(clientConn.GetLastRxTestTlvValue() == 0x30); + + clientConn.ClearTestFlags(); + serverConn.ClearTestFlags(); + clientConn.SendTestRequestMessage(0x40); + VerifyOrQuit(!clientConn.DidProcessRequest()); + VerifyOrQuit(clientConn.DidProcessResponse()); + VerifyOrQuit(serverConn.DidProcessRequest()); + VerifyOrQuit(!serverConn.DidProcessResponse()); + VerifyOrQuit(serverConn.GetLastRxTestTlvValue() == 0x40); + VerifyOrQuit(clientConn.GetLastRxTestTlvValue() == 0x40); + + VerifyOrQuit(clientConn.GetState() == Connection::kStateSessionEstablished); + VerifyOrQuit(serverConn.GetState() == Connection::kStateSessionEstablished); + + Log("- - - - - - - - - - - - - - - - - - - - - - - - - - - - - -"); + Log("Send unknown TLV request"); + + clientConn.ClearTestFlags(); + serverConn.ClearTestFlags(); + + message = clientConn.NewMessage(); + VerifyOrQuit(message != nullptr); + tlv.Init(kUnknownTlvType, 0); + SuccessOrQuit(message->Append(tlv)); + SuccessOrQuit(clientConn.SendRequestMessage(*message, messageId)); + + VerifyOrQuit(!clientConn.DidProcessRequest()); + VerifyOrQuit(clientConn.DidProcessResponse()); + VerifyOrQuit(serverConn.DidProcessRequest()); + VerifyOrQuit(!serverConn.DidProcessResponse()); + VerifyOrQuit(clientConn.GetLastRxResponseCode() == Dns::Header::kDsoTypeNotImplemented); + Log("Received a response with DSO Type Unknown error code"); + + VerifyOrQuit(clientConn.GetState() == Connection::kStateSessionEstablished); + VerifyOrQuit(serverConn.GetState() == Connection::kStateSessionEstablished); + + Log("- - - - - - - - - - - - - - - - - - - - - - - - - - - - - -"); + Log("Send unknown TLV unidirectional"); + + clientConn.ClearTestFlags(); + serverConn.ClearTestFlags(); + + message = clientConn.NewMessage(); + VerifyOrQuit(message != nullptr); + tlv.Init(kUnknownTlvType, 0); + SuccessOrQuit(message->Append(tlv)); + SuccessOrQuit(clientConn.SendUnidirectionalMessage(*message)); + VerifyOrQuit(serverConn.DidProcessUnidirectional()); + Log("Unknown TLV unidirectional is correctly ignored"); + VerifyOrQuit(clientConn.GetState() == Connection::kStateSessionEstablished); + VerifyOrQuit(serverConn.GetState() == Connection::kStateSessionEstablished); + + Log("- - - - - - - - - - - - - - - - - - - - - - - - - - - - - -"); + Log("Send malformed/invalid request"); + + clientConn.ClearTestFlags(); + serverConn.ClearTestFlags(); + + message = clientConn.NewMessage(); + VerifyOrQuit(message != nullptr); + tlv.Init(Dso::Tlv::kEncryptionPaddingType, 0); + SuccessOrQuit(message->Append(tlv)); + + SuccessOrQuit(serverConn.SendRequestMessage(*message, messageId)); + VerifyOrQuit(clientConn.GetState() == Connection::kStateDisconnected); + VerifyOrQuit(serverConn.GetState() == Connection::kStateDisconnected); + VerifyOrQuit(clientConn.GetDisconnectReason() == Connection::kReasonPeerMisbehavior); + VerifyOrQuit(serverConn.GetDisconnectReason() == Connection::kReasonPeerAborted); + VerifyOrQuit(clientConn.DidGetDisconnectSignal()); + VerifyOrQuit(serverConn.DidGetDisconnectSignal()); + Log("Client aborted on invalid request message"); + + Log("- - - - - - - - - - - - - - - - - - - - - - - - - - - - - -"); + Log("Response timeout during session establishment"); + + clientConn.ClearTestFlags(); + serverConn.ClearTestFlags(); + + SuccessOrQuit(serverConn.SetTimeouts(Dso::kResponseTimeout, Dso::kInfiniteTimeout)); + clientConn.Connect(); + VerifyOrQuit(clientConn.GetState() == Connection::kStateConnectedButSessionless); + VerifyOrQuit(serverConn.GetState() == Connection::kStateConnectedButSessionless); + + sTestDsoForwardMessageToPeer = false; + clientConn.SendTestRequestMessage(); + sTestDsoForwardMessageToPeer = true; + + VerifyOrQuit(clientConn.GetState() == Connection::kStateEstablishingSession); + VerifyOrQuit(serverConn.GetState() == Connection::kStateConnectedButSessionless); + + sTestDsoSignalDisconnectToPeer = false; + AdvanceTime(Dso::kResponseTimeout); + sTestDsoSignalDisconnectToPeer = true; + VerifyOrQuit(clientConn.GetState() == Connection::kStateDisconnected); + VerifyOrQuit(clientConn.GetDisconnectReason() == Connection::kReasonResponseTimeout); + VerifyOrQuit(clientConn.DidGetDisconnectSignal()); + VerifyOrQuit(serverConn.GetState() == Connection::kStateConnectedButSessionless); + Log("Client disconnected after response timeout"); + + AdvanceTime(Dso::kResponseTimeout); + VerifyOrQuit(serverConn.GetState() == Connection::kStateDisconnected); + VerifyOrQuit(serverConn.GetDisconnectReason() == Connection::kReasonInactivityTimeout); + VerifyOrQuit(serverConn.DidGetDisconnectSignal()); + Log("Server disconnected after twice the inactivity timeout"); + + Log("- - - - - - - - - - - - - - - - - - - - - - - - - - - - - -"); + Log("Response timeout after session establishment"); + + clientConn.ClearTestFlags(); + serverConn.ClearTestFlags(); + + SuccessOrQuit(serverConn.SetTimeouts(Dso::kInfiniteTimeout, Dso::kInfiniteTimeout)); + clientConn.Connect(); + SuccessOrQuit(clientConn.SendKeepAliveMessage()); + VerifyOrQuit(clientConn.GetState() == Connection::kStateSessionEstablished); + VerifyOrQuit(serverConn.GetState() == Connection::kStateSessionEstablished); + + sTestDsoForwardMessageToPeer = false; + serverConn.SendTestRequestMessage(); + sTestDsoForwardMessageToPeer = true; + + VerifyOrQuit(clientConn.GetState() == Connection::kStateSessionEstablished); + VerifyOrQuit(serverConn.GetState() == Connection::kStateSessionEstablished); + + AdvanceTime(Dso::kResponseTimeout - 1); + VerifyOrQuit(clientConn.GetState() == Connection::kStateSessionEstablished); + VerifyOrQuit(serverConn.GetState() == Connection::kStateSessionEstablished); + + AdvanceTime(1); + VerifyOrQuit(serverConn.GetState() == Connection::kStateDisconnected); + VerifyOrQuit(clientConn.GetState() == Connection::kStateDisconnected); + VerifyOrQuit(serverConn.GetDisconnectReason() == Connection::kReasonResponseTimeout); + VerifyOrQuit(clientConn.GetDisconnectReason() == Connection::kReasonPeerAborted); + VerifyOrQuit(serverConn.DidGetDisconnectSignal()); + VerifyOrQuit(clientConn.DidGetDisconnectSignal()); + + clientConn.ClearTestFlags(); + serverConn.ClearTestFlags(); + + SuccessOrQuit(serverConn.SetTimeouts(Dso::kInfiniteTimeout, Dso::kInfiniteTimeout)); + clientConn.Connect(); + SuccessOrQuit(clientConn.SendKeepAliveMessage()); + VerifyOrQuit(clientConn.GetState() == Connection::kStateSessionEstablished); + VerifyOrQuit(serverConn.GetState() == Connection::kStateSessionEstablished); + + sTestDsoForwardMessageToPeer = false; + serverConn.SendTestRequestMessage(0, kLongResponseTimeout); + sTestDsoForwardMessageToPeer = true; + + VerifyOrQuit(clientConn.GetState() == Connection::kStateSessionEstablished); + VerifyOrQuit(serverConn.GetState() == Connection::kStateSessionEstablished); + + AdvanceTime(kLongResponseTimeout - 1); + VerifyOrQuit(clientConn.GetState() == Connection::kStateSessionEstablished); + VerifyOrQuit(serverConn.GetState() == Connection::kStateSessionEstablished); + + AdvanceTime(1); + VerifyOrQuit(serverConn.GetState() == Connection::kStateDisconnected); + VerifyOrQuit(clientConn.GetState() == Connection::kStateDisconnected); + VerifyOrQuit(serverConn.GetDisconnectReason() == Connection::kReasonResponseTimeout); + VerifyOrQuit(clientConn.GetDisconnectReason() == Connection::kReasonPeerAborted); + VerifyOrQuit(serverConn.DidGetDisconnectSignal()); + VerifyOrQuit(clientConn.DidGetDisconnectSignal()); + + Log("-------------------------------------------------------------------------------------------"); + Log("Retry Delay message"); + + clientConn.ClearTestFlags(); + serverConn.ClearTestFlags(); + + SuccessOrQuit(serverConn.SetTimeouts(Dso::kInfiniteTimeout, Dso::kInfiniteTimeout)); + clientConn.Connect(); + SuccessOrQuit(clientConn.SendKeepAliveMessage()); + VerifyOrQuit(clientConn.GetState() == Connection::kStateSessionEstablished); + VerifyOrQuit(serverConn.GetState() == Connection::kStateSessionEstablished); + + SuccessOrQuit(serverConn.SendRetryDelayMessage(kRetryDelayInterval, Dns::Header::kResponseServerFailure)); + + VerifyOrQuit(clientConn.GetState() == Connection::kStateDisconnected); + VerifyOrQuit(serverConn.GetState() == Connection::kStateDisconnected); + VerifyOrQuit(clientConn.DidGetDisconnectSignal()); + VerifyOrQuit(serverConn.DidGetDisconnectSignal()); + VerifyOrQuit(clientConn.GetDisconnectReason() == Connection::kReasonServerRetryDelayRequest); + VerifyOrQuit(serverConn.GetDisconnectReason() == Connection::kReasonPeerClosed); + + VerifyOrQuit(clientConn.GetRetryDelay() == kRetryDelayInterval); + VerifyOrQuit(clientConn.GetRetryDelayErrorCode() == Dns::Header::kResponseServerFailure); + + Log("End of test"); + + testFreeInstance(&instance); +} + +} // namespace Dns +} // namespace ot + +#endif // OPENTHREAD_CONFIG_DNS_DSO_ENABLE + +int main(void) +{ +#if OPENTHREAD_CONFIG_DNS_DSO_ENABLE + ot::Dns::TestDso(); + printf("All tests passed\n"); +#else + printf("DSO feature is not enabled\n"); +#endif + + return 0; +} diff --git a/tests/unit/test_platform.cpp b/tests/unit/test_platform.cpp index 3189f26a0..466ebb905 100644 --- a/tests/unit/test_platform.cpp +++ b/tests/unit/test_platform.cpp @@ -557,4 +557,33 @@ otError otPlatRadioSetCcaEnergyDetectThreshold(otInstance *aInstance, int8_t aTh return OT_ERROR_NONE; } + +#if OPENTHREAD_CONFIG_DNS_DSO_ENABLE + +OT_TOOL_WEAK void otPlatDsoEnableListening(otInstance *aInstance, bool aEnable) +{ + OT_UNUSED_VARIABLE(aInstance); + OT_UNUSED_VARIABLE(aEnable); +} + +OT_TOOL_WEAK void otPlatDsoConnect(otPlatDsoConnection *aConnection, const otSockAddr *aPeerSockAddr) +{ + OT_UNUSED_VARIABLE(aConnection); + OT_UNUSED_VARIABLE(aPeerSockAddr); +} + +OT_TOOL_WEAK void otPlatDsoSend(otPlatDsoConnection *aConnection, otMessage *aMessage) +{ + OT_UNUSED_VARIABLE(aConnection); + OT_UNUSED_VARIABLE(aMessage); +} + +OT_TOOL_WEAK void otPlatDsoDisconnect(otPlatDsoConnection *aConnection, otPlatDsoDisconnectMode aMode) +{ + OT_UNUSED_VARIABLE(aConnection); + OT_UNUSED_VARIABLE(aMode); +} + +#endif // #if OPENTHREAD_CONFIG_DNS_DSO_ENABLE + } // extern "C" diff --git a/tests/unit/test_platform.h b/tests/unit/test_platform.h index 07070b988..2944e5225 100644 --- a/tests/unit/test_platform.h +++ b/tests/unit/test_platform.h @@ -33,6 +33,7 @@ #include #include +#include #include #include #include