From 5b9a1c046738618cad5d39c194ebbbd8160eba7a Mon Sep 17 00:00:00 2001 From: Abtin Keshavarzian Date: Fri, 14 Jan 2022 16:25:16 -0800 Subject: [PATCH] [net] add support for DNS Stateful Operations (DSO) (#7081) This commit adds support for DNS Stateful Operations (DSO) as specified in RFC 8490. It adds `platform/dso_transport.hpp` header file which defines the platform APIs/callbacks for DSO transport layer (e.g., DSN-over-TLS or DNS-over-TCP). The `Dso` module handles establishing connection with a peer, acting either as a DSO client or server, establishing a DSO session over a connection, and then sending and processing DSO request, response, and unidirectional messages (including support for DSO TLV formats). The `Dso` module also manages the session life cycle and timeouts, namely the "Inactivity" and "Keep Alive" timeouts (including sending and processing of Keep Alive messages when needed). It also handles adding encryption padding before sending a message. It implements the padding policy "Random-Block-Length Padding" from RFC 8467. This commit also adds a detailed unit test `test_dso` covering the behavior (including corner cases) of the `Dso` implementation. The unit test provides an implementation of the DSO platform APIs which emulate the DSO transport layer. It also includes a simplified alarm platform implementation (emulating timers and allowing time to advance in the unit test). These allow the unit test to cover more complicated situations and behaviors (timeouts, failures, etc). --- Android.mk | 1 + etc/cmake/options.cmake | 5 + examples/Makefile-simulation | 1 + examples/README.md | 1 + examples/common-switches.mk | 5 + examples/platforms/simulation/CMakeLists.txt | 1 + examples/platforms/simulation/Makefile.am | 1 + examples/platforms/simulation/dso_transport.c | 59 + include/Makefile.am | 1 + include/openthread/BUILD.gn | 1 + include/openthread/instance.h | 2 +- include/openthread/platform/dso_transport.h | 202 +++ script/check-scan-build | 1 + script/check-simulation-build-autotools | 1 + script/make-pretty | 1 + script/test | 1 + src/core/BUILD.gn | 3 + src/core/CMakeLists.txt | 1 + src/core/Makefile.am | 3 + src/core/common/instance.hpp | 7 + src/core/common/time.hpp | 8 +- src/core/config/dns_dso.h | 78 + src/core/net/dns_dso.cpp | 1535 +++++++++++++++++ src/core/net/dns_dso.hpp | 973 +++++++++++ src/core/net/dns_types.cpp | 1 + src/core/net/dns_types.hpp | 5 +- src/core/openthread-core-config.h | 1 + src/core/thread/thread_netif.cpp | 3 + src/core/thread/thread_netif.hpp | 4 + ...openthread-core-toranj-config-simulation.h | 8 + tests/unit/CMakeLists.txt | 22 + tests/unit/Makefile.am | 4 + tests/unit/test_dso.cpp | 1240 +++++++++++++ tests/unit/test_platform.cpp | 29 + tests/unit/test_platform.h | 1 + 35 files changed, 4206 insertions(+), 4 deletions(-) create mode 100644 examples/platforms/simulation/dso_transport.c create mode 100644 include/openthread/platform/dso_transport.h create mode 100644 src/core/config/dns_dso.h create mode 100644 src/core/net/dns_dso.cpp create mode 100644 src/core/net/dns_dso.hpp create mode 100644 tests/unit/test_dso.cpp 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