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;
- 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.
- 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. - Second-Level Domains (SLDs): This is the part of the domain name that you typically register, like
espressif
inespressif.com
. - Subdomains: Further subdivisions, like
docs.espressif.com
.
DNS Resolution Process
When your ESP32 (as a DNS client) needs to resolve a hostname:
- 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.
- 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.
- 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.
- 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
- Create a new ESP-IDF project in VS Code (e.g.,
esp32_dns_client_example
) based on theesp-idf-template
. - 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
:
#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:
- 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. 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 togetaddrinfo()
about the type of socket and address family desired.hints.ai_family = AF_UNSPEC;
: Allowsgetaddrinfo()
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 astruct 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 ofaddrinfo
structures. The code iterates through this list. - Address Conversion:
inet_ntop()
(preferred overinet_ntoa
for thread-safety and IPv6 support) converts the binary IP address fromp->ai_addr
into a string format for printing. freeaddrinfo(res);
: Crucial step! The memory allocated for the results list bygetaddrinfo()
must be freed usingfreeaddrinfo()
to prevent memory leaks.
app_main
: Initializes NVS, connects to Wi-Fi, and then createsdns_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.
// 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
:
- 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
- Replace
"YOUR_WIFI_SSID"
and"YOUR_WIFI_PASSWORD"
inmain.c
with your actual Wi-Fi credentials. - Open VS Code, ensure your project is loaded and the ESP32 target is selected.
- Build the project:
F1
->ESP-IDF: Build your project
.
Run/Flash/Observe Steps
- Connect your ESP32 board.
- Select the serial port:
F1
->ESP-IDF: Select Port to Use
. - Flash the project:
F1
->ESP-IDF: Flash your project
. - Monitor output:
F1
->ESP-IDF: Monitor your device
.
You should see logs similar to:
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:
- Run
getaddrinfo()
in a separate FreeRTOS task: This is the most common and straightforward approach, as demonstrated indns_lookup_task
. The main application logic remains unblocked. - 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 theesp_netif
layer for system services. 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 asgetaddrinfo
, the LwIP stack itself handles DNS asynchronously internally to some extent. For most application developers, offloadinggetaddrinfo
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
- 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 intoipv4_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 ensurefreeaddrinfo
is called. Only consider the first IPv4 address found.
- Write a C function
- 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()
orxTaskGetTickCount()
before and after eachgetaddrinfo()
call to measure and log the time taken for each DNS resolution attempt.
- Modify the
- Simple HTTP GET after DNS Lookup:
- Extend the
dns_lookup_task
. After successfully resolving a hostname (e.g.,httpbin.org
) to an IPv4 address:- Create a TCP socket.
- Connect to the resolved IP address on port 80 (HTTP).
- Send a basic HTTP GET request (e.g.,
GET /ip HTTP/1.1\r\nHost: httpbin.org\r\n\r\n
). - Read and log the first few hundred bytes of the server’s response.
- 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.
- Extend the
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 usinggetaddrinfo()
. - 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:- esp_netif Documentation – For
esp_netif_set_dns_info
,esp_netif_get_dns_info
.
- esp_netif Documentation – For
- LwIP DNS Client Documentation:
- While often abstracted, understanding the underlying LwIP DNS client can be helpful: LwIP
dns.h
source
- While often abstracted, understanding the underlying LwIP DNS client can be helpful: LwIP
getaddrinfo()
Manual Page:- A standard Linux man page provides comprehensive details:
man getaddrinfo
or online versions.
- A standard Linux man page provides comprehensive details:
- 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):
- ESP-IDF Wi-Fi Examples (Adapt branch for v5.x)
