Chapter 82: DNS Client Implementation

Chapter Objectives

By the end of this chapter, you will be able to:

  • Understand the fundamental purpose and operation of the Domain Name System (DNS).
  • Identify common DNS record types (A, AAAA, CNAME, etc.).
  • Implement synchronous DNS lookups on ESP32 using the getaddrinfo function.
  • Configure primary, secondary, and fallback DNS servers for an ESP32 network interface.
  • Understand how to handle DNS resolution errors effectively.
  • Appreciate the implications of blocking DNS calls and how to manage them in an RTOS environment.

Introduction

In our digital world, we interact with countless online services using human-readable domain names like www.espressif.com or www.google.com. However, network communication fundamentally relies on numerical IP addresses. The Domain Name System (DNS) serves as the internet’s directory, translating these memorable domain names into the IP addresses that computers use to identify each other on a network.

For an ESP32 device to connect to a server or service on the internet using its hostname, it must first perform a DNS lookup to resolve that name to an IP address. This process is handled by a DNS client running on the ESP32. This chapter delves into the theory behind DNS and provides practical guidance on implementing DNS client functionality within your ESP-IDF projects, enabling your ESP32 applications to seamlessly connect to the wider internet. Understanding DNS is crucial for virtually any IoT application that needs to communicate with remote servers.

Theory

What is DNS?

The Domain Name System (DNS) is a hierarchical and decentralized naming system for computers, services, or other resources connected to the Internet or a private network. It associates various information with domain names assigned to each of the participating entities. Most prominently, it translates more readily memorized domain names to the numerical IP addresses needed for locating and identifying computer services and devices with the underlying network protocols.

Think of DNS as the phonebook of the Internet. When you want to call someone (access a website), you look up their name (domain name) in the phonebook (DNS) to get their phone number (IP address).

Hierarchical Structure of DNS

DNS has a tree-like structure, or hierarchy:

%%{init: {"theme": "base", "themeVariables": {
  "primaryColor": "#DBEAFE", "primaryTextColor": "#1E40AF", "primaryBorderColor": "#2563EB",
  "lineColor": "#6B7280", "textColor": "#1F2937",
  "fontSize": "14px", "fontFamily": "Open Sans"
}}}%%
graph TD
    Root["<b>.</b><br>(Root DNS Servers)"]

    subgraph TLDs ["Top-Level Domains (TLDs)"]
        COM[".com"]
        ORG[".org"]
        NET[".net"]
        UK[".uk (ccTLD)"]
        EDU[".edu"]
    end

    subgraph SLDs_COM ["Second-Level Domains (under .com)"]
        direction LR
        EXAMPLE_COM["example.com"]
        GOOGLE_COM["google.com"]
    end
    
    subgraph SLDs_ORG ["Second-Level Domains (under .org)"]
        direction LR
        WIKIPEDIA_ORG["wikipedia.org"]
    end

    subgraph Subdomains_EXAMPLE_COM ["Subdomains (under example.com)"]
        direction LR
        WWW_EXAMPLE_COM["www[.]example[.]com"]
        API_EXAMPLE_COM["api.example.com"]
        FTP_EXAMPLE_COM["ftp.example.com"]
    end
    
    subgraph Subdomains_GOOGLE_COM ["Subdomains (under google.com)"]
        direction LR
        WWW_GOOGLE_COM["www[.]google[.]com"]
        MAIL_GOOGLE_COM["mail.google.com"]
    end

    Root --> COM;
    Root --> ORG;
    Root --> NET;
    Root --> UK;
    Root --> EDU;

    COM --> EXAMPLE_COM;
    COM --> GOOGLE_COM;
    ORG --> WIKIPEDIA_ORG;
    
    EXAMPLE_COM --> WWW_EXAMPLE_COM;
    EXAMPLE_COM --> API_EXAMPLE_COM;
    EXAMPLE_COM --> FTP_EXAMPLE_COM;

    GOOGLE_COM --> WWW_GOOGLE_COM;
    GOOGLE_COM --> MAIL_GOOGLE_COM;

    classDef rootNode fill:#EDE9FE,stroke:#5B21B6,stroke-width:2px,color:#5B21B6,font-weight:bold
    classDef tldNode fill:#DBEAFE,stroke:#2563EB,stroke-width:1px,color:#1E40AF
    classDef sldNode fill:#FEF3C7,stroke:#D97706,stroke-width:1px,color:#92400E
    classDef subdomainNode fill:#D1FAE5,stroke:#059669,stroke-width:1px,color:#065F46
    
    class Root rootNode;
    class COM,ORG,NET,UK,EDU tldNode;
    class EXAMPLE_COM,GOOGLE_COM,WIKIPEDIA_ORG sldNode;
    class WWW_EXAMPLE_COM,API_EXAMPLE_COM,FTP_EXAMPLE_COM,WWW_GOOGLE_COM,MAIL_GOOGLE_COM subdomainNode;
  1. Root Level: At the top are the root DNS servers, which know where to find the TLD servers. There are 13 logical root name server clusters worldwide.
  2. Top-Level Domains (TLDs): These are the extensions like .com, .org, .net, .gov, .edu, and country-code TLDs (ccTLDs) like .uk or .jp. TLD servers know where to find the authoritative name servers for domains under them.
  3. Second-Level Domains (SLDs): This is the part of the domain name that you typically register, like espressif in espressif.com.
  4. Subdomains: Further subdivisions, like docs.espressif.com.

DNS Resolution Process

When your ESP32 (as a DNS client) needs to resolve a hostname:

  1. Client Query: The ESP32’s DNS client sends a query to a DNS resolver (also known as a recursive DNS server). This resolver’s IP address is typically obtained automatically via DHCP when the ESP32 connects to a Wi-Fi network, or it can be manually configured.
  2. Resolver Action:
    • Cache Check: The resolver first checks its local cache. If it has recently resolved the same hostname, it returns the cached IP address immediately.
    • Recursive Query (Typical): If the name is not in its cache, the resolver initiates a series of queries:
      • It queries a root DNS server.
      • The root server responds with the address of the TLD server for the requested TLD (e.g., the .com TLD server).
      • The resolver then queries the TLD server.
      • The TLD server responds with the address of the authoritative name server for the specific domain (e.g., espressif.com). An authoritative name server holds the actual DNS records for that domain.
      • Finally, the resolver queries the authoritative name server.
      • The authoritative name server returns the IP address (or other requested record) for the hostname.
    • Iterative Query: In an iterative query, the client (or a resolver acting on its behalf) directly contacts each server in the chain itself. Recursive resolvers do this iterative process for the client.
  3. Response to Client: The resolver caches the result (with a Time-To-Live or TTL value indicating how long to cache it) and returns the IP address to the ESP32.
  4. Connection: The ESP32 can now use this IP address to establish a connection with the target server.
%%{init: {"theme": "base", "themeVariables": {
  "primaryColor": "#DBEAFE", "primaryTextColor": "#1E40AF", "primaryBorderColor": "#2563EB",
  "lineColor": "#6B7280", "textColor": "#1F2937",
  "fontSize": "14px", "fontFamily": "Open Sans"
}}}%%
sequenceDiagram
    participant ESP32 as ESP32 Client<br/>(e.g., Your App)
    participant Resolver as Local DNS Resolver<br/>(e.g., Router or 8.8.8.8)
    participant RootNS as Root DNS Server
    participant TLDNS as TLD DNS Server<br/>(e.g., .com server)
    participant AuthNS as Authoritative NS<br/>(e.g., for example.com)

    ESP32->>+Resolver: 1. Query: "www.example.com?"
    Resolver->>Resolver: 2. Check Cache
    alt Cache Hit
        Resolver-->>ESP32: 3. IP Address (from cache)
    else Cache Miss
        Resolver->>+RootNS: 3. Query: "www.example.com?" (asks for .com NS)
        RootNS-->>-Resolver: 4. Reply: TLDNS Server IP for .com
        Resolver->>+TLDNS: 5. Query: "www.example.com?" (asks for example.com NS)
        TLDNS-->>-Resolver: 6. Reply: AuthNS Server IP for example.com
        Resolver->>+AuthNS: 7. Query: "www.example.com?" (asks for IP)
        AuthNS-->>-Resolver: 8. Reply: IP Address for www.example.com
        Resolver->>Resolver: 9. Cache Result (IP + TTL)
        Resolver-->>ESP32: 10. IP Address for www.example.com
    end
    deactivate Resolver
    ESP32->>AuthNS: 11. (Uses IP to connect to www.example.com server)

    Note right of ESP32: Initiates DNS Lookup
    Note right of Resolver: Performs recursive lookup if not cached
    Note left of AuthNS: Holds the actual DNS record

Common DNS Record Types

DNS doesn’t just store IP addresses. It can store various types of information in DNS records:

Record Type Full Name Description Example Value / Use
A Address Record Maps a hostname to an IPv4 address. www.example.com -> 192.0.2.1
AAAA IPv6 Address Record Maps a hostname to an IPv6 address. (Pronounced “quad-A”) www.example.com -> 2001:db8::1
CNAME Canonical Name Record Creates an alias from one domain name to another (the canonical name). The DNS client then re-queries for the canonical name. ftp.example.com -> server1.example.com
MX Mail Exchange Record Specifies mail servers responsible for accepting email messages for a domain. Includes a priority value. example.com -> Priority 10, Server mail.example.com
NS Name Server Record Delegates a DNS zone to use the given authoritative name servers for that zone. example.com -> ns1.auth-server.net
TXT Text Record Allows arbitrary text to be associated with a domain. Used for verification (SPF, DKIM, domain ownership). _domainkey.example.com -> "v=DKIM1; k=rsa; p=MIGf..."
SRV Service Record Specifies hostname, port, priority, and weight for specific services (e.g., SIP, XMPP), not just a generic IP. _sip._tcp.example.com -> Priority 0, Weight 5, Port 5060, Target sipserver.example.com

DNS Transport Protocol

DNS queries primarily use UDP (User Datagram Protocol) on port 53 because it’s fast and lightweight. UDP is connectionless, so there’s less overhead than TCP. However, if the DNS response data is too large to fit in a single UDP packet (typically over 512 bytes, though EDNS extensions allow larger), the query may be retried using TCP on port 53. TCP is also used for DNS zone transfers between servers.

DNS Client on ESP32 (LwIP)

The ESP-IDF utilizes the DNS client functionality provided by the LwIP (Lightweight IP) TCP/IP stack. When your ESP32 connects to a network (e.g., via Wi-Fi station mode) and obtains an IP address through DHCP, the DHCP server usually also provides the IP addresses of one or more DNS servers. LwIP’s DNS client will then use these servers for resolution.

ESP-IDF provides APIs through esp_netif to configure and manage DNS server settings for each network interface. For performing actual DNS lookups, the standard C library function getaddrinfo() is the recommended and most portable method.

Practical Examples

This section demonstrates how to perform DNS lookups and configure DNS settings on an ESP32 using ESP-IDF v5.x.

Prerequisites

  • ESP-IDF v5.x installed and configured with VS Code.
  • An ESP32 development board.
  • A Wi-Fi network with internet access that your ESP32 can connect to. The router for this network should provide DNS server information via DHCP.

Project Setup

  1. Create a new ESP-IDF project in VS Code (e.g., esp32_dns_client_example) based on the esp-idf-template.
  2. Ensure your ESP32 target is correctly selected.

Code Snippet 1: Synchronous DNS Lookup with getaddrinfo()

This example connects the ESP32 to Wi-Fi and then resolves the hostname www.espressif.com to its IP address(es).

Modify main/main.c:

C
#include <string.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "freertos/event_groups.h"
#include "esp_system.h"
#include "esp_wifi.h"
#include "esp_event.h"
#include "esp_log.h"
#include "nvs_flash.h"

#include "lwip/err.h"
#include "lwip/sys.h"
#include "lwip/netdb.h" // For getaddrinfo()
#include "lwip/sockets.h" // For AF_INET, SOCK_STREAM, IPPROTO_IP, inet_ntoa

// Wi-Fi Configuration - Replace with your network credentials
#define EXAMPLE_ESP_WIFI_SSID      "YOUR_WIFI_SSID"
#define EXAMPLE_ESP_WIFI_PASS      "YOUR_WIFI_PASSWORD"
#define EXAMPLE_ESP_MAXIMUM_RETRY  5

/* FreeRTOS event group to signal when we are connected*/
static EventGroupHandle_t s_wifi_event_group;

/* The event group allows multiple bits for each event, but we only care about two events:
 * - we are connected to the AP with an IP
 * - we failed to connect after the maximum amount of retries */
#define WIFI_CONNECTED_BIT BIT0
#define WIFI_FAIL_BIT      BIT1

static const char *TAG = "dns_client";
static int s_retry_num = 0;

static void event_handler(void* arg, esp_event_base_t event_base,
                                int32_t event_id, void* event_data)
{
    if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_STA_START) {
        esp_wifi_connect();
    } else if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_STA_DISCONNECTED) {
        if (s_retry_num < EXAMPLE_ESP_MAXIMUM_RETRY) {
            esp_wifi_connect();
            s_retry_num++;
            ESP_LOGI(TAG, "retry to connect to the AP");
        } else {
            xEventGroupSetBits(s_wifi_event_group, WIFI_FAIL_BIT);
        }
        ESP_LOGI(TAG,"connect to the AP fail");
    } else if (event_base == IP_EVENT && event_id == IP_EVENT_STA_GOT_IP) {
        ip_event_got_ip_t* event = (ip_event_got_ip_t*) event_data;
        ESP_LOGI(TAG, "got ip:" IPSTR, IP2STR(&event->ip_info.ip));
        s_retry_num = 0;
        xEventGroupSetBits(s_wifi_event_group, WIFI_CONNECTED_BIT);
    }
}

void wifi_init_sta(void)
{
    s_wifi_event_group = xEventGroupCreate();

    ESP_ERROR_CHECK(esp_netif_init());
    ESP_ERROR_CHECK(esp_event_loop_create_default());
    esp_netif_create_default_wifi_sta();

    wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT();
    ESP_ERROR_CHECK(esp_wifi_init(&cfg));

    esp_event_handler_instance_t instance_any_id;
    esp_event_handler_instance_t instance_got_ip;
    ESP_ERROR_CHECK(esp_event_handler_instance_register(WIFI_EVENT,
                                                        ESP_EVENT_ANY_ID,
                                                        &event_handler,
                                                        NULL,
                                                        &instance_any_id));
    ESP_ERROR_CHECK(esp_event_handler_instance_register(IP_EVENT,
                                                        IP_EVENT_STA_GOT_IP,
                                                        &event_handler,
                                                        NULL,
                                                        &instance_got_ip));

    wifi_config_t wifi_config = {
        .sta = {
            .ssid = EXAMPLE_ESP_WIFI_SSID,
            .password = EXAMPLE_ESP_WIFI_PASS,
            .threshold.authmode = WIFI_AUTH_WPA2_PSK, // Adjust if needed
        },
    };
    ESP_ERROR_CHECK(esp_wifi_set_mode(WIFI_MODE_STA) );
    ESP_ERROR_CHECK(esp_wifi_set_config(WIFI_IF_STA, &wifi_config) );
    ESP_ERROR_CHECK(esp_wifi_start() );

    ESP_LOGI(TAG, "wifi_init_sta finished.");

    /* Waiting until either the connection is established (WIFI_CONNECTED_BIT) or connection failed for the maximum
     * number of re-tries (WIFI_FAIL_BIT). The bits are set by event_handler() (see above) */
    EventBits_t bits = xEventGroupWaitBits(s_wifi_event_group,
            WIFI_CONNECTED_BIT | WIFI_FAIL_BIT,
            pdFALSE,
            pdFALSE,
            portMAX_DELAY);

    if (bits & WIFI_CONNECTED_BIT) {
        ESP_LOGI(TAG, "connected to ap SSID:%s", EXAMPLE_ESP_WIFI_SSID);
    } else if (bits & WIFI_FAIL_BIT) {
        ESP_LOGI(TAG, "Failed to connect to SSID:%s", EXAMPLE_ESP_WIFI_SSID);
    } else {
        ESP_LOGE(TAG, "UNEXPECTED EVENT");
    }
}

void dns_lookup_task(void *pvParameters)
{
    const char *hostname = "www.espressif.com";
    struct addrinfo hints;
    struct addrinfo *res = NULL;
    struct addrinfo *p = NULL;
    int err;

    // Wait for Wi-Fi connection
    ESP_LOGI(TAG, "Waiting for Wi-Fi connection to perform DNS lookup...");
    xEventGroupWaitBits(s_wifi_event_group, WIFI_CONNECTED_BIT, pdFALSE, pdTRUE, portMAX_DELAY);
    ESP_LOGI(TAG, "Wi-Fi connected. Starting DNS lookup for %s", hostname);

    memset(&hints, 0, sizeof(hints));
    hints.ai_family = AF_UNSPEC; // AF_INET for IPv4, AF_INET6 for IPv6, AF_UNSPEC for both
    hints.ai_socktype = SOCK_STREAM; // Typically for TCP, use SOCK_DGRAM for UDP

    // Perform DNS lookup
    // Note: getaddrinfo() is a blocking call!
    err = getaddrinfo(hostname, NULL, &hints, &res);

    if (err != 0 || res == NULL) {
        ESP_LOGE(TAG, "DNS lookup failed for %s. getaddrinfo error: %s (Code: %d)", hostname, gai_strerror(err), err);
        // gai_strerror() converts error code to string. Common error codes:
        // EAI_NONAME: Name or service not known
        // EAI_AGAIN: Temporary failure in name resolution
        // EAI_FAIL: Non-recoverable failure in name resolution
        // EAI_MEMORY: Memory allocation failure
        vTaskDelete(NULL);
        return;
    }

    ESP_LOGI(TAG, "DNS lookup successful for %s:", hostname);

    // Iterate through the results (a single hostname can resolve to multiple addresses)
    for (p = res; p != NULL; p = p->ai_next) {
        char ip_str[46]; // Max length for an IPv6 string

        if (p->ai_family == AF_INET) { // IPv4
            struct sockaddr_in *ipv4 = (struct sockaddr_in *)p->ai_addr;
            inet_ntop(AF_INET, &(ipv4->sin_addr), ip_str, sizeof(ip_str));
            ESP_LOGI(TAG, "  IPv4 address: %s", ip_str);
        } else if (p->ai_family == AF_INET6) { // IPv6
            struct sockaddr_in6 *ipv6 = (struct sockaddr_in6 *)p->ai_addr;
            inet_ntop(AF_INET6, &(ipv6->sin6_addr), ip_str, sizeof(ip_str));
            ESP_LOGI(TAG, "  IPv6 address: %s", ip_str);
        } else {
            ESP_LOGW(TAG, "  Unknown address family: %d", p->ai_family);
        }
    }

    // Free the linked list of results
    freeaddrinfo(res);

    ESP_LOGI(TAG, "DNS lookup task finished.");
    vTaskDelete(NULL);
}

void app_main(void)
{
    // Initialize NVS
    esp_err_t ret = nvs_flash_init();
    if (ret == ESP_ERR_NVS_NO_FREE_PAGES || ret == ESP_ERR_NVS_NEW_VERSION_FOUND) {
      ESP_ERROR_CHECK(nvs_flash_erase());
      ret = nvs_flash_init();
    }
    ESP_ERROR_CHECK(ret);

    ESP_LOGI(TAG, "ESP_WIFI_MODE_STA");
    wifi_init_sta(); // Connect to Wi-Fi

    // Create a task for DNS lookup to avoid blocking app_main
    // and to ensure Wi-Fi is connected before lookup.
    xTaskCreate(dns_lookup_task, "dns_lookup_task", 4096, NULL, 5, NULL);
}

Explanation:

  1. Wi-Fi Connection (wifi_init_sta): Standard ESP-IDF code to connect to a Wi-Fi AP. An event group (s_wifi_event_group) signals when the connection is established and an IP address is obtained.
  2. dns_lookup_task:
    • This task performs the DNS lookup after Wi-Fi is confirmed to be connected.
    • struct addrinfo hints;: This structure is used to provide hints to getaddrinfo() about the type of socket and address family desired.
      • hints.ai_family = AF_UNSPEC;: Allows getaddrinfo() to return both IPv4 (AF_INET) and IPv6 (AF_INET6) addresses if available.
      • hints.ai_socktype = SOCK_STREAM;: Indicates we are interested in stream sockets (typically TCP).
    • getaddrinfo(hostname, NULL, &hints, &res);: This is the core POSIX function for name resolution.
      • hostname: The domain name to resolve (e.g., “www.espressif.com“).
      • NULL: Service name (e.g., “http”, “ftp”) or port number as a string. NULL if not needed.
      • &hints: Pointer to the hints structure.
      • &res: A pointer to a struct addrinfo * which will be filled with a linked list of result structures upon success.
      • Warning: getaddrinfo() is a blocking function. It will pause the execution of the current task until the lookup is complete or times out. This is why it’s good practice to run it in a separate, non-critical task in an RTOS environment.
    • Error Handling: If getaddrinfo() fails, it returns a non-zero value. gai_strerror(err) converts this error code into a human-readable string.
    • Iterating Results: A single hostname can resolve to multiple IP addresses. res points to the head of a linked list of addrinfo structures. The code iterates through this list.
    • Address Conversion: inet_ntop() (preferred over inet_ntoa for thread-safety and IPv6 support) converts the binary IP address from p->ai_addr into a string format for printing.
    • freeaddrinfo(res);: Crucial step! The memory allocated for the results list by getaddrinfo() must be freed using freeaddrinfo() to prevent memory leaks.
  3. app_main: Initializes NVS, connects to Wi-Fi, and then creates dns_lookup_task.
`hints` Field Name Description Common Values & (Meaning)
ai_flags Flags that modify the behavior of getaddrinfo(). Can be bitwise ORed. 0 (No specific flags)
AI_PASSIVE (Socket address will be used in bind(); for server-side)
AI_CANONNAME (Request canonical name in ai_canonname of first result)
AI_NUMERICHOST (Hostname is a numeric IP string, no lookup needed)
AI_ADDRCONFIG (Return addresses only if a local interface is configured for the address family)
ai_family Specifies the desired address family for the returned addresses. AF_UNSPEC (Any address family; IPv4 or IPv6)
AF_INET (IPv4 addresses only)
AF_INET6 (IPv6 addresses only)
ai_socktype Specifies the preferred socket type (e.g., TCP or UDP). 0 (Any socket type)
SOCK_STREAM (Stream socket; typically TCP)
SOCK_DGRAM (Datagram socket; typically UDP)
ai_protocol Specifies the protocol for the returned socket addresses. 0 (Any protocol)
IPPROTO_TCP (TCP protocol)
IPPROTO_UDP (UDP protocol)
ai_addrlen (Not set in hints, returned in results) Length of the ai_addr sockaddr structure. N/A for hints
ai_addr (Not set in hints, returned in results) Pointer to a socket address structure. N/A for hints
ai_canonname (Not set in hints, returned in results if AI_CANONNAME flag is set) Pointer to the canonical name. N/A for hints
ai_next (Not set in hints, returned in results) Pointer to the next element in the linked list of results. N/A for hints

Error Code Constant (POSIX) `gai_strerror()` Example Output Meaning & Common Causes
EAI_NONAME “Name or service not known” or “Unknown host” The hostname or service name provided could not be resolved. This could be due to a typo in the hostname, the domain not existing, or the DNS server not having a record for it.
EAI_AGAIN “Temporary failure in name resolution” A temporary failure occurred in the name resolution process. The request might succeed if retried later. Could be due to an unresponsive DNS server or network issues.
EAI_FAIL “Non-recoverable failure in name resolution” A permanent, non-recoverable error occurred. For example, the DNS server might have responded with a SERVFAIL error.
EAI_MEMORY “Memory allocation failure” Insufficient memory available to complete the request or store the results.
EAI_SYSTEM “System error” (errno will be set) A system error occurred (e.g., a socket error). Check the global errno variable for more details.
EAI_SERVICE “Servname not supported for ai_socktype” The requested service name (e.g., “http”) is not supported for the specified socket type (ai_socktype).
EAI_FAMILY “ai_family not supported” The requested address family (ai_family in hints) is not supported by the system or for the given hostname.
EAI_NODATA (Often maps to EAI_NONAME on some systems) “No address associated with hostname” The hostname is valid, but it does not have any DNS records of the requested type (e.g., no AAAA records if AF_INET6 was specified).

Code Snippet 2: Configuring Custom DNS Servers

You can manually set DNS servers for a specific network interface using esp_netif_set_dns_info(). This is useful if you want to use specific DNS servers (e.g., Google Public DNS: 8.8.8.8, 8.8.4.4; or Cloudflare: 1.1.1.1) instead of those provided by DHCP.

This should be done after esp_netif_create_default_wifi_sta() and usually before esp_wifi_connect(), or if after connection, the change might take effect on subsequent DNS queries.

Add this function and call it from wifi_init_sta before esp_wifi_start() or after getting an IP if you want to override DHCP-provided DNS.

C
// Add to your includes:
#include "esp_netif.h" // For esp_netif_set_dns_info, etc.

// ... (rest of your existing code)

void set_custom_dns_servers(esp_netif_t *netif) {
    if (netif == NULL) {
        ESP_LOGE(TAG, "Network interface is NULL, cannot set DNS servers.");
        return;
    }

    esp_netif_dns_info_t dns_info_primary, dns_info_secondary, dns_info_fallback;
    
    // Primary DNS Server (e.g., Google DNS)
    inet_pton(AF_INET, "8.8.8.8", &dns_info_primary.ip.u_addr.ip4);
    dns_info_primary.ip.type = ESP_IPADDR_TYPE_V4;
    esp_err_t err = esp_netif_set_dns_info(netif, ESP_NETIF_DNS_MAIN, &dns_info_primary);
    if (err != ESP_OK) {
        ESP_LOGE(TAG, "Failed to set primary DNS server: %s", esp_err_to_name(err));
    } else {
        ESP_LOGI(TAG, "Primary DNS server set to 8.8.8.8");
    }

    // Secondary DNS Server (e.g., Google DNS Backup)
    inet_pton(AF_INET, "8.8.4.4", &dns_info_secondary.ip.u_addr.ip4);
    dns_info_secondary.ip.type = ESP_IPADDR_TYPE_V4;
    err = esp_netif_set_dns_info(netif, ESP_NETIF_DNS_BACKUP, &dns_info_secondary);
     if (err != ESP_OK) {
        ESP_LOGE(TAG, "Failed to set secondary DNS server: %s", esp_err_to_name(err));
    } else {
        ESP_LOGI(TAG, "Secondary DNS server set to 8.8.4.4");
    }
    
    // Fallback DNS (optional, often the same as primary or secondary, or a local one)
    // For this example, let's set it to Cloudflare's DNS
    inet_pton(AF_INET, "1.1.1.1", &dns_info_fallback.ip.u_addr.ip4);
    dns_info_fallback.ip.type = ESP_IPADDR_TYPE_V4;
    err = esp_netif_set_dns_info(netif, ESP_NETIF_DNS_FALLBACK, &dns_info_fallback);
    if (err != ESP_OK) {
        ESP_LOGE(TAG, "Failed to set fallback DNS server: %s", esp_err_to_name(err));
    } else {
        ESP_LOGI(TAG, "Fallback DNS server set to 1.1.1.1");
    }
}

// In wifi_init_sta(), after esp_netif_create_default_wifi_sta():
// esp_netif_t *sta_netif = esp_netif_create_default_wifi_sta();
// set_custom_dns_servers(sta_netif); 
// // Then proceed with esp_wifi_init, handlers, esp_wifi_start, etc.

To use set_custom_dns_servers:

  1. Get the esp_netif_t* handle for the STA interface: C// In wifi_init_sta(): // ... esp_netif_t *sta_netif = esp_netif_create_default_wifi_sta(); // This creates it // Call before esp_wifi_start() or immediately after esp_netif_create_default_wifi_sta() set_custom_dns_servers(sta_netif); // ...

Tip: LwIP uses the configured DNS servers in order: Main, then Backup (if Main fails), then Fallback (if Backup fails). If DHCP is enabled for IP address configuration on the interface, it might overwrite manually set DNS servers unless specific LwIP options are configured to prevent this, or if you set them after the IP is obtained. Usually, setting them after interface creation and before connection is effective for esp_netif.

Build Instructions

  1. Replace "YOUR_WIFI_SSID" and "YOUR_WIFI_PASSWORD" in main.c with your actual Wi-Fi credentials.
  2. Open VS Code, ensure your project is loaded and the ESP32 target is selected.
  3. Build the project: F1 -> ESP-IDF: Build your project.

Run/Flash/Observe Steps

  1. Connect your ESP32 board.
  2. Select the serial port: F1 -> ESP-IDF: Select Port to Use.
  3. Flash the project: F1 -> ESP-IDF: Flash your project.
  4. Monitor output: F1 -> ESP-IDF: Monitor your device.

You should see logs similar to:

Plaintext
I (XXX) dns_client: ESP_WIFI_MODE_STA
I (YYY) wifi: wifi driver task: 3ffc0868, prio:23, stack:4096, core=0
...
I (ZZZ) dns_client: got ip:192.168.1.100  // Your ESP32's IP
I (ZZZ) dns_client: connected to ap SSID:YOUR_WIFI_SSID
I (AAA) dns_client: Waiting for Wi-Fi connection to perform DNS lookup...
I (AAA) dns_client: Wi-Fi connected. Starting DNS lookup for www.espressif.com
I (BBB) dns_client: DNS lookup successful for www.espressif.com:
I (BBB) dns_client:   IPv4 address: 3.163.208.171 // Example IP, will vary
I (BBB) dns_client: DNS lookup task finished.

If you implemented the custom DNS server configuration, you would also see logs for that.

Asynchronous DNS Lookups

While getaddrinfo() is standard, its blocking nature can be an issue. For true non-blocking DNS:

  1. Run getaddrinfo() in a separate FreeRTOS task: This is the most common and straightforward approach, as demonstrated in dns_lookup_task. The main application logic remains unblocked.
  2. LwIP Raw API: LwIP offers dns_gethostbyname_addrtype() which takes a callback function. This is more complex to use directly and typically wrapped by higher-level APIs or managed within the esp_netif layer for system services.
  3. esp_netif_ppp_set_auth_async() (Specific to PPP, illustrative): ESP-IDF sometimes provides asynchronous wrappers for certain operations. While there isn’t a widely advertised generic async DNS API for direct application use in the same vein as getaddrinfo, the LwIP stack itself handles DNS asynchronously internally to some extent. For most application developers, offloading getaddrinfo to a task is the recommended practice.

For this chapter, we focus on getaddrinfo() and managing its blocking nature via FreeRTOS tasks.

Variant Notes

DNS client functionality is a software feature provided by the TCP/IP stack (LwIP) and managed through esp_netif. It does not have specific hardware dependencies on the ESP32 variant.

  • ESP32, ESP32-S2, ESP32-S3, ESP32-C3, ESP32-C6, ESP32-H2: All these variants, when equipped with a network interface (like Wi-Fi or Ethernet), can perform DNS client operations. The code and APIs (getaddrinfo, esp_netif_set_dns_info) are consistent across these variants within ESP-IDF v5.x.
  • Performance: While there might be minor differences in the raw processing speed of DNS packet handling due to CPU differences, these are generally negligible for typical DNS lookup scenarios. The primary factor affecting DNS lookup time is network latency to the DNS server.
  • Resource Usage: The dns_lookup_task requires stack memory. Ensure sufficient stack is allocated, especially if performing complex operations within the same task. The LwIP DNS client itself consumes a modest amount of RAM for caching and operation.

In summary, the DNS client implementation discussed is portable across all ESP32 variants that support TCP/IP networking via ESP-IDF.

Common Mistakes & Troubleshooting Tips

Mistake / Issue Symptom(s) Troubleshooting / Solution
No Network Connectivity / No IP Address getaddrinfo() fails immediately, often with EAI_AGAIN or EAI_SYSTEM (if network interface is down).
– ESP32 logs show Wi-Fi connection failures or no IP obtained event.
Ensure Network Readiness: Verify Wi-Fi (or Ethernet) connection is established and an IP address is obtained before attempting DNS lookup. Use event groups (like s_wifi_event_group in the example) or other synchronization methods.
Check Wi-Fi Credentials: Double-check SSID and password if using Wi-Fi.
Incorrect or Unreachable DNS Servers getaddrinfo() times out or returns errors like EAI_AGAIN, EAI_FAIL, or EAI_NONAME even for valid hostnames.
– Pinging known IP addresses works, but hostname resolution fails.
Verify DNS Configuration: If using DHCP, ensure the router provides valid DNS servers. If setting manually with esp_netif_set_dns_info(), double-check the IP addresses.
Test with Public DNS: Temporarily configure known public DNS servers (e.g., Google: 8.8.8.8, Cloudflare: 1.1.1.1) to isolate the issue.
Firewall/Router Issues: Check if your router or network firewall is blocking DNS queries (UDP/TCP port 53).
Inspect Current DNS: Use esp_netif_get_dns_info() to log the currently active DNS servers for the interface.
Forgetting freeaddrinfo(res); – Application crashes or behaves erratically after several successful DNS lookups due to memory exhaustion.
– Heap memory steadily decreases over time.
Always Free Results: Ensure every successful call to getaddrinfo() that allocates res is paired with a call to freeaddrinfo(res) once the results are no longer needed, even in error paths after res has been populated.
Blocking getaddrinfo() Freezes System – Entire system or critical tasks become unresponsive during DNS lookups.
– Watchdog timer might trigger and reset the ESP32.
Use a Separate Task: Execute getaddrinfo() in a dedicated FreeRTOS task with appropriate priority and stack size. This prevents blocking critical system functions or other application tasks.
Consider Timeouts (Indirectly): While getaddrinfo() itself doesn’t have a direct timeout parameter in its standard signature, LwIP’s internal DNS client has retry and timeout mechanisms. Long delays usually point to network or server issues.
Hostname Not Found (EAI_NONAME / HOST_NOT_FOUND) getaddrinfo() returns EAI_NONAME for a hostname you expect to exist. Check Hostname String: Verify for typos or incorrect characters in the hostname string.
Verify Domain Existence: Use an external tool (e.g., nslookup on a PC) to confirm the domain exists and is resolvable from another network.
Check hints.ai_family: If you request AF_INET but the host only has AAAA records (IPv6), or vice-versa, you might get this error. Use AF_UNSPEC to get any available address type.
Insufficient Stack for DNS Task – Task performing DNS lookup crashes, often due to stack overflow, especially if other operations are done in the same task. Allocate Sufficient Stack: Ensure the task calling getaddrinfo() has enough stack space. 4096 bytes is a common starting point for network tasks, but monitor actual usage if possible (uxTaskGetStackHighWaterMark()).

Exercises

  1. Hostname to IPv4 String Function:
    • Write a C function esp_err_t get_ipv4_for_hostname(const char *hostname, char *ipv4_str, size_t ipv4_str_len) that takes a hostname, resolves it, and if an IPv4 address is found, writes it as a string into ipv4_str.
    • The function should return ESP_OK on success, ESP_ERR_NOT_FOUND if no IPv4 address is found (but other records might exist), or other ESP-IDF error codes for different failures (e.g., ESP_FAIL for general DNS failure).
    • Handle getaddrinfo errors and ensure freeaddrinfo is called. Only consider the first IPv4 address found.
  2. Multiple Hostname Lookup & Timing:
    • Modify the dns_lookup_task to resolve an array of hostnames (e.g., {"www.google.com", "www.github.com", "id.espressif.com", "thisdomainprobablydoesnotexist.xyz"}).
    • For each hostname, log whether the lookup was successful and list the IP addresses.
    • Use esp_timer_get_time() or xTaskGetTickCount() before and after each getaddrinfo() call to measure and log the time taken for each DNS resolution attempt.
  3. Simple HTTP GET after DNS Lookup:
    • Extend the dns_lookup_task. After successfully resolving a hostname (e.g., httpbin.org) to an IPv4 address:
      1. Create a TCP socket.
      2. Connect to the resolved IP address on port 80 (HTTP).
      3. Send a basic HTTP GET request (e.g., GET /ip HTTP/1.1\r\nHost: httpbin.org\r\n\r\n).
      4. Read and log the first few hundred bytes of the server’s response.
      5. Close the socket.
    • This exercise combines DNS resolution with basic socket programming (covered in later chapters, but a simple implementation can be illustrative here). Focus on successfully connecting after DNS resolution; a full HTTP client is complex.

Summary

  • DNS translates human-readable hostnames into numerical IP addresses, acting like the internet’s phonebook.
  • DNS has a hierarchical structure (root, TLD, authoritative servers).
  • The resolution process typically involves a client querying a local DNS resolver, which may perform recursive queries.
  • Common DNS record types include A (IPv4), AAAA (IPv6), CNAME, MX, and NS.
  • ESP-IDF uses LwIP’s DNS client. getaddrinfo() is the standard C function for performing DNS lookups.
  • DNS servers can be obtained via DHCP or configured manually using esp_netif_set_dns_info().
  • getaddrinfo() is a blocking call; run it in a separate task in FreeRTOS applications to avoid freezing other operations.
  • Always call freeaddrinfo() to prevent memory leaks after using getaddrinfo().
  • Proper error handling for DNS lookups is essential for robust network applications.
  • DNS client functionality is consistent across all network-enabled ESP32 variants.

Further Reading

  • ESP-IDF esp_netif API Reference:
  • LwIP DNS Client Documentation:
    • While often abstracted, understanding the underlying LwIP DNS client can be helpful: LwIP dns.h source
  • getaddrinfo() Manual Page:
    • A standard Linux man page provides comprehensive details: man getaddrinfo or online versions.
  • RFC 1034: DOMAIN NAMES – CONCEPTS AND FACILITIES
  • RFC 1035: DOMAIN NAMES – IMPLEMENTATION AND SPECIFICATION
  • ESP-IDF Wi-Fi Station Example: (For basic Wi-Fi setup):

Leave a Comment

Your email address will not be published. Required fields are marked *

Scroll to Top