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?

  • Regarding the SNTP query, I added the Kconfig option CONFIG_OPENTHREAD_NORDIC_LIBRARY_MASTER=y, and the code I presented above started to work:

    [00:00:20.062,381] <inf> main_app: The Thread Child role.
    [00:00:20.062,416] <inf> main_app: Sending SNTP query to fd84:eb3c:3ceb:2:0:0:d8ef:2308
    [00:00:20.062,890] <inf> main_app: SNTP query sent
    [00:00:23.081,992] <inf> main_app: SNTP response - Unix time: 176279874

    However, this configuration enables many features that I don’t use. Could you advise which specific configurations I should enable instead of including all these libraries? Also, what is the Nordic recommendation for determining which Kconfig options are needed for a given feature? It currently feels like almost trial and error to figure out which Kconfig variables are required.

    And equally important, I’m still waiting for clarification on how the ot-cli sample in nRF Connect SDK is built. The source files from the OpenThread repositories aren’t included in main.c, and I can’t see any configuration files that add these files to the build process.

  • Hi again,

    gc0rreiab said:

    1) I finally got the DNS query working! It turns out the main issue was the missing Kconfig option CONFIG_OPENTHREAD_SRP_CLIENT=y.

    gc0rreiab said:

    However, in my code I’m not able to get an SNTP query response. I’ve added the following Kconfig option: CONFIG_OPENTHREAD_SNTP_CLIENT=y, but it seems that nRF Connect for VS Code doesn’t even detect the #include <openthread/sntp.h> header.

    Below is my SNTP code:

    Glad you've gotten further! I believe that SNTP Client is part of Master library, but is not included in MTD/FTD optimized libraries. So I think you would think you would have to build from sources to enable that with, using CONFIG_OPENTHREAD_SOURCES=y.

    Though CONFIG_OPENTHREAD_NORDIC_LIBRARY_MASTER would work too.

    gc0rreiab said:
    However, this configuration enables many features that I don’t use.

    True. Building from sources might be better then. But please note that only the prebuilt libraries are certified. If you build the libraries from sources, you might not be able to use certification by inheritance. Additionally, using a different version of the OpenThread stack than what is used in the precompiled and certified library will also prevent you from using certification by inheritance.

    gc0rreiab said:
    Also, what is the Nordic recommendation for determining which Kconfig options are needed for a given feature? It currently feels like almost trial and error to figure out which Kconfig variables are required.

    Understanable. It can be a bit messy at the moment. 

    The go-to answer is that you can check what KConfig symbols are dependant on eachother using the VSC Kconfig GUI, the reference here, or check what you need for a given feature with just the docs in general. Or find samples that do things similar to what you want and see what they have enabled. Though I am glad that you still have the option to reach out to us here, as some of these options aren't clear cut enough for any of these options to cover it perfectly.

    gc0rreiab said:
    And equally important, I’m still waiting for clarification on how the ot-cli sample in nRF Connect SDK is built. The source files from the OpenThread repositories aren’t included in main.c, and I can’t see any configuration files that add these files to the build process.

    I must have missed that question. It does this by linking pre-built libraries. See for instance prj.conf file, I wonder if the only thing needed is essentially CONFIG_OPENTHREAD_SHELL.

    gc0rreiab said:
     just checking in — any updates?

    I've forwarded a question to the relevant R&D about this, but I have yet to hear from them. Though as we have this working now I guess that is fine.

    Regards,

    Elfving

  • Glad you've gotten further! I believe that SNTP Client is part of Master library, but is not included in MTD/FTD optimized libraries. So I think you would think you would have to build from sources to enable that with, using CONFIG_OPENTHREAD_SOURCES=y.

    Though CONFIG_OPENTHREAD_NORDIC_LIBRARY_MASTER would work too.

    In this documentation, it looks like CONFIG_OPENTHREAD_SOURCES is enabled by default, so CONFIG_OPENTHREAD_NORDIC_LIBRARY_MASTER is likely what ensures the SNTP query works properly. 

  • Hmm I guess you are right. Question is if it happen to be enabled on your application but yeah, I guess it likely is. We can make sure by first removing any explicit change to CONFIG_OPENTHREAD_NORDIC_LIBRARY_MASTER, and then checking whether or not it is set in the resulting .config file. You can find this in /build/[app]/zephyr/.

    Given that it is set there, I would say that tells us that there is another setting that CONFIG_OPENTHREAD_NORDIC_LIBRARY_MASTER is enabling, that is making this work.

    Regards,

    Elfving

  • The question is which settings are enabled by CONFIG_OPENTHREAD_NORDIC_LIBRARY_MASTER to ensure that SNTP queries work properly. Ideally, the following configurations should be sufficient for both DNS and SNTP queries to function correctly. I would prefer not to enable additional features that might alter OpenThread’s behavior without my explicit consent:

    CONFIG_NETWORKING=y
    CONFIG_NET_IPV6=y
    CONFIG_NET_L2_OPENTHREAD=y
    CONFIG_OPENTHREAD_THREAD_VERSION_1_3=y
    CONFIG_OPENTHREAD=y
    CONFIG_OPENTHREAD_MTD=y
    CONFIG_OPENTHREAD_MTD_SED=y
    CONFIG_OPENTHREAD_MANUAL_START=n
    CONFIG_OPENTHREAD_DNS_CLIENT=y
    CONFIG_DNS_RESOLVER=y
    CONFIG_OPENTHREAD_SNTP_CLIENT=y

Related