Performing a DNS query on an OpenThread SED to get a synthesized IPv6 Address from an IPv4-only CoAP Server

I’ve been setting up an OpenThread network with multiple OT Sleepy End Devices (SEDs) that periodically transmit sensor data to an IPv4-only CoAP server. To enable communication between the OT devices and the CoAP server, I need the synthesized IPv6 address. To test DNS resolution, I first flashed an nRF54L15-DK with the ot-cli sample, configured the network settings (network key, PAN ID, channel), joined the network as an MTD, and then ran the following command:

> ot dns resolve4 <hostname>

This command returned the synthesized IPv6 address of the CoAP server, indicating that the OpenThread Border Router (OTBR)—which is based on a Raspberry Pi 4 and an nRF52840 dongle—is functioning correctly. Therefore, it appears that NAT64/DNS64 is working as expected.

Then, I used the synthesized IPv6 address to send data to the CoAP server using the code below, and I was able to verify that the data successfully arrived at the server:

#include <stdio.h>
#include <zephyr/kernel.h>
#include <zephyr/device.h>
#include <zephyr/logging/log.h>

#include <zephyr/drivers/sensor.h>
#include <zephyr/drivers/sensor/sht4x.h>

#include <zephyr/net/openthread.h>
#include <openthread/coap.h>
#include <openthread/link.h>

LOG_MODULE_REGISTER(main_app, LOG_LEVEL_INF);

static const struct device* sht_dev = DEVICE_DT_GET(DT_NODELABEL(sht4x));

struct sensor_data {
  float relativeHumidity;
  float temperature;
};

char myPayloadJson[256] = "";

void coap_init(void) {
  otInstance* p_instance = openthread_get_default_instance();
  otError error = otCoapStart(p_instance, OT_DEFAULT_COAP_PORT);
  if (error != OT_ERROR_NONE) {
    LOG_ERR("Failed to start CoAP: %d. (line %i)\n", error, __LINE__);
  }
}

static void coap_send_data_response_cb(void* p_context, otMessage* p_message, const otMessageInfo* p_message_info, otError result) {
  if (result == OT_ERROR_NONE) {
    LOG_INF("Delivery confirmed.\n");

  } else {
    LOG_WRN("Delivery not confirmed: %d\n", result);
  }
}

static void coap_send_data_request(void) {
  otError error = OT_ERROR_NONE;
  otMessage* myMessage;
  otMessageInfo myMessageInfo;
  otInstance* myInstance = openthread_get_default_instance();


  otIp6Address serverAddress;
  otIp6AddressFromString("/*CoAP server ipv6 string here */", &serverAddress);

  do { 
    myMessage = otCoapNewMessage(myInstance, NULL);
    if (myMessage == NULL) {
      LOG_ERR("Failed to allocate message for CoAP request\n");
      return;
    }
    otCoapMessageInit(myMessage, OT_COAP_TYPE_CONFIRMABLE, OT_COAP_CODE_PUT);

    error = otCoapMessageAppendUriPathOptions(myMessage, "put-resource");
    if (error != OT_ERROR_NONE) {
      break;
    }

    error = otCoapMessageAppendContentFormatOption(myMessage, OT_COAP_OPTION_CONTENT_FORMAT_JSON);
    if (error != OT_ERROR_NONE) {
      break;
    }

    error = otCoapMessageSetPayloadMarker(myMessage);
    if (error != OT_ERROR_NONE) {
      break;
    }

    error = otMessageAppend(myMessage, myPayloadJson, strlen(myPayloadJson));
    if (error != OT_ERROR_NONE) {
      break;
    }

    memset(&myMessageInfo, 0, sizeof(myMessageInfo));
    memcpy(&myMessageInfo.mPeerAddr, &serverAddress, sizeof(serverAddress));
    myMessageInfo.mPeerPort = OT_DEFAULT_COAP_PORT;

    error = otCoapSendRequest(myInstance, myMessage, &myMessageInfo, coap_send_data_response_cb, NULL);
  } while (false);

  if (error != OT_ERROR_NONE) {
    LOG_ERR("Failed to send CoAP Request: %d. (line %i)\n", error, __LINE__);
    otMessageFree(myMessage);
  } else {
    LOG_INF("CoAP data transmitted. (line %i)\n", __LINE__);
  }
}


int main(void) {
  struct sensor_data data;
  struct sensor_value temp, humidity;
  int ret;

  if (device_init(sht_dev) || !device_is_ready(sht_dev)) {
    LOG_ERR("I2C device \"%s\" not found or not ready (line %i)", sht_dev->name, __LINE__);
    return 0;
  }


  /* Init CoAP */
  coap_init();

  while (1) {
   
    ret = sensor_sample_fetch(sht_dev);
    if (!ret) {
      sensor_channel_get(sht_dev, SENSOR_CHAN_AMBIENT_TEMP, &temp);
      sensor_channel_get(sht_dev, SENSOR_CHAN_HUMIDITY, &humidity);
      LOG_INF("Temp: %d.%06d C, RH: %d.%06d%%", temp.val1, temp.val2, humidity.val1, humidity.val2);
    } else {
      LOG_ERR("Sensor read failed: %d", ret);
    }

    // Create the payload to transmit via OpenThread
    data.temperature = sensor_value_to_float(&temp);
    data.relativeHumidity = sensor_value_to_float(&humidity);

   snprintf(myPayloadJson, sizeof(myPayloadJson),
         "{\"relativeHumidity\":%.1f,\"temperature\":%.1f}",
         (double)data.relativeHumidity, (double)data.temperature);

    if (ret >= 0) {
      LOG_INF("JSON Output: %s\n", myPayloadJson);
    } else {
      LOG_ERR("JSON encoding failed: %d. (line %i)\n", ret, __LINE__);
    }
    coap_send_data_request();

    /* Go Sleep */
    k_sleep(K_SECONDS(10));
  }
}

However, since I have multiple devices, it becomes inconvenient when the CoAP server’s IPv4 address changes. In that case, I have to manually check the new synthesized IPv6 address, hardcode it into the firmware, and re-flash all devices — which is quite tedious. To solve this, I’d like to implement a DNS query directly on the OT SED devices so they can dynamically retrieve the IPv6 address. Since the ot-cli sample is capable of performing this query, it indicates that such functionality should be possible to implement.

I inspected the ncs ot-cli sample repository but couldn’t determine which source files correspond to each CLI command, making it difficult to replicate the implementation. How does the build system for this sample work? Does it use the official Google OpenThread repository? I was able to locate the code executed for the dns resolve4 command there:

I tried implementing the DNS query using my nRF Connect SDK application (using NCS v3.1). The application builds successfully, but when I run it, I don’t receive any response. My implementation is shown below:

main.c

#include <stdint.h>
#include <stdio.h>
#include <zephyr/device.h>
#include <zephyr/kernel.h>
#include <zephyr/logging/log.h>

#include <openthread/thread.h>
#include <openthread/instance.h>
#include <openthread/ip6.h>
#include <openthread/dns.h>
#include <openthread/dns_client.h>
#include <zephyr/net/openthread.h>

#define OPENTHREAD_CONFIG_DNS_CLIENT_ENABLE
#define OPENTHREAD_CONFIG_DNS_CLIENT_DEFAULT_NAT64_ENABLE

#define OPENTHREAD_CONFIG_IP6_ENABLE

LOG_MODULE_REGISTER(main_app, LOG_LEVEL_INF);

void handle_dns_response(otError aResult, const otDnsAddressResponse* aResponse, void* aContext) {
  if (aResult == OT_ERROR_NONE && aResponse) {
    otIp6Address address;
    uint32_t ttl;
    uint16_t index = 0;

    // Iterate over all addresses in the response
    while (otDnsAddressResponseGetAddress(aResponse, index, &address, &ttl) == OT_ERROR_NONE) {
      char buf[OT_IP6_ADDRESS_STRING_SIZE];
      otIp6AddressToString(&address, buf, sizeof(buf));

      LOG_INF("Synthesized IPv6: %s (TTL: %" PRIu32 "). (line %i)", buf, ttl, __LINE__);
      // ...
      index++;
    }
  } else {
    LOG_ERR("DNS resolution failed: %d. (line %i)", aResult, __LINE__);
  }
}

void query_dns_address(otInstance* ot) {
  // otDnsQueryConfig config = {0};
  otError error;

  const otDnsQueryConfig* queryConfig = otDnsClientGetDefaultConfig(ot);

  if (queryConfig != NULL) {
    char buf[OT_IP6_ADDRESS_STRING_SIZE];
    otIp6AddressToString(&queryConfig->mServerSockAddr.mAddress, buf, sizeof(buf));
    LOG_INF("NAT64 Server Addr: %s. (line %i)", buf, __LINE__);
  } else {
    LOG_WRN("No DNS query config available. (line %i)", __LINE__);
  }

  error = otDnsClientResolveIp4Address(ot, "time.google.com", handle_dns_response, NULL, queryConfig);

  switch (error) {
    case OT_ERROR_NONE:
      LOG_INF("Query sent successfully. aCallback will be invoked to report the status. (line %i)", __LINE__);
      break;
    case OT_ERROR_NO_BUFS:
      LOG_INF("Insufficient buffer to prepare and send query. (line %i)", __LINE__);
      break;
    case OT_ERROR_INVALID_ARGS:
      LOG_INF("The host name is not valid format or NAT64 is not enabled in config. (line %i)", __LINE__);
      break;
    case OT_ERROR_INVALID_STATE:
      LOG_INF("Cannot send query since Thread interface is not up. (line %i)", __LINE__);
      break;
  }
}

int main(void) {
  otInstance* instance = openthread_get_default_instance();

  if (instance == NULL) {
    LOG_INF("Failed to get default OpenThread instance");
    return -1;
  }

  while (1) {
    for (int i = 0; i < 5000; i++) {
      LOG_INF("%i", i);

      /* Check OT Role */
      otDeviceRole role = otThreadGetDeviceRole(openthread_get_default_instance());

      switch (role) {
        case OT_DEVICE_ROLE_DISABLED:
          LOG_INF("The Thread stack is disabled.");
          break;
        case OT_DEVICE_ROLE_DETACHED:
          LOG_INF("Not currently participating in a Thread network/partition.");
          break;
        case OT_DEVICE_ROLE_CHILD:
          LOG_INF("The Thread Child role.");
          break;
        case OT_DEVICE_ROLE_ROUTER:
          LOG_INF("The Thread Router role.");
          break;
        case OT_DEVICE_ROLE_LEADER:
          LOG_INF("The Thread Leader role.");
          break;
      }

      query_dns_address(instance);
      k_sleep(K_SECONDS(10));
    }
  }

  return 0;
}


prj.conf:

# STACKS
CONFIG_SYSTEM_WORKQUEUE_STACK_SIZE=2048
CONFIG_MAIN_STACK_SIZE=4096

CONFIG_CBPRINTF_FP_SUPPORT=y
CONFIG_PM_DEVICE=y
CONFIG_PM_DEVICE_RUNTIME=y

# RTT
CONFIG_USE_SEGGER_RTT=y

# LOGS
CONFIG_LOG=y
CONFIG_LOG_BACKEND_RTT=y
CONFIG_LOG_BACKEND_RTT_BUFFER=0
CONFIG_SENSOR_LOG_LEVEL_DBG=y

# OpenThread (Generic)
CONFIG_NETWORKING=y
CONFIG_OPENTHREAD=y
CONFIG_NET_IPV6=y
CONFIG_NET_L2_OPENTHREAD=y
CONFIG_OPENTHREAD_THREAD_VERSION_1_3=y
CONFIG_OPENTHREAD_COAP=y
CONFIG_OPENTHREAD_MANUAL_START=n
CONFIG_OPENTHREAD_DNS_CLIENT=y
CONFIG_DNS_RESOLVER=y
CONFIG_OPENTHREAD_SNTP_CLIENT=y
# OpenThread Sleep-End-Device
CONFIG_OPENTHREAD_MTD=y
#CONFIG_OPENTHREAD_MTD_SED=y 
#CONFIG_OPENTHREAD_POLL_PERIOD=30000

# OpenThread Network identity
CONFIG_OPENTHREAD_NETWORK_NAME="my-ot"
CONFIG_OPENTHREAD_CHANNEL=12
CONFIG_OPENTHREAD_PANID=43537
CONFIG_OPENTHREAD_XPANID="de:e2:27:71:01:3c:35:26"
CONFIG_OPENTHREAD_NETWORKKEY="00:11:22:33:44:55:66:77:88:99:aa:bb:cc:dd:ee:ff"

# Enable UDP since SNTP uses UDP
CONFIG_NET_UDP=y

# Enable IPv6 (Thread uses IPv6)
CONFIG_NET_IPV6=y

# Enable sockets if you plan to use BSD socket APIs
CONFIG_NET_SOCKETS=y

# Increase networking stack size (important for OpenThread apps)
CONFIG_NET_TX_STACK_SIZE=2048
CONFIG_NET_RX_STACK_SIZE=2048

RTT Logs:

[00:01:20.063,012] <inf> main_app: 8
[00:01:20.063,020] <inf> main_app: The Thread Child role.
[00:01:20.063,119] <inf> main_app: NAT64 Server Addr: 2001:4860:4860:0:0:0:0:8888. (line 50)
[00:01:20.063,414] <inf> main_app: Query sent successfully. aCallback will be invoked to report the status. (line 59)
[00:01:28.062,251] <err> main_app: DNS resolution failed: 28. (line 37)
[00:01:30.063,492] <inf> main_app: 9
[00:01:30.063,501] <inf> main_app: The Thread Child role.
[00:01:30.063,599] <inf> main_app: NAT64 Server Addr: 2001:4860:4860:0:0:0:0:8888. (line 50)
[00:01:30.063,894] <inf> main_app: Query sent successfully. aCallback will be invoked to report the status. (line 59)
[00:01:38.064,683] <err> main_app: DNS resolution failed: 28. (line 37)
[00:01:40.063,971] <inf> main_app: 10
[00:01:40.063,980] <inf> main_app: The Thread Child role.
[00:01:40.064,079] <inf> main_app: NAT64 Server Addr: 2001:4860:4860:0:0:0:0:8888. (line 50)
[00:01:40.064,374] <inf> main_app: Query sent successfully. aCallback will be invoked to report the status. (line 59)
[00:01:48.064,107] <err> main_app: DNS resolution failed: 28. (line 37)
[00:01:50.064,452] <inf> main_app: 11
[00:01:50.064,460] <inf> main_app: The Thread Child role.
[00:01:50.064,559] <inf> main_app: NAT64 Server Addr: 2001:4860:4860:0:0:0:0:8888. (line 50)
[00:01:50.064,854] <inf> main_app: Query sent successfully. aCallback will be invoked to report the status. (line 59)
[00:01:58.064,491] <err> main_app: DNS resolution failed: 28. (line 37)
[00:02:00.060,795] <dbg> temp_nrf5_mpsl: temp_nrf5_mpsl_sample_fetch: sample: 119
[00:02:00.060,808] <dbg> temp_nrf5_mpsl: temp_nrf5_mpsl_channel_get: Temperature:29,750000
[00:02:00.064,929] <inf> main_app: 12
[00:02:00.064,934] <inf> main_app: The Thread Child role.
[00:02:00.065,032] <inf> main_app: NAT64 Server Addr: 2001:4860:4860:0:0:0:0:8888. (line 50)
[00:02:00.065,327] <inf> main_app: Query sent successfully. aCallback will be invoked to report the status. (line 59)
[00:02:08.064,907] <err> main_app: DNS resolution failed: 28. (line 37)

I'm getting the hard-coded default Google DNS from the OpenThread library instead of my local DNS64 server address (e.g., fd84:...). I know the local NAT64/DNS64 is working because when I use the ot-cli sample, I get the correct synthesized IPv6 addresses.

Are there any guidelines on what I might be missing and how to generate the synthesized IPv6 address in the firmware?

Parents
  • This is also a procedure required to obtain the time from an SNTP server: using the ot-cli sample, I can perform a DNS query to Google’s NTP server, and then use the resulting synthesized IPv6 address to perform an SNTP query:

    $ ot dns resolve4 time.google.com
    DNS response for time.google.com. - fd84:eb3c:3ceb:2:0:0:d8ef:230c TTL:4123 fd84:eb3c:3ceb:2:0:0:d8ef:2304 TTL:4123 fd84:eb3c:3ceb:2:0:0:d8ef:2300 TTL:4123 fd84:eb3c:3ceb:2:0:0:d8ef:2308 TTL:4123 
    
    $ ot sntp query fd84:eb3c:3ceb:2:0:0:d8ef:230c
    SNTP response - Unix time: 1762369805 (era: 0)
    Done

    I’d like to have both the DNS and SNTP queries working correctly. This would allow me to send data to the CoAP server over OpenThread using its hostname, and also include an up-to-date timestamp in the data payload. The plan is to perform an SNTP query at power-on to synchronize the time, and then use the internal RTC to keep Zephyr’s system time updated.

    I’ve been following the Google OpenThread API documentation from this webpage, which is very well organized. It clearly explains how to implement functions such as otDnsClientResolveIp4Address() and provides detailed information about the related data types.

Reply
  • This is also a procedure required to obtain the time from an SNTP server: using the ot-cli sample, I can perform a DNS query to Google’s NTP server, and then use the resulting synthesized IPv6 address to perform an SNTP query:

    $ ot dns resolve4 time.google.com
    DNS response for time.google.com. - fd84:eb3c:3ceb:2:0:0:d8ef:230c TTL:4123 fd84:eb3c:3ceb:2:0:0:d8ef:2304 TTL:4123 fd84:eb3c:3ceb:2:0:0:d8ef:2300 TTL:4123 fd84:eb3c:3ceb:2:0:0:d8ef:2308 TTL:4123 
    
    $ ot sntp query fd84:eb3c:3ceb:2:0:0:d8ef:230c
    SNTP response - Unix time: 1762369805 (era: 0)
    Done

    I’d like to have both the DNS and SNTP queries working correctly. This would allow me to send data to the CoAP server over OpenThread using its hostname, and also include an up-to-date timestamp in the data payload. The plan is to perform an SNTP query at power-on to synchronize the time, and then use the internal RTC to keep Zephyr’s system time updated.

    I’ve been following the Google OpenThread API documentation from this webpage, which is very well organized. It clearly explains how to implement functions such as otDnsClientResolveIp4Address() and provides detailed information about the related data types.

Children
No Data
Related