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:

C
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 an addrinfo structure that provides criteria for the lookup. Key fields in hints:
    • 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 as AI_PASSIVE (for server-side bind), AI_CANONNAME.
  • res: On successful return, this points to a linked list of addrinfo 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:

  1. Iterate through getaddrinfo results: Loop through the linked list of addrinfo structures returned by getaddrinfo().
  2. Try IPv6 first: Attempt to create a socket and connect using the AF_INET6 addresses.
  3. 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 the domain parameter in socket().
  • To create an IPv6 socket, use AF_INET6 for the domain parameter in socket().

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

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.

Plaintext
# 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:

Plaintext
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

  1. Open VS Code: Open your dual_stack_http_client project folder in VS Code.
  2. Configure with menuconfig:
    • Press Ctrl+Shift+P (or Cmd+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, ensure Enable IPv6 Stateless Address Autoconfiguration (SLAAC) is checked.
    • Save the configuration and exit menuconfig.
  3. Build the Project:
    • Open the VS Code Terminal (Terminal > New Terminal).
    • Run idf.py build. This will compile the project.

6. Run/Flash/Observe Steps

  1. Connect Hardware:
    • Connect your ESP32 development board to your computer via USB.
  2. Flash the Firmware:
    • In the VS Code Terminal, run idf.py flash. This will erase the chip and flash your compiled firmware.
  3. Monitor Serial Output:
    • After flashing, run idf.py monitor. This will open the serial monitor and display the logs from your ESP32.

Expected Output (connecting to example.com on an IPv6-enabled network):

Plaintext
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:

Plaintext
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:

  1. 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.
  2. 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.
  • Verify router/ISP provides IPv6. Check router admin panel for IPv6 settings (RA enabled).
  • Test network with test-ipv6.com from a PC.
  • Client will fall back to IPv4 if implemented correctly.
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.
  • Run idf.py menuconfig.
  • Go to Component config -> LWIP. Ensure Enable IPv6 support and Enable IPv6 Stateless Address Autoconfiguration (SLAAC) are checked.
DNS Resolution Issues (getaddrinfo fails) getaddrinfo() returns error (e.g., EAI_NONAME, EAI_FAIL). Client cannot resolve hostname.
  • Ensure ESP32 has valid DNS servers (via DHCP/SLAAC RDNSS/Static).
  • Ping known IP addresses (IPv4: 8.8.8.8, IPv6: 2001:4860:4860::8888) to test basic connectivity.
  • Check hostname spelling. Try resolving the same hostname from a PC on the same network.
Firewall Blocking IPv6 or Specific Ports IPv6 connections fail even if addresses are resolved. ICMPv6 for NDP might be blocked, preventing SLAAC.
  • Temporarily disable firewalls (router/PC) for testing.
  • Ensure firewall allows ICMPv6 (for NDP) and the required application ports for both IPv4 and IPv6.
Incorrect IPV6_V6ONLY for Servers An IPv6 server socket does not accept connections from IPv4 clients (IPv4-mapped addresses).
  • For a dual-stack server on an IPv6 socket, explicitly set IPV6_V6ONLY socket option to 0 (false) before binding.
  • setsockopt(s, IPPROTO_IPV6, IPV6_V6ONLY, &optval_zero, sizeof(optval_zero));
Client Connection Logic Flawed Client doesn’t correctly iterate getaddrinfo() results or doesn’t fall back from IPv6 to IPv4 (or vice-versa).
  • Ensure client code loops through all addrinfo entries.
  • Implement a clear strategy (e.g., try all IPv6 then all IPv4, or interleave based on RFC 6724 preferences if more advanced).
  • Properly close sockets on failed connection attempts before trying the next address.
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.
  • Use protocol-agnostic functions like getaddrinfo(), inet_ntop(), inet_pton().
  • Use the correct socket address structure (sockaddr_in for IPv4, sockaddr_in6 for IPv6, or sockaddr_storage for generic storage). Check ai_family from getaddrinfo().

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:

  1. Include SNTP/Time Headers: Add #include "esp_sntp.h" and #include "lwip/apps/sntp.h".
  2. Configure SNTP: Use sntp_setservername() with your chosen NTP server hostname.
  3. Initialize SNTP: Call sntp_init() to start the SNTP client.
  4. Wait for Time: Use a loop with time() and localtime() to wait until the time is synchronized.
  5. 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.
  6. 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:

  1. Server Socket Setup:
    • Use getaddrinfo() with AI_PASSIVE flag and AF_UNSPEC family to get addresses for binding (e.g., NULL for nodename and "3333" for servname).
    • Loop through the results. For each addrinfo struct:
      • Create a socket (socket()).
      • If AF_INET6, set IPV6_V6ONLY to 0 to allow IPv4-mapped connections.
      • bind() the socket.
      • listen() on the socket.
      • Store the listening sockets in a list or array.
  2. 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.
  3. Echo Logic: In the client handling task, read() data from the client and write() it back.
  4. 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:

  1. Console Input: Use fgets() or scanf() to read a hostname string from stdin (the serial console).
  2. Perform Lookup: Call getaddrinfo() with the entered hostname, NULL for servname, and AF_UNSPEC for ai_family in hints.
  3. Print Results: Iterate through the addrinfo linked list (res). For each entry:
    • Print the address family (AF_INET or AF_INET6).
    • Convert the IP address to a human-readable string using inet_ntoa_r() (for IPv4) or inet6_ntoa_r() (for IPv6) and print it.
    • Print the canonical name if available (rp->ai_canonname).
  4. Error Handling: Handle getaddrinfo() errors and NULL results gracefully.
  5. Free Resources: Remember to call freeaddrinfo(res) after processing.
  6. 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 of addrinfo 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 to 0 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

Leave a Comment

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

Scroll to Top