Chapter 79: Dual-stack IPv4/IPv6 Applications
Chapter Objectives
Upon completing this chapter, you will be able to:
- Understand the concept of dual-stack networking and its importance in the transition from IPv4 to IPv6.
- Explain how applications can be designed to seamlessly communicate over both IPv4 and IPv6.
- Utilize the
getaddrinfo()
function for protocol-agnostic hostname resolution in ESP-IDF. - Implement a dual-stack client application capable of connecting to both IPv4 and IPv6 endpoints.
- Comprehend the principles of address selection in a dual-stack environment.
- Troubleshoot common issues related to dual-stack application development.
Introduction
In the current global network landscape, we are in a prolonged transition period from IPv4 to IPv6. While IPv6 offers a vast address space and numerous improvements, IPv4 remains widely deployed and will continue to be for many years. This creates a challenge for networked applications: how can they ensure connectivity to resources that might only be reachable via IPv4, while also leveraging the benefits of IPv6 when available? The answer lies in dual-stack networking.
A dual-stack device or application is one that can operate with both IPv4 and IPv6 simultaneously. It has the capability to process both types of IP packets and can communicate with other devices regardless of whether they are IPv4-only, IPv6-only, or also dual-stack. For ESP32-based IoT devices, implementing dual-stack capabilities is crucial for maximizing compatibility, ensuring future-proofing, and enabling robust communication in diverse network environments. This chapter will delve into the theory and practical implementation of dual-stack applications using ESP-IDF v5.x.
Theory
1. What is Dual-stack Networking?
Imagine you have a phone that can make calls using both the traditional cellular network (like 4G) and a brand-new, super-fast network (like 5G). If you call someone, your phone intelligently decides which network to use based on availability and preference, or it can even try both if one fails. This is analogous to dual-stack networking.
In the context of IP, a dual-stack host (like your ESP32) has both an IPv4 address and an IPv6 address assigned to its network interface. It can send and receive both IPv4 and IPv6 packets. This allows it to communicate with:
- IPv4-only devices: By using its IPv4 address.
- IPv6-only devices: By using its IPv6 address.
- Other dual-stack devices: By choosing the most suitable protocol (often preferring IPv6).
The primary benefit of dual-stack is interoperability during the transition period. It avoids the need for complex translation mechanisms (like NAT64 or DNS64) at the application layer, allowing applications to communicate directly with endpoints regardless of their IP version.
Aspect | Benefit / Feature | Consideration / Challenge |
---|---|---|
Interoperability | Seamless communication with IPv4-only, IPv6-only, and other dual-stack devices. | Slightly increased complexity in network stack configuration on the device. |
Transition Strategy | Facilitates smooth migration from IPv4 to IPv6 without breaking existing IPv4 connectivity. | Network infrastructure (routers, DNS) must also support dual-stack or provide necessary transition mechanisms. |
Future-Proofing | Ensures devices remain compatible as IPv6 adoption grows. | Application logic might need to handle both address families correctly (e.g., logging, UI display). |
Address Availability | Leverages vast IPv6 address space when available, reducing reliance on NAT for IPv4. | Device will consume both an IPv4 and one or more IPv6 addresses. |
Application Development | Modern APIs like getaddrinfo() simplify development for dual-stack clients. |
Developers need to be aware of address selection rules and potential pitfalls (e.g., IPV6_V6ONLY for servers). |
Performance | Can potentially offer better performance over IPv6 due to simplified headers and no NAT. | Minimal overhead for maintaining both stacks; actual performance depends on network conditions. |
Resource Usage | Slightly more memory and processing for managing both IP stacks on the ESP32. | Usually manageable on ESP32, but a factor for extremely constrained devices. |
2. The Role of getaddrinfo()
When an application wants to connect to a server, it typically uses a hostname (e.g., www.example.com
) rather than a raw IP address. In a dual-stack environment, this hostname might resolve to both IPv4 and IPv6 addresses. How does the application know which one to use? This is where the getaddrinfo()
function becomes indispensable.
getaddrinfo()
is a protocol-independent function that translates a hostname (and optional service name/port) into a list of socket address structures. It’s the modern and preferred way to perform DNS lookups because it handles both IPv4 and IPv6 resolution automatically.
%%{init: {"theme": "base", "themeVariables": {"primaryTextColor": "#333333", "lineColor": "#777777"}}}%% graph TD Start["Start: Client wants to connect to<br><b>hostname</b> (e.g., 'example.com') on <b>port</b> (e.g., '80')"] --> CallGetAddrInfo["Call getaddrinfo(hostname, port, hints, &res)<br>hints.ai_family = AF_UNSPEC<br>hints.ai_socktype = SOCK_STREAM"]; CallGetAddrInfo --> CheckResult{"getaddrinfo() Successful?"}; CheckResult -- "No (err != 0 or res == NULL)" --> ErrorDNS["Error: DNS Lookup Failed<br>Handle error (e.g., log, retry)"]; ErrorDNS --> End; CheckResult -- "Yes" --> LoopResults["Iterate through linked list of 'addrinfo' structures (res)"]; LoopResults --> ForEachAddr{"For each 'addrinfo' (rp) in 'res':"}; ForEachAddr --> CreateSocket["1. Create Socket:<br>s = socket(rp->ai_family, rp->ai_socktype, rp->ai_protocol)"]; CreateSocket --> CheckSocket{"Socket Created Successfully (s >= 0)?"}; CheckSocket -- "No" --> LogSocketFail["Log: Socket Creation Failed for this family.<br>Continue to next 'addrinfo'."]; LogSocketFail --> LoopResults; CheckSocket -- "Yes" --> Connect["2. Attempt Connection:<br>connect(s, rp->ai_addr, rp->ai_addrlen)"]; Connect --> CheckConnect{"Connection Successful?"}; CheckConnect -- "No" --> LogConnectFail["Log: Connection Failed for this address.<br>close(s).<br>Continue to next 'addrinfo'."]; LogConnectFail --> LoopResults; CheckConnect -- "Yes" --> Connected["Successfully Connected!<br>Using socket 's'.<br>Protocol: (rp->ai_family == AF_INET6 ? 'IPv6' : 'IPv4')<br>Proceed with communication."]; Connected --> FreeAddrInfo["freeaddrinfo(res)"]; FreeAddrInfo --> End["End: Communication or Cleanup"]; LoopResults -- "End of list & no connection" --> AllAttemptsFailed["Error: All Connection Attempts Failed<br>Handle error."]; AllAttemptsFailed --> FreeAddrInfo; classDef startEnd fill:#EDE9FE,stroke:#5B21B6,stroke-width:2px,color:#5B21B6; classDef process fill:#DBEAFE,stroke:#2563EB,stroke-width:1px,color:#1E40AF; classDef decision fill:#FEF3C7,stroke:#D97706,stroke-width:1px,color:#92400E; classDef success fill:#D1FAE5,stroke:#059669,stroke-width:2px,color:#065F46; classDef error fill:#FEE2E2,stroke:#DC2626,stroke-width:1px,color:#991B1B; class Start startEnd; class End startEnd; class CallGetAddrInfo process; class LoopResults process; class ForEachAddr process; class CreateSocket process; class Connect process; class FreeAddrInfo process; class CheckResult decision; class CheckSocket decision; class CheckConnect decision; class Connected success; class ErrorDNS error; class LogSocketFail error; class LogConnectFail error; class AllAttemptsFailed error;
Analogy: Think of getaddrinfo()
as a smart travel agent. You tell the agent where you want to go (the hostname) and what kind of service you need (e.g., web server, email server). The agent then provides you with all possible routes (IPv4 and IPv6 addresses) to that destination, along with the necessary details (like which type of vehicle, i.e., socket family, to use for each route). Your application then tries these routes until one works.
getaddrinfo()
Parameters:
int getaddrinfo(const char *nodename, // Hostname or IP address string
const char *servname, // Service name (e.g., "http", "80") or port number string
const struct addrinfo *hints, // Optional criteria for the lookup
struct addrinfo **res); // Pointer to store the linked list of results
nodename
: The hostname (e.g.,"www.google.com"
) or a numeric IP address string (e.g.,"192.168.1.1"
,"[2001:db8::1]"
).servname
: The service name (e.g.,"http"
,"https"
,"ftp"
) or a port number as a string (e.g.,"80"
,"443"
).hints
: A pointer to anaddrinfo
structure that provides criteria for the lookup. Key fields inhints
:ai_family
: Specifies the desired address family (AF_UNSPEC
for any,AF_INET
for IPv4,AF_INET6
for IPv6). For dual-stack,AF_UNSPEC
is typically used.ai_socktype
: Specifies the socket type (SOCK_STREAM
for TCP,SOCK_DGRAM
for UDP).ai_flags
: Additional flags, such asAI_PASSIVE
(for server-sidebind
),AI_CANONNAME
.
res
: On successful return, this points to a linked list ofaddrinfo
structures, each containing a resolved address and associated socket information.
hints Field |
Common Value(s) | Purpose in Dual-Stack Context |
---|---|---|
ai_family |
AF_UNSPEC |
Allows getaddrinfo() to return addresses for any address family (IPv4 or IPv6). This is key for dual-stack clients. |
AF_INET |
Restricts results to IPv4 addresses only. | |
AF_INET6 |
Restricts results to IPv6 addresses only. | |
ai_socktype |
SOCK_STREAM |
Specifies a stream socket (typically TCP). |
SOCK_DGRAM |
Specifies a datagram socket (typically UDP). | |
ai_flags |
AI_PASSIVE |
Indicates the address structure will be used in a call to bind() . The returned socket address will contain the “wildcard address” (0.0.0.0 for IPv4, :: for IPv6) if nodename is NULL . Essential for server applications. |
AI_CANONNAME |
Requests the canonical name of the host. If set, the ai_canonname field in the first returned addrinfo structure will point to a null-terminated string containing the canonical name. |
|
AI_NUMERICHOST |
If set, nodename must be a numeric IP address string. Prevents DNS resolution. |
3. Address Selection in Dual-stack Clients
When getaddrinfo()
returns multiple addresses (both IPv4 and IPv6) for a given hostname, the client application needs a strategy to choose which address to try first. The standard behavior, defined in RFC 6724 (Default Address Selection for IPv6), generally prioritizes IPv6 if a Global Unicast IPv6 address is available on the local interface and the destination is also IPv6.
A common client-side strategy is:
- Iterate through
getaddrinfo
results: Loop through the linked list ofaddrinfo
structures returned bygetaddrinfo()
. - Try IPv6 first: Attempt to create a socket and connect using the
AF_INET6
addresses. - Fallback to IPv4: If all IPv6 connection attempts fail, then try the
AF_INET
addresses.
This approach ensures that if an IPv6 path exists and is working, it will be used, but the application can still fall back to IPv4 if necessary, providing robust connectivity.
%%{init: {"theme": "base", "themeVariables": {"primaryTextColor": "#333333", "lineColor": "#777777"}}}%% graph TD Start["Start: Resolve Hostname (getaddrinfo with AF_UNSPEC)"] --> Results{"List of IPv6 and IPv4 Addresses Received?"}; Results -- "No / Error" --> HandleError["Handle DNS Error"]; HandleError --> End; Results -- "Yes" --> TryIPv6{"Attempt Connections using IPv6 Addresses First"}; subgraph IPv6Attempts ["Try IPv6 Addresses"] direction LR LoopIPv6["For each IPv6 address:"] --> ConnectIPv6{"Connect?"}; ConnectIPv6 -- "Success" --> SuccessIPv6["Connected via IPv6!"]; ConnectIPv6 -- "Fail" --> NextIPv6{"More IPv6 addresses?"}; NextIPv6 -- "Yes" --> LoopIPv6; end SuccessIPv6 --> End["End: Communication"]; TryIPv6 --> LoopIPv6; NextIPv6 -- "No more IPv6 / All IPv6 failed" --> TryIPv4{"Fallback: Attempt Connections using IPv4 Addresses"}; subgraph IPv4Attempts ["Try IPv4 Addresses"] direction LR LoopIPv4["For each IPv4 address:"] --> ConnectIPv4{"Connect?"}; ConnectIPv4 -- "Success" --> SuccessIPv4["Connected via IPv4!"]; ConnectIPv4 -- "Fail" --> NextIPv4{"More IPv4 addresses?"}; NextIPv4 -- "Yes" --> LoopIPv4; end SuccessIPv4 --> End; TryIPv4 --> LoopIPv4; NextIPv4 -- "No more IPv4 / All IPv4 failed" --> FailAll["All Connection Attempts Failed"]; FailAll --> HandleError; classDef startEnd fill:#EDE9FE,stroke:#5B21B6,stroke-width:2px,color:#5B21B6; classDef process fill:#DBEAFE,stroke:#2563EB,stroke-width:1px,color:#1E40AF; classDef decision fill:#FEF3C7,stroke:#D97706,stroke-width:1px,color:#92400E; classDef success fill:#D1FAE5,stroke:#059669,stroke-width:2px,color:#065F46; classDef error fill:#FEE2E2,stroke:#DC2626,stroke-width:1px,color:#991B1B; class Start startEnd; class End startEnd; class Results decision; class TryIPv6 decision; class NextIPv6 decision; class TryIPv4 decision; class NextIPv4 decision; class LoopIPv6 process; class ConnectIPv6 process; class LoopIPv4 process; class ConnectIPv4 process; class SuccessIPv6 success; class SuccessIPv4 success; class HandleError error; class FailAll error;
4. Dual-stack Sockets (LwIP and ESP-IDF)
LwIP, the TCP/IP stack used by ESP-IDF, supports both IPv4 and IPv6. The standard socket API (socket()
, bind()
, connect()
, listen()
, accept()
, send()
, recv()
) works seamlessly with both address families.
- To create an IPv4 socket, use
AF_INET
for thedomain
parameter insocket()
. - To create an IPv6 socket, use
AF_INET6
for thedomain
parameter insocket()
.
For server applications, an IPv6 socket can often be configured to accept connections from both IPv4 and IPv6 clients. This is done by setting the IPV6_V6ONLY
socket option to 0
(false) on an AF_INET6
socket. This allows the IPv6 socket to bind to a wildcard address (::
) and listen for both IPv4 and IPv6 connections, simplifying server development.
Warning: While IPV6_V6ONLY
can simplify server code, it’s important to understand its implications. If IPV6_V6ONLY
is 0
, an IPv6 socket can also accept IPv4 connections (mapped to IPv6 format). If it’s 1
(default), it will only accept IPv6 connections.
Practical Examples
This example demonstrates a dual-stack HTTP client that attempts to connect to a given hostname. It uses getaddrinfo()
to resolve the hostname and then tries to connect via IPv6 first, falling back to IPv4 if IPv6 connection fails.
1. Project Setup
Create a new ESP-IDF project using VS Code. You can use the “ESP-IDF: New Project” command. Name it dual_stack_http_client
.
2. main/dual_stack_http_client_main.c
#include <string.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "esp_system.h"
#include "esp_wifi.h"
#include "esp_event.h"
#include "esp_log.h"
#include "nvs_flash.h"
#include "sdkconfig.h"
#include "lwip/err.h"
#include "lwip/sockets.h"
#include "lwip/netdb.h"
#include "lwip/dns.h" // For DNS resolution
static const char *TAG = "DUAL_STACK_CLIENT";
#define WEB_SERVER CONFIG_WEB_SERVER_HOSTNAME // Hostname to connect to (e.g., "example.com", "ipv6.google.com")
#define WEB_PORT "80" // HTTP port
// Event handler for Wi-Fi and IP events
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_LOGI(TAG, "Wi-Fi STA started, connecting...");
esp_wifi_connect();
} else if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_STA_DISCONNECTED) {
ESP_LOGI(TAG, "Wi-Fi STA disconnected, retrying connection...");
esp_wifi_connect(); // Retry connection
} 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 IPv4 address: " IPSTR, IP2STR(&event->ip_info.ip));
} else if (event_base == IP_EVENT && event_id == IP_EVENT_STA_GOT_IP6) {
ip_event_got_ip6_t *event = (ip_event_got_ip6_t *)event_data;
char ip6_addr[64];
esp_ip6addr_ntoa(&event->ip_info.ip, ip6_addr, sizeof(ip6_addr));
ESP_LOGI(TAG, "Got IPv6 address: %s", ip6_addr);
if (esp_netif_is_link_local_ip6(event->esp_netif, &event->ip_info.ip)) {
ESP_LOGI(TAG, " (Link-Local Address)");
} else {
ESP_LOGI(TAG, " (Global Unicast Address)");
}
}
}
/**
* @brief Performs an HTTP GET request using dual-stack approach.
* Resolves hostname using getaddrinfo, tries IPv6 first, then IPv4.
*/
static void http_client_task(void *pvParameters)
{
const struct addrinfo hints = {
.ai_family = AF_UNSPEC, // Allow IPv4 or IPv6
.ai_socktype = SOCK_STREAM, // TCP socket
};
struct addrinfo *res;
int s, r;
char recv_buf[128];
char http_request[256];
while (1) {
int err = getaddrinfo(WEB_SERVER, WEB_PORT, &hints, &res);
if (err != 0 || res == NULL) {
ESP_LOGE(TAG, "DNS lookup failed for %s. Error: %d", WEB_SERVER, err);
vTaskDelay(pdMS_TO_TICKS(5000));
continue;
}
struct addrinfo *rp;
s = -1; // Socket descriptor
for (rp = res; rp != NULL; rp = rp->ai_next) {
s = socket(rp->ai_family, rp->ai_socktype, rp->ai_protocol);
if (s < 0) {
ESP_LOGE(TAG, "Failed to create socket (family %d): %s", rp->ai_family, strerror(errno));
continue;
}
// Set socket receive timeout
struct timeval receiving_timeout;
receiving_timeout.tv_sec = 5;
receiving_timeout.tv_usec = 0;
if (setsockopt(s, SOL_SOCKET, SO_RCVTIMEO, &receiving_timeout, sizeof(receiving_timeout)) < 0) {
ESP_LOGE(TAG, "Failed to set socket receiving timeout");
close(s);
s = -1;
continue;
}
// Convert address to string for logging
char addr_str[64];
if (rp->ai_family == AF_INET) {
inet_ntoa_r(((struct sockaddr_in *)rp->ai_addr)->sin_addr, addr_str, sizeof(addr_str) - 1);
ESP_LOGI(TAG, "Attempting to connect to IPv4: %s:%s", addr_str, WEB_PORT);
} else if (rp->ai_family == AF_INET6) {
inet6_ntoa_r(((struct sockaddr_in6 *)rp->ai_addr)->sin6_addr, addr_str, sizeof(addr_str) - 1);
ESP_LOGI(TAG, "Attempting to connect to IPv6: [%s]:%s", addr_str, WEB_PORT);
}
if (connect(s, rp->ai_addr, rp->ai_addrlen) != 0) {
ESP_LOGE(TAG, "Connect failed (family %d): %s", rp->ai_family, strerror(errno));
close(s);
s = -1;
continue;
}
ESP_LOGI(TAG, "Successfully connected to %s via %s", WEB_SERVER, (rp->ai_family == AF_INET6) ? "IPv6" : "IPv4");
break; // Connection successful, break from loop
}
freeaddrinfo(res); // Free the linked list of addrinfo structures
if (s < 0) {
ESP_LOGE(TAG, "Could not connect to %s after trying all addresses.", WEB_SERVER);
vTaskDelay(pdMS_TO_TICKS(5000));
continue;
}
// Construct HTTP GET request
snprintf(http_request, sizeof(http_request), "GET / HTTP/1.0\r\nHost: %s\r\nUser-Agent: ESP32Client\r\n\r\n", WEB_SERVER);
if (write(s, http_request, strlen(http_request)) < 0) {
ESP_LOGE(TAG, "Send failed: %s", strerror(errno));
close(s);
vTaskDelay(pdMS_TO_TICKS(5000));
continue;
}
ESP_LOGI(TAG, "HTTP GET request sent.");
/* Read HTTP response */
do {
r = read(s, recv_buf, sizeof(recv_buf) - 1);
if (r > 0) {
recv_buf[r] = 0; // Null-terminate
ESP_LOGI(TAG, "Received %d bytes:\n%s", r, recv_buf);
} else if (r == 0) {
ESP_LOGI(TAG, "Connection closed by remote host.");
} else {
ESP_LOGE(TAG, "Read failed: %s", strerror(errno));
}
} while (r > 0);
ESP_LOGI(TAG, "Closing socket.");
close(s);
// Wait before next request
vTaskDelay(pdMS_TO_TICKS(10000));
}
}
void app_main(void)
{
// Initialize NVS (Non-Volatile Storage) for Wi-Fi configuration
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);
// Initialize TCP/IP stack
ESP_ERROR_CHECK(esp_netif_init());
// Create default event loop
ESP_ERROR_CHECK(esp_event_loop_create_default());
// Create Wi-Fi station interface
esp_netif_t *sta_netif = esp_netif_create_default_wifi_sta();
assert(sta_netif);
// Register event handlers
ESP_ERROR_CHECK(esp_event_handler_register(WIFI_EVENT, ESP_EVENT_ANY_ID, &event_handler, NULL));
ESP_ERROR_CHECK(esp_event_handler_register(IP_EVENT, IP_EVENT_STA_GOT_IP, &event_handler, NULL));
ESP_ERROR_CHECK(esp_event_handler_register(IP_EVENT, IP_EVENT_STA_GOT_IP6, &event_handler, NULL));
// Initialize Wi-Fi
wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT();
ESP_ERROR_CHECK(esp_wifi_init(&cfg));
// Configure Wi-Fi station mode
wifi_config_t wifi_config = {
.sta = {
.ssid = CONFIG_WIFI_SSID,
.password = CONFIG_WIFI_PASSWORD,
.threshold.authmode = WIFI_AUTH_WPA2_PSK,
.sae_pwe_h2e = WIFI_SAE_MODE_OWE, // For WPA3, if supported by AP
.sae_h2e_identifier = "",
},
};
ESP_ERROR_CHECK(esp_wifi_set_mode(WIFI_MODE_STA));
ESP_ERROR_CHECK(esp_wifi_set_config(WIFI_IF_STA, &wifi_config));
// Enable IPv6 support (important for dual-stack)
// This is typically enabled by default with esp_netif_create_default_wifi_sta(),
// but explicitly enabling it ensures it's active.
ESP_ERROR_CHECK(esp_netif_set_ip6_addr_type(sta_netif, ESP_NETIF_IP6_ADDR_TYPE_AUTOCONF));
ESP_ERROR_CHECK(esp_wifi_start());
ESP_LOGI(TAG, "Waiting for IP addresses (IPv4 and IPv6) to start client...");
// Create a task to run the HTTP client after network is up
// We'll wait for both IPv4 and IPv6 to be up, or just IPv4 if IPv6 is not available.
// A simple approach is to delay and then start the client task.
// For production, you might want to use a FreeRTOS event group or semaphore
// signaled by the IP_EVENT handlers to ensure network readiness.
xTaskCreate(&http_client_task, "http_client_task", 4096, NULL, 5, NULL);
}
3. main/CMakeLists.txt
The default CMakeLists.txt
should be sufficient. Ensure idf_component_register
is present.
# In the main/CMakeLists.txt file
idf_component_register(SRCS "dual_stack_http_client_main.c"
INCLUDE_DIRS ".")
4. main/Kconfig.projbuild
This file allows you to configure project-specific settings via idf.py menuconfig.
Create main/Kconfig.projbuild and add the following:
menu "Network Configuration"
config WIFI_SSID
string "Wi-Fi SSID"
default "YOUR_SSID"
help
SSID of your Wi-Fi network.
config WIFI_PASSWORD
string "Wi-Fi Password"
default "YOUR_PASSWORD"
help
Password of your Wi-Fi network.
config WEB_SERVER_HOSTNAME
string "Web Server Hostname"
default "example.com"
help
Hostname of the web server to connect to (e.g., example.com, ipv6.google.com).
endmenu
5. Build Instructions
- Open VS Code: Open your
dual_stack_http_client
project folder in VS Code. - Configure with
menuconfig
:- Press
Ctrl+Shift+P
(orCmd+Shift+P
on macOS) to open the command palette. - Type and select “ESP-IDF: SDK Configuration Editor (menuconfig)”.
- Navigate to “Network Configuration”.
- Enter your Wi-Fi SSID and Password.
- Enter the
Web Server Hostname
you want to test (e.g.,example.com
,ipv6.google.com
,www.google.com
). - Crucially, ensure IPv6 is enabled in the ESP-IDF configuration:
- Navigate to
Component config
->LWIP
. - Check
Enable IPv6 support
. - Under
IPv6
, ensureEnable IPv6 Stateless Address Autoconfiguration (SLAAC)
is checked.
- Navigate to
- Save the configuration and exit
menuconfig
.
- Press
- Build the Project:
- Open the VS Code Terminal (
Terminal > New Terminal
). - Run
idf.py build
. This will compile the project.
- Open the VS Code Terminal (
6. Run/Flash/Observe Steps
- Connect Hardware:
- Connect your ESP32 development board to your computer via USB.
- Flash the Firmware:
- In the VS Code Terminal, run
idf.py flash
. This will erase the chip and flash your compiled firmware.
- In the VS Code Terminal, run
- Monitor Serial Output:
- After flashing, run
idf.py monitor
. This will open the serial monitor and display the logs from your ESP32.
- After flashing, run
Expected Output (connecting to example.com
on an IPv6-enabled network):
I (XXX) DUAL_STACK_CLIENT: Waiting for IP addresses (IPv4 and IPv6) to start client...
I (XXX) DUAL_STACK_CLIENT: Wi-Fi STA started, connecting...
I (XXX) DUAL_STACK_CLIENT: Got IPv4 address: 192.168.1.105
I (XXX) DUAL_STACK_CLIENT: Got IPv6 address: fe80::XXXX:XXXX:XXXX:XXXX
I (XXX) DUAL_STACK_CLIENT: (Link-Local Address)
I (XXX) DUAL_STACK_CLIENT: Got IPv6 address: 2001:db8:XXXX:XXXX:XXXX:XXXX:XXXX:XXXX
I (XXX) DUAL_STACK_CLIENT: (Global Unicast Address)
I (XXX) DUAL_STACK_CLIENT: Attempting to connect to IPv6: [2606:2800:220:1:248:1893:25c8:1946]:80 // Example IPv6 for example.com
I (XXX) DUAL_STACK_CLIENT: Successfully connected to example.com via IPv6
I (XXX) DUAL_STACK_CLIENT: HTTP GET request sent.
I (XXX) DUAL_STACK_CLIENT: Received XXX bytes:
<HTTP response content from example.com>
I (XXX) DUAL_STACK_CLIENT: Connection closed by remote host.
I (XXX) DUAL_STACK_CLIENT: Closing socket.
If the IPv6 connection fails or is not available, you would see:
I (XXX) DUAL_STACK_CLIENT: Attempting to connect to IPv6: [....]
E (XXX) DUAL_STACK_CLIENT: Connect failed (family 10): Connection refused // or similar error
I (XXX) DUAL_STACK_CLIENT: Attempting to connect to IPv4: 93.184.216.34:80 // Example IPv4 for example.com
I (XXX) DUAL_STACK_CLIENT: Successfully connected to example.com via IPv4
I (XXX) DUAL_STACK_CLIENT: HTTP GET request sent.
...
Variant Notes
The concept of dual-stack networking and its implementation using the LwIP stack and esp_netif
component are universal across all ESP32 variants (ESP32, ESP32-S2, ESP32-S3, ESP32-C3, ESP32-C6, ESP32-H2).
The core APIs for network interface management (esp_netif
), event handling (esp_event
), and socket programming (lwip/sockets.h
, lwip/netdb.h
) are designed to be hardware-agnostic at the application level. This means that the dual-stack client code presented in this chapter will function identically on any ESP32 variant, provided that:
- The specific variant’s underlying network interface (Wi-Fi or Ethernet) is properly initialized and capable of establishing both IPv4 and IPv6 connectivity with the network.
- IPv6 support is enabled in the ESP-IDF
menuconfig
for the project.
The differences between variants primarily lie in their physical layer capabilities (e.g., internal Ethernet MAC on ESP32 classic vs. external SPI Ethernet on ESP32-S2/S3/C3/C6/H2). However, once the esp_netif
instance is created and configured for a particular interface, the dual-stack logic at the application layer remains consistent, leveraging the common LwIP and esp_netif
APIs. Therefore, no specific variant-dependent code changes are required for dual-stack applications.
Common Mistakes & Troubleshooting Tips
Mistake / Issue | Symptom(s) | Troubleshooting / Solution |
---|---|---|
IPv6 Not Enabled/Configured on Network/Router | getaddrinfo() might return IPv6 addresses for global hostnames, but connection attempts to them fail. ESP32 may only have an IPv4 GUA and an IPv6 LLA. |
|
IPv6 Support Not Enabled in ESP-IDF | getaddrinfo() with AF_UNSPEC might only return IPv4. IPv6 sockets fail to create. No IPv6 addresses on ESP32 interface. |
|
DNS Resolution Issues (getaddrinfo fails) |
getaddrinfo() returns error (e.g., EAI_NONAME , EAI_FAIL ). Client cannot resolve hostname. |
|
Firewall Blocking IPv6 or Specific Ports | IPv6 connections fail even if addresses are resolved. ICMPv6 for NDP might be blocked, preventing SLAAC. |
|
Incorrect IPV6_V6ONLY for Servers |
An IPv6 server socket does not accept connections from IPv4 clients (IPv4-mapped addresses). |
|
Client Connection Logic Flawed | Client doesn’t correctly iterate getaddrinfo() results or doesn’t fall back from IPv6 to IPv4 (or vice-versa). |
|
Using IPv4-specific APIs with IPv6 addresses (or vice-versa) | Functions like inet_aton() or structures like sockaddr_in used with IPv6 data, leading to errors or crashes. |
|
Exercises
Exercise 1: Dual-stack Time Client (SNTP)
Modify the dual-stack HTTP client example to instead implement a dual-stack SNTP (Simple Network Time Protocol) client. The client should use getaddrinfo()
to resolve a public NTP server hostname (e.g., pool.ntp.org
) and then attempt to synchronize time over both IPv4 and IPv6.
Steps:
- Include SNTP/Time Headers: Add
#include "esp_sntp.h"
and#include "lwip/apps/sntp.h"
. - Configure SNTP: Use
sntp_setservername()
with your chosen NTP server hostname. - Initialize SNTP: Call
sntp_init()
to start the SNTP client. - Wait for Time: Use a loop with
time()
andlocaltime()
to wait until the time is synchronized. - Log Protocol Used: The SNTP client in LwIP will automatically try both IPv4 and IPv6 if available. Observe the logs to see which protocol it uses for time synchronization. You might need to increase LwIP log level (
menuconfig -> Component config -> LWIP -> Log level for LWIP
) to see details. - Test: Flash the code and verify that the ESP32 successfully obtains the current time, noting whether it used IPv4 or IPv6.
Exercise 2: Dual-stack Echo Server
Implement a simple dual-stack TCP echo server on the ESP32. The server should listen on a specific port (e.g., 3333) and be capable of accepting connections from both IPv4 and IPv6 clients. It should echo back any data it receives.
Steps:
- Server Socket Setup:
- Use
getaddrinfo()
withAI_PASSIVE
flag andAF_UNSPEC
family to get addresses for binding (e.g.,NULL
fornodename
and"3333"
forservname
). - Loop through the results. For each
addrinfo
struct:- Create a socket (
socket()
). - If
AF_INET6
, setIPV6_V6ONLY
to0
to allow IPv4-mapped connections. bind()
the socket.listen()
on the socket.- Store the listening sockets in a list or array.
- Create a socket (
- Use
- Accepting Connections:
- Use
select()
to monitor all listening sockets for incoming connections. - When a connection is detected on a listening socket,
accept()
it. - Create a new task for each accepted client connection to handle echoing.
- Use
- Echo Logic: In the client handling task,
read()
data from the client andwrite()
it back. - Test:
- Flash the ESP32.
- From a computer on the same network, use
netcat
or a simple client program to connect to the ESP32’s IPv4 address and then its IPv6 address (if available) on port 3333. - Example IPv4:
nc 192.168.1.105 3333
- Example IPv6:
nc -6 2001:db8::XXXX:XXXX:XXXX:XXXX 3333
(replace with actual IPv6) - Type messages and observe them being echoed back.
Exercise 3: Dual-stack DNS Lookup Tool
Create a command-line utility on the ESP32 that allows you to enter a hostname via the serial console. The ESP32 will then perform a getaddrinfo()
lookup for that hostname and print all resolved IPv4 and IPv6 addresses, along with their address families.
Steps:
- Console Input: Use
fgets()
orscanf()
to read a hostname string fromstdin
(the serial console). - Perform Lookup: Call
getaddrinfo()
with the entered hostname,NULL
forservname
, andAF_UNSPEC
forai_family
inhints
. - Print Results: Iterate through the
addrinfo
linked list (res
). For each entry:- Print the address family (
AF_INET
orAF_INET6
). - Convert the IP address to a human-readable string using
inet_ntoa_r()
(for IPv4) orinet6_ntoa_r()
(for IPv6) and print it. - Print the canonical name if available (
rp->ai_canonname
).
- Print the address family (
- Error Handling: Handle
getaddrinfo()
errors andNULL
results gracefully. - Free Resources: Remember to call
freeaddrinfo(res)
after processing. - Test: Flash the code. Open the serial monitor. Type hostnames (e.g.,
google.com
,facebook.com
,ipv6.google.com
) and observe the resolved IP addresses.
Summary
- Dual-stack networking enables devices to simultaneously support both IPv4 and IPv6, crucial for interoperability during the ongoing transition period.
- A dual-stack host has both an IPv4 and an IPv6 address on its network interface, allowing communication with IPv4-only, IPv6-only, and other dual-stack devices.
- The
getaddrinfo()
function is the preferred and protocol-agnostic API for resolving hostnames to IP addresses, returning a list ofaddrinfo
structures that can contain both IPv4 and IPv6 addresses. - Client applications typically iterate through
getaddrinfo()
results, attempting to connect via IPv6 first and falling back to IPv4 if IPv6 connection fails, following RFC 6724 address selection rules. - LwIP’s standard socket API supports both address families (
AF_INET
for IPv4,AF_INET6
for IPv6). - For dual-stack servers, an IPv6 socket can be configured with
IPV6_V6ONLY
set to0
to accept both IPv4-mapped and native IPv6 connections. - Troubleshooting involves ensuring network IPv6 readiness, correct ESP-IDF
menuconfig
settings, proper DNS resolution, and firewall configurations.
Further Reading
- Espressif ESP-IDF Programming Guide – Sockets: https://docs.espressif.com/projects/esp-idf/en/latest/esp32/api-reference/network/sockets.html
- Espressif ESP-IDF Programming Guide – getaddrinfo: https://docs.espressif.com/projects/esp-idf/en/latest/esp32/api-reference/network/sockets.html#getaddrinfo
- Espressif ESP-IDF Examples – protocols/sockets/tcp_client (can be adapted for dual-stack): https://github.com/espressif/esp-idf/tree/master/examples/protocols/sockets/tcp_client
- RFC 6724 – Default Address Selection for IPv6: https://datatracker.ietf.org/doc/html/rfc6724
- Wikipedia – Dual-stack (networking): https://en.wikipedia.org/wiki/Dual-stack