I am using OpenThread Secure CoAP with DTLS/PSK on an nRF54L15 DK.
Environment:
- nRF Connect SDK 3.4.0
- Zephyr 4.4.0
- nRF54L15 DK
- OpenThread Secure CoAP API (
otCoapSecure*) - DTLS PSK authentication
- Also tested with
CONFIG_OPENTHREAD_NORDIC_LIBRARY_MASTER=y - Also reproduced with NCS 3.2.4
- Tested as MTD/SED and FTD
The DTLS connection itself works correctly, but establishing the connection consistently takes about 16.2 seconds.
After the DTLS connection has been established, the actual CoAP request/response works normally.
A packet capture on the OpenThread Border Router shows that the server answers the DTLS packets within only a few milliseconds. However, the client waits approximately 8 seconds twice during the handshake.
Example timing:
Client -> Server
Server -> Client ~27 ms later
~8 second delay
Client -> Server
Server -> Client
...
~8 second delay
Client -> Server
Handshake then continues normally
I inspected OpenThread's SecureTransport implementation and found:
mbedtls_ssl_conf_handshake_timeout(&mConf, 8000, 60000);
As a diagnostic test only, I changed the initial timeout from 8000 ms to 1000 ms:
mbedtls_ssl_conf_handshake_timeout(&mConf, 1000, 60000);
With this change, the DTLS connection time dropped reproducibly from approximately 16.2 seconds to approximately 2.2 seconds.
This seems to indicate that the handshake is waiting twice for the configured initial DTLS retransmission timer, even though the corresponding packets from the peer are received promptly.
The same behavior occurs:
- with NCS 3.4.0
- with NCS 3.2.4
- with MTD/SED
- with FTD
- with the Nordic OpenThread master library
The server is a libcoap/mbedTLS CoAPS server using PSK authentication. Linux libcoap clients using mbedTLS, GnuTLS, and OpenSSL connect without this delay.
Could you please clarify:
- Is this a known issue in OpenThread Secure CoAP /
SecureTransport? - Is there a supported way to configure the DTLS initial retransmission timeout instead of modifying
secure_transport.cpp? - Could this be related to how
SecureTransport::Process()handles received packets orMBEDTLS_ERR_SSL_HELLO_VERIFY_REQUIRED? - Is there a recommended configuration for short-lived DTLS/CoAPS connections on battery-powered Thread devices
Here my source code:
#include <zephyr/kernel.h>
#include <stdbool.h>
#include <stdint.h>
#include <stdio.h>
#include <string.h>
#include <zephyr/net/openthread.h>
#include <zephyr/logging/log.h>
#include <openthread/coap.h>
#include <openthread/coap_secure.h>
#include <openthread/ip6.h>
#include <openthread/message.h>
#include <openthread/netdata.h>
#include <openthread/thread.h>
LOG_MODULE_REGISTER(coaps_psk_client, LOG_LEVEL_INF);
#define COAP_PAYLOAD_SIZE 180
/*
* Replace these values with the actual CoAPS server configuration.
*
* 192.0.2.1 belongs to TEST-NET-1 and is used here as a
* documentation placeholder.
*/
#define COAPS_SERVER_IPV4_ADDRESS "192.0.2.1"
#define COAPS_SERVER_PORT 5684
#define COAPS_URI_PATH "telemetry"
#define COAPS_PSK_IDENTITY "test-device"
#define COAPS_PSK "0123456789abcdef0123456789abcdef"
#define COAP_TABLE "test_table"
#define SEND_INTERVAL_SECONDS 10
#define THREAD_WAIT_SECONDS 2
#define CONNECT_WAIT_SECONDS 30
#define RESPONSE_WAIT_SECONDS 10
typedef enum {
STATE_WAIT_THREAD = 0,
STATE_START_CONNECT,
STATE_WAIT_CONNECT,
STATE_SEND_REQUEST,
STATE_WAIT_RESPONSE,
STATE_DISCONNECT,
STATE_SLEEP
} app_state_t;
static bool coaps_initialized;
static uint8_t server_ipv4[4];
static volatile app_state_t app_state = STATE_WAIT_THREAD;
static uint8_t connect_wait_counter;
static uint8_t response_wait_counter;
static int telemetry_counter;
static int64_t connect_start_ms;
static int64_t request_start_ms;
/* ---------------- Thread helper functions ---------------- */
static const char *state_to_string(app_state_t state)
{
switch (state) {
case STATE_WAIT_THREAD: return "WAIT_THREAD";
case STATE_START_CONNECT: return "START_CONNECT";
case STATE_WAIT_CONNECT: return "WAIT_CONNECT";
case STATE_SEND_REQUEST: return "SEND_REQUEST";
case STATE_WAIT_RESPONSE: return "WAIT_RESPONSE";
case STATE_DISCONNECT: return "DISCONNECT";
case STATE_SLEEP: return "SLEEP";
default: return "UNKNOWN";
}
}
static const char *role_to_string(otDeviceRole role)
{
switch (role) {
case OT_DEVICE_ROLE_DISABLED: return "disabled";
case OT_DEVICE_ROLE_DETACHED: return "detached";
case OT_DEVICE_ROLE_CHILD: return "child";
case OT_DEVICE_ROLE_ROUTER: return "router";
case OT_DEVICE_ROLE_LEADER: return "leader";
default: return "unknown";
}
}
static bool role_is_ready(otDeviceRole role)
{
return role == OT_DEVICE_ROLE_CHILD ||
role == OT_DEVICE_ROLE_ROUTER ||
role == OT_DEVICE_ROLE_LEADER;
}
static otDeviceRole get_thread_role(otInstance *instance)
{
otDeviceRole role;
openthread_mutex_lock();
role = otThreadGetDeviceRole(instance);
openthread_mutex_unlock();
return role;
}
/* ---------------- IPv4 / NAT64 ---------------- */
static bool parse_ipv4_address(const char *address, uint8_t ipv4[4])
{
unsigned int octet[4];
char trailing;
if (sscanf(address, "%u.%u.%u.%u%c",
&octet[0], &octet[1], &octet[2], &octet[3], &trailing) != 4) {
return false;
}
for (size_t i = 0; i < 4; i++) {
if (octet[i] > 255) {
return false;
}
ipv4[i] = (uint8_t)octet[i];
}
return true;
}
static bool initialize_server_ipv4(void)
{
if (!parse_ipv4_address(COAPS_SERVER_IPV4_ADDRESS, server_ipv4)) {
LOG_ERR("Invalid CoAPS server IPv4 address: %s",
COAPS_SERVER_IPV4_ADDRESS);
return false;
}
return true;
}
static bool find_nat64_prefix(otInstance *instance, otIp6Prefix *prefix)
{
otNetworkDataIterator iterator = OT_NETWORK_DATA_ITERATOR_INIT;
otExternalRouteConfig route;
while (otNetDataGetNextRoute(instance, &iterator, &route) == OT_ERROR_NONE) {
if (route.mNat64 && route.mPrefix.mLength == 96) {
*prefix = route.mPrefix;
return true;
}
}
return false;
}
static void synthesize_nat64_address(const otIp6Prefix *prefix,
const uint8_t ipv4[4],
otIp6Address *address)
{
memset(address, 0, sizeof(*address));
memcpy(&address->mFields.m8[0],
&prefix->mPrefix.mFields.m8[0],
12);
memcpy(&address->mFields.m8[12],
ipv4,
4);
}
/* ---------------- DTLS / CoAPS ---------------- */
static void coaps_connect_cb(otCoapSecureConnectEvent event, void *context)
{
ARG_UNUSED(context);
switch (event) {
case OT_COAP_SECURE_CONNECTED:
LOG_INF("CoAPS connected after %lld ms",
(long long)(k_uptime_get() - connect_start_ms));
app_state = STATE_SEND_REQUEST;
break;
case OT_COAP_SECURE_DISCONNECTED_ERROR:
LOG_ERR("CoAPS disconnected: error");
app_state = STATE_SLEEP;
break;
case OT_COAP_SECURE_DISCONNECTED_LOCAL_CLOSED:
LOG_INF("CoAPS disconnected: local closed");
break;
case OT_COAP_SECURE_DISCONNECTED_MAX_ATTEMPTS:
LOG_WRN("CoAPS disconnected: max attempts");
app_state = STATE_SLEEP;
break;
case OT_COAP_SECURE_DISCONNECTED_PEER_CLOSED:
LOG_WRN("CoAPS disconnected: peer closed");
app_state = STATE_SLEEP;
break;
default:
LOG_WRN("Unknown CoAPS connection event: %d", event);
app_state = STATE_SLEEP;
break;
}
}
/*
* Initialize OpenThread Secure CoAP with DTLS-PSK.
*/
static bool coaps_client_init(otInstance *instance)
{
otError error;
openthread_mutex_lock();
otCoapSecureSetPsk(
instance,
(const uint8_t *)COAPS_PSK,
(uint16_t)strlen(COAPS_PSK),
(const uint8_t *)COAPS_PSK_IDENTITY,
(uint16_t)strlen(COAPS_PSK_IDENTITY));
otCoapSecureSetSslAuthMode(instance, false);
error = otCoapSecureStart(instance, 0);
openthread_mutex_unlock();
if (error != OT_ERROR_NONE && error != OT_ERROR_ALREADY) {
LOG_ERR("otCoapSecureStart failed: %d", error);
return false;
}
LOG_INF("OpenThread CoAPS initialized with PSK");
return true;
}
/*
* Obtain the NAT64 destination address and start the DTLS handshake.
*/
static bool coaps_connect_start(otInstance *instance)
{
otIp6Prefix nat64_prefix;
otSockAddr peer_sock_addr;
otError error;
memset(&peer_sock_addr, 0, sizeof(peer_sock_addr));
peer_sock_addr.mPort = COAPS_SERVER_PORT;
openthread_mutex_lock();
if (!find_nat64_prefix(instance, &nat64_prefix)) {
openthread_mutex_unlock();
LOG_WRN("No NAT64 /96 prefix available");
return false;
}
synthesize_nat64_address(
&nat64_prefix,
server_ipv4,
&peer_sock_addr.mAddress);
connect_start_ms = k_uptime_get();
error = otCoapSecureConnect(
instance,
&peer_sock_addr,
coaps_connect_cb,
NULL);
openthread_mutex_unlock();
if (error != OT_ERROR_NONE) {
LOG_ERR("otCoapSecureConnect failed: %d", error);
return false;
}
return true;
}
/* ---------------- CoAPS request ---------------- */
static void coaps_response_cb(void *context,
otMessage *message,
const otMessageInfo *message_info,
otError result)
{
ARG_UNUSED(context);
ARG_UNUSED(message_info);
int64_t elapsed_ms = k_uptime_get() - request_start_ms;
if (result == OT_ERROR_RESPONSE_TIMEOUT) {
LOG_WRN("CoAPS response timeout after %lld ms",
(long long)elapsed_ms);
app_state = STATE_DISCONNECT;
return;
}
if (result != OT_ERROR_NONE) {
LOG_ERR("CoAPS response error %d after %lld ms",
result,
(long long)elapsed_ms);
app_state = STATE_DISCONNECT;
return;
}
if (message == NULL) {
LOG_ERR("CoAPS response message is NULL");
app_state = STATE_DISCONNECT;
return;
}
otCoapCode code = otCoapMessageGetCode(message);
LOG_INF("CoAPS response: code=%u.%02u, message_id=%u, time=%lld ms",
(unsigned int)(code >> 5),
(unsigned int)(code & 0x1f),
(unsigned int)otCoapMessageGetMessageId(message),
(long long)elapsed_ms);
if (code == OT_COAP_CODE_CREATED) {
LOG_INF("CoAPS request successful");
telemetry_counter++;
}
app_state = STATE_DISCONNECT;
}
static bool send_secure_request(otInstance *instance)
{
char payload[COAP_PAYLOAD_SIZE];
otMessage *message;
otError error;
snprintf(payload,
sizeof(payload),
"{\"table\":\"%s\","
"\"device_id\":\"test-device\","
"\"temperature\":%d,"
"\"humidity\":%d}",
COAP_TABLE,
25 + (telemetry_counter % 10),
60 + (telemetry_counter % 5));
openthread_mutex_lock();
message = otCoapNewMessage(instance, NULL);
if (message == NULL) {
openthread_mutex_unlock();
LOG_ERR("Failed to allocate CoAPS message");
return false;
}
otCoapMessageInit(
message,
OT_COAP_TYPE_CONFIRMABLE,
OT_COAP_CODE_POST);
otCoapMessageGenerateToken(
message,
OT_COAP_DEFAULT_TOKEN_LENGTH);
error = otCoapMessageAppendUriPathOptions(
message,
COAPS_URI_PATH);
if (error != OT_ERROR_NONE) {
LOG_ERR("Failed to append CoAPS URI path: %d", error);
goto fail;
}
error = otCoapMessageAppendContentFormatOption(
message,
OT_COAP_OPTION_CONTENT_FORMAT_JSON);
if (error != OT_ERROR_NONE) {
LOG_ERR("Failed to append content format: %d", error);
goto fail;
}
error = otCoapMessageSetPayloadMarker(message);
if (error != OT_ERROR_NONE) {
LOG_ERR("Failed to set payload marker: %d", error);
goto fail;
}
error = otMessageAppend(
message,
payload,
strlen(payload));
if (error != OT_ERROR_NONE) {
LOG_ERR("Failed to append payload: %d", error);
goto fail;
}
request_start_ms = k_uptime_get();
error = otCoapSecureSendRequest(
instance,
message,
coaps_response_cb,
NULL);
if (error != OT_ERROR_NONE) {
LOG_ERR("otCoapSecureSendRequest failed: %d", error);
goto fail;
}
openthread_mutex_unlock();
LOG_INF("CoAPS POST request sent");
return true;
fail:
otMessageFree(message);
openthread_mutex_unlock();
return false;
}
static void coaps_disconnect(otInstance *instance)
{
openthread_mutex_lock();
otCoapSecureDisconnect(instance);
openthread_mutex_unlock();
}
/* ---------------- Main ---------------- */
int main(void)
{
otInstance *instance;
LOG_INF("OpenThread NAT64 CoAPS PSK test client started");
instance = openthread_get_default_instance();
if (instance == NULL) {
LOG_ERR("No OpenThread instance");
return 0;
}
if (!initialize_server_ipv4()) {
return 0;
}
while (1) {
LOG_DBG("State: %s", state_to_string(app_state));
switch (app_state) {
case STATE_WAIT_THREAD: {
otDeviceRole role = get_thread_role(instance);
if (!role_is_ready(role)) {
LOG_INF("Thread role: %s - not ready",
role_to_string(role));
k_sleep(K_SECONDS(THREAD_WAIT_SECONDS));
break;
}
LOG_INF("Thread role: %s - ready",
role_to_string(role));
if (!coaps_initialized) {
if (!coaps_client_init(instance)) {
k_sleep(K_SECONDS(THREAD_WAIT_SECONDS));
break;
}
coaps_initialized = true;
}
app_state = STATE_START_CONNECT;
break;
}
case STATE_START_CONNECT:
connect_wait_counter = 0;
/*
* Set WAIT_CONNECT before starting the asynchronous
* connection because the callback may be invoked
* immediately.
*/
app_state = STATE_WAIT_CONNECT;
if (!coaps_connect_start(instance)) {
app_state = STATE_SLEEP;
}
break;
case STATE_WAIT_CONNECT:
if (connect_wait_counter < CONNECT_WAIT_SECONDS) {
connect_wait_counter++;
k_sleep(K_SECONDS(1));
} else {
LOG_WRN("CoAPS connect timeout after %d seconds",
CONNECT_WAIT_SECONDS);
app_state = STATE_DISCONNECT;
}
break;
case STATE_SEND_REQUEST:
response_wait_counter = 0;
app_state = STATE_WAIT_RESPONSE;
if (!send_secure_request(instance)) {
app_state = STATE_DISCONNECT;
}
break;
case STATE_WAIT_RESPONSE:
if (response_wait_counter < RESPONSE_WAIT_SECONDS) {
response_wait_counter++;
k_sleep(K_SECONDS(1));
} else {
LOG_WRN("CoAPS response timeout after %d seconds",
RESPONSE_WAIT_SECONDS);
app_state = STATE_DISCONNECT;
}
break;
case STATE_DISCONNECT:
coaps_disconnect(instance);
app_state = STATE_SLEEP;
break;
case STATE_SLEEP:
LOG_INF("Sleeping for %d seconds",
SEND_INTERVAL_SECONDS);
k_sleep(K_SECONDS(SEND_INTERVAL_SECONDS));
app_state = STATE_WAIT_THREAD;
break;
default:
LOG_ERR("Unknown application state");
app_state = STATE_DISCONNECT;
break;
}
}
return 0;
}