Chapter 83: DNS Server and DNS-SD Implementation
Chapter Objectives
By the end of this chapter, you will be able to:
- Understand the purpose and functionality of Multicast DNS (mDNS) for local hostname resolution.
- Implement mDNS on an ESP32 to make it discoverable by its hostname on the local network.
- Learn the principles of DNS-based Service Discovery (DNS-SD).
- Advertise network services running on the ESP32 using DNS-SD and mDNS.
- Understand the structure and use of SRV, PTR, and TXT DNS records in service discovery.
- Implement a basic unicast DNS server on the ESP32 in Access Point (AP) mode to redirect clients.
- Browse for services advertised by other devices on the local network (conceptual understanding).
Introduction
In the previous chapter, we explored how an ESP32 acts as a DNS client to resolve internet hostnames into IP addresses. Now, we shift our focus to scenarios where the ESP32 itself needs to be easily discoverable on a local network or even serve DNS information to other devices. This involves two key technologies: Multicast DNS (mDNS) for zero-configuration hostname resolution (e.g., my-esp32.local
) and DNS-based Service Discovery (DNS-SD) for advertising and finding network services (like a web server or a custom IoT service) without manual setup.
Imagine wanting to connect to your ESP32-powered device’s web interface without knowing its IP address. Instead, you could simply type my-esp32.local
into your browser. Or, picture an application that automatically discovers all available temperature sensors or smart lights on your network. These user-friendly experiences are made possible by mDNS and DNS-SD.
Furthermore, when an ESP32 operates in AP mode, it might need to act as a simple DNS server to guide connected clients, for instance, to a configuration web page (a common captive portal behavior). This chapter will cover the theory and practical implementation of these local DNS and service discovery mechanisms on ESP32 using ESP-IDF.
Theory
Multicast DNS (mDNS)
Multicast DNS (mDNS) provides the ability to perform DNS-like operations on a local link in the absence of any conventional unicast DNS server. It’s a zero-configuration service, meaning devices can publish and resolve hostnames without needing manual setup of DNS servers or entries.
How mDNS Works:
- Hostname Claiming: When an mDNS-enabled device joins a network, it typically wants to use a hostname ending in
.local
(e.g.,mydevice.local
). It sends out mDNS queries to see if that name is already in use. If not, it claims the name. - Queries and Responses:
- Instead of sending DNS queries to a specific unicast DNS server, mDNS clients send queries to a designated multicast address on the local network (IPv4:
224.0.0.251
, IPv6:FF02::FB
) on UDP port5353
. - All mDNS-enabled devices on that local network segment listen for these multicast queries.
- If a device is authoritative for the queried name (i.e., it’s its own hostname), it responds with a multicast mDNS message containing the corresponding IP address (A or AAAA record).
- Instead of sending DNS queries to a specific unicast DNS server, mDNS clients send queries to a designated multicast address on the local network (IPv4:
- Caching: Devices cache mDNS responses, similar to unicast DNS, but typically with shorter TTLs (Time-To-Live values).
Key Features:
Feature | Description |
---|---|
Zero Configuration | Devices can publish and resolve hostnames without manual setup of DNS servers or entries. Plug-and-play network discovery. |
Local Scope | Operates only on the local link/subnet. It does not replace or interfere with global DNS systems. |
.local Domain | Hostnames resolved via mDNS conventionally use the .local top-level domain (e.g., my-esp32.local). |
Multicast Communication | Queries and responses are sent to a designated multicast address (IPv4: 224.0.0.251, IPv6: FF02::FB) on UDP port 5353. |
Hostname Claiming | Devices check if a desired .local hostname is in use before claiming it, preventing conflicts. |
Peer-to-Peer | No central mDNS server is required; all mDNS-enabled devices participate in the protocol. |
%%{ init: { 'theme': 'base', 'themeVariables': { 'fontFamily': 'Open Sans' } } }%% graph LR subgraph "Local Area Network (LAN)" DeviceA["Device A <br> (Client)"] DeviceB["Device B <br> (mydevice.local)"] DeviceC["Device C <br> (Other Device)"] MulticastAddr((Multicast Address <br> 224.0.0.251:5353)) DeviceA --"1- mDNS Query: Who has 'mydevice.local'?"--> MulticastAddr MulticastAddr -.-> DeviceB MulticastAddr -.-> DeviceC DeviceB --"2- mDNS Response: 'mydevice.local' is at IP_B"--> MulticastAddr MulticastAddr -.-> DeviceA MulticastAddr -.-> DeviceC DeviceC --"Ignores irrelevant query/response"--> DeviceC end %% Styling classDef primary fill:#EDE9FE,stroke:#5B21B6,stroke-width:2px,color:#5B21B6; classDef process fill:#DBEAFE,stroke:#2563EB,stroke-width:1px,color:#1E40AF; classDef multicast fill:#FEF3C7,stroke:#D97706,stroke-width:1px,color:#92400E; class DeviceA,DeviceB,DeviceC primary; class MulticastAddr multicast;
Aspect | Unicast DNS | Multicast DNS (mDNS) |
---|---|---|
Primary Use | Resolving global internet hostnames (e.g., www.google.com) | Resolving hostnames on the local network link (e.g., my-esp32.local) |
Configuration | Requires configuration of DNS server IP addresses on clients (often via DHCP) | Zero-configuration; devices automatically discover each other |
Server Architecture | Hierarchical, with authoritative servers, resolvers, and root servers | Peer-to-peer; no dedicated server required |
Scope | Global | Local link/subnet only |
Communication | Client sends query directly to a configured DNS server (unicast) on UDP/TCP port 53 | Client sends query to a multicast address (224.0.0.251 or FF02::FB) on UDP port 5353 |
Hostname Suffix | Various (e.g., .com, .org, .net) | Typically .local |
Caching | Responses are cached by resolvers and clients with TTLs | Responses are cached by devices, typically with shorter TTLs |
DNS-based Service Discovery (DNS-SD)
While mDNS helps resolve hostnames to IP addresses, DNS-based Service Discovery (DNS-SD) builds upon DNS (often using mDNS as the transport on local networks) to allow devices to advertise and discover network services. For example, a device can advertise that it’s running an HTTP server, an FTP server, a printer service, or a custom IoT service.
How DNS-SD Works:
DNS-SD uses specific types of DNS records to advertise services:
- PTR (Pointer) Records for Service Type Enumeration:
- A client interested in a particular type of service (e.g., all HTTP servers) queries for PTR records associated with a service type name.
- Service type names follow the convention:
_<ApplicationProtocol>._<TransportProtocol>
(e.g.,_http._tcp
,_ftp._tcp
,_myservice._udp
). - The response is a list of PTR records, each pointing to a specific service instance name.
- Example: Query for
_http._tcp.local.
might return a PTR record pointing toMy Web Server._http._tcp.local.
.
Component | Format | Description | Example |
---|---|---|---|
Service Type Name | _ApplicationProtocol._TransportProtocol | The full string used in PTR queries to discover services of a specific type. Usually appended with .local for mDNS. | _http._tcp |
Application Protocol | Starts with an underscore (_), followed by the IANA-registered or custom protocol name. | Identifies the specific application-level protocol (e.g., HTTP, FTP, custom IoT protocol). | _http, _ftp, _myservice |
Transport Protocol | Starts with an underscore (_), followed by tcp or udp. | Specifies the transport layer protocol used by the service. | _tcp, _udp |
Service Instance Name | InstanceName._ApplicationProtocol._TransportProtocol | A user-friendly or unique name for a specific instance of a service. This is what PTR records point to. | My Web Server._http._tcp |
Domain | .local (typically for mDNS) | The domain in which the service is advertised. For mDNS, this is almost always .local. | _http._tcp.local. |
- SRV (Service) Records for Service Instance Details:
- Once a service instance name is known (from the PTR record), the client queries for its SRV record.
- The SRV record provides:
- Priority: For selecting among multiple servers (lower is better).
- Weight: For load balancing if priorities are equal (higher is better).
- Port: The TCP or UDP port number where the service is listening.
- Target Hostname: The actual hostname of the device providing the service (e.g.,
my-esp32.local.
). This hostname is then resolved to an IP address using mDNS (for A/AAAA records).
- Example: Query for
My Web Server._http._tcp.local.
might return an SRV record like:0 0 80 my-esp32.local.
. (Priority 0, Weight 0, Port 80, Target my-esp32.local).
Record Type | Purpose in DNS-SD | Example Query Name | Example Response Data |
---|---|---|---|
PTR (Pointer) | Enumerates available instances of a particular service type. Points from a generic service type to specific service instance names. | _http._tcp.local. | My Web Server._http._tcp.local. |
SRV (Service) | Provides details for a specific service instance: priority, weight, port number, and the target hostname offering the service. | My Web Server._http._tcp.local. | Priority: 0, Weight: 0, Port: 80, Target: my-esp32.local. |
TXT (Text) | Provides additional, application-specific metadata about a service instance as key-value pairs. | My Web Server._http._tcp.local. | “path=/admin”, “version=1.2”, “board=ESP32” |
A / AAAA | (Used in conjunction) Resolves the target hostname (from SRV record) to an IPv4 (A) or IPv6 (AAAA) address. Typically handled by mDNS for .local names. | my-esp32.local. | IPv4: 192.168.1.10 (A) or IPv6 address (AAAA) |
- TXT (Text) Records for Additional Information:
- Clients can also query for TXT records associated with a service instance name.
- TXT records provide additional, application-specific metadata as key-value pairs.
- Example: For a web server, TXT records might include
path=/admin
orversion=1.2
. For a printer, it might bepaper=A4
,color=true
.
DNS-SD over mDNS:
On local networks, DNS-SD typically uses mDNS as its underlying transport. This means SRV, PTR, and TXT record queries and responses are sent via multicast to port 5353. The domain used is .local
.
%%{ init: { 'theme': 'base', 'themeVariables': { 'fontFamily': 'Open Sans' } } }%% sequenceDiagram actor Client participant ESP32_Device as ESP32 (my-esp32.local <br> MyESPWebServer) Client->>+ESP32_Device: 1. mDNS Query (PTR) for <br> "_http._tcp.local." ESP32_Device-->>-Client: 2. mDNS Response (PTR): <br> "MyESPWebServer._http._tcp.local." Client->>+ESP32_Device: 3. mDNS Query (SRV) for <br> "MyESPWebServer._http._tcp.local." ESP32_Device-->>-Client: 4. mDNS Response (SRV): <br> Port: 80, Target: "my-esp32.local." <br> (Prio:0, Weight:0) Client->>+ESP32_Device: 5. mDNS Query (TXT) for <br> "MyESPWebServer._http._tcp.local." ESP32_Device-->>-Client: 6. mDNS Response (TXT): <br> "path=/index.html", "board=ESP32" Client->>+ESP32_Device: 7. mDNS Query (A/AAAA) for <br> "my-esp32.local." ESP32_Device-->>-Client: 8. mDNS Response (A/AAAA): <br> IP Address of ESP32 (e.g., 192.168.1.X) Client->>ESP32_Device: 9. HTTP Connect to ESP32_IP:80
ESP32 as a Unicast DNS Server (AP Mode / Captive Portal)
When an ESP32 operates as a Wi-Fi Access Point, it typically runs a DHCP server to assign IP addresses to connecting clients. The DHCP server also tells clients which DNS server to use. Often, this is set to the ESP32’s own IP address on the AP interface.
Scenario | ESP32 DNS Server Behavior | Client Experience | Common Use Case |
---|---|---|---|
Captive Portal / Redirect All | Responds to ALL DNS queries with its own AP IP address. | Clients attempting to access any external website are redirected to a web page hosted on the ESP32. | Initial device configuration, Wi-Fi login pages, terms of service acceptance. |
Selective Redirection | Responds with its own AP IP address for specific hostnames (e.g., esp-config.com) and potentially NXDOMAIN (Name Error) for others. | Accessing the specific hostname leads to the ESP32’s web page. Other lookups might fail or be ignored. | Providing a dedicated configuration portal accessible via a memorable name. |
DNS Forwarder (Advanced) | If ESP32 has an uplink (STA mode + AP mode), it forwards DNS queries from AP clients to an upstream DNS server (e.g., router’s DNS or public DNS). | Clients can resolve internet hostnames normally, as if connected directly to the main network. | Turning the ESP32 into a simple Wi-Fi range extender or a custom router (complex to implement fully). |
No DNS Server / Rely on mDNS | ESP32 does not run a unicast DNS server on port 53. DHCP might not provide a DNS server or provide one that’s unreachable. | Clients can only resolve .local hostnames via mDNS (if ESP32’s mDNS service is running and client supports mDNS). Internet access via DNS lookup fails. | Isolated local network where only mDNS discovery is needed. |
If the ESP32 is configured this way, all DNS queries from clients connected to its AP will be sent (via unicast UDP on port 53) to the ESP32. The ESP32 can then:
- Forward queries: If the ESP32 also has an uplink connection (e.g., Wi-Fi station mode connected to a router), it could forward these DNS queries to an upstream DNS server. This is complex and essentially turns the ESP32 into a NAT router + DNS forwarder.
- Respond authoritatively for specific names: The ESP32’s mDNS service can respond to queries for its
.local
hostname. - Act as a simple “captive portal” DNS server: For any (or specific) DNS query, it can respond with its own IP address. This forces clients trying to access any website to be redirected to a web page served by the ESP32 itself (e.g., a configuration page, terms of service, or login page).
Implementing a full DNS forwarder is beyond the scope of typical ESP32 applications. However, a simple DNS server that resolves all (or a few predefined) hostnames to the ESP32’s own IP is feasible and useful for captive portals. This requires listening for UDP packets on port 53 and crafting valid DNS responses.
%%{ init: { 'theme': 'base', 'themeVariables': { 'fontFamily': 'Open Sans' } } }%% graph TD A[Client Device] --> B{Connect to ESP32_AP WiFi}; B --> C[DHCP Request]; C --> D[ESP32 DHCP Server]; D --"Assign IP & DNS Server = ESP32_AP_IP"--> C; C --> E[Client Configured]; E --> F{"Client attempts to access<br><b>www[.]example[.]com</b>"}; F --> G["DNS Query for <b>www[.]example[.]com</b><br>sent to ESP32_AP_IP:53"]; G --> H[ESP32 Unicast DNS Server Task]; H --"Regardless of query,<br>responds with ESP32_AP_IP"--> G; G --> I["Client receives DNS Response:<br><b>www[.]example[.]com</b> is at ESP32_AP_IP"]; I --> J{"Client's browser connects to<br>ESP32_AP_IP (for <b>www[.]example[.]com</b>)"}; J --> K[ESP32 Web Server]; K --"Serves Captive Portal Page"--> J; J --> L[Client sees Captive Portal]; %% Styling classDef start 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 endo fill:#D1FAE5,stroke:#059669,stroke-width:2px,color:#065F46; classDef esp_comp fill:#E0E7FF,stroke:#3730A3,stroke-width:1.5px,color:#3730A3; class A,E,L start; class B,F,G,I,J decision; class C,H,K process; class D esp_comp;
Practical Examples
This section provides practical examples of implementing mDNS hostname resolution, DNS-SD service advertisement, and a basic unicast DNS server on the ESP32.
Prerequisites
- ESP-IDF v5.x installed and configured with VS Code.
- An ESP32 development board.
- A Wi-Fi network (for STA mode examples) or a Wi-Fi client device (for AP mode examples).
- An mDNS/DNS-SD browser tool on your computer (e.g., “Bonjour Browser” for Windows/macOS,
avahi-discover
ordns-sd
command-line tool for Linux/macOS).
Project Setup
- Create a new ESP-IDF project in VS Code (e.g.,
esp32_mdns_example
) based on theesp-idf-template
. - Ensure your ESP32 target is correctly selected.
- In
main/CMakeLists.txt
, ensuremdns
is listed as a required component if not automatically inferred: CMake# ... other configurations ... # REQUIRES mdns # Usually inferred, but can be explicit
Typically, just including"mdns.h"
is enough for the build system.
Example 1: mDNS Hostname Resolution
This example initializes Wi-Fi in Station mode, connects to an AP, and then starts the mDNS service, making the ESP32 discoverable by a hostname like my-esp32.local
.
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 "mdns.h"
#include "esp_netif.h"
// 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
// mDNS Configuration
#define EXAMPLE_MDNS_HOSTNAME "my-esp32" // Hostname will be "my-esp32.local"
#define EXAMPLE_MDNS_INSTANCE "My ESP32 Device"
static EventGroupHandle_t s_wifi_event_group;
#define WIFI_CONNECTED_BIT BIT0
#define WIFI_FAIL_BIT BIT1
static const char *TAG = "mdns_example";
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,
},
};
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.");
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 start_mdns_service(void)
{
// Initialize mDNS service
esp_err_t err = mdns_init();
if (err) {
ESP_LOGE(TAG, "mDNS Init failed: %d", err);
return;
}
ESP_LOGI(TAG, "mDNS Initalized.");
// Set hostname
ESP_ERROR_CHECK(mdns_hostname_set(EXAMPLE_MDNS_HOSTNAME));
ESP_LOGI(TAG, "mDNS hostname set to: [%s.local]", EXAMPLE_MDNS_HOSTNAME);
// Set default instance name
ESP_ERROR_CHECK(mdns_instance_name_set(EXAMPLE_MDNS_INSTANCE));
ESP_LOGI(TAG, "mDNS instance name set to: [%s]", EXAMPLE_MDNS_INSTANCE);
// Structure with TXT records - Can be empty if not advertising specific service attributes here
// mdns_txt_item_t serviceTxtData[1] = {
// {"board", "ESP32"}
// };
// // Initialize service advertising (optional here, more relevant for DNS-SD)
// ESP_ERROR_CHECK(mdns_service_add("ESP32-WebServer", "_http", "_tcp", 80, serviceTxtData, 1));
// ESP_LOGI(TAG, "mDNS service _http._tcp added (port 80).");
// ESP_LOGI(TAG, "You can now ping %s.local or browse for its services.", EXAMPLE_MDNS_HOSTNAME);
}
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);
wifi_init_sta(); // Connect to Wi-Fi
// Wait until Wi-Fi is connected before starting mDNS
EventBits_t bits = xEventGroupWaitBits(s_wifi_event_group, WIFI_CONNECTED_BIT, pdFALSE, pdTRUE, portMAX_DELAY);
if (bits & WIFI_CONNECTED_BIT) {
start_mdns_service();
} else {
ESP_LOGE(TAG, "Wi-Fi connection failed. mDNS not started.");
}
}
Explanation:
%%{ init: { 'theme': 'base', 'themeVariables': { 'fontFamily': 'Open Sans' } } }%% graph TD Start((Start: After Wi-Fi Connected)) --> A["Call <i>mdns_init()</i>"]; A --> B{mDNS Init Success?}; B --No--> C["Log Error: <b>mDNS Init failed</b>"]; C --> EndFail((End: Init Failed)); B --Yes--> D["Log: <b>mDNS Initialized</b>"]; D --> E["Call <i>mdns_hostname_set(<b>my-esp32</b>)</i>"]; E --> F{Hostname Set Success?}; F --No--> G[Log Error/ESP_ERROR_CHECK asserts]; G --> EndFail; F --Yes--> H["Log: <b>Hostname set to my-esp32.local</b>"]; H --> I["Call <i>mdns_instance_name_set(<b>My ESP32 Device</b>)</i>"]; I --> J{Instance Name Set Success?}; J --No--> K[Log Error/ESP_ERROR_CHECK asserts]; K --> EndFail; J --Yes--> L["Log: <b>Instance name set</b>"]; L --> M["mDNS service now active: <br> Responds to <b>my-esp32.local</b> queries <br> Probes network for name conflicts"]; M --> EndSuccess((End: mDNS Active)); %% Styling classDef startNode fill:#EDE9FE,stroke:#5B21B6,stroke-width:2px,color:#5B21B6; classDef processNode fill:#DBEAFE,stroke:#2563EB,stroke-width:1px,color:#1E40AF; classDef decisionNode fill:#FEF3C7,stroke:#D97706,stroke-width:1px,color:#92400E; classDef endSuccessNode fill:#D1FAE5,stroke:#059669,stroke-width:2px,color:#065F46; classDef endFailNode fill:#FEE2E2,stroke:#DC2626,stroke-width:1px,color:#991B1B; class Start,M startNode; class A,D,E,H,I,L processNode; class B,F,J decisionNode; class C,G,K processNode; class EndSuccess endSuccessNode; class EndFail endFailNode;
- Wi-Fi Setup: Standard Wi-Fi station mode initialization.
start_mdns_service()
:mdns_init()
: Initializes the mDNS service. This should be called once.mdns_hostname_set(EXAMPLE_MDNS_HOSTNAME)
: Sets the hostname for the ESP32. The mDNS service will automatically append.local
. So, ifEXAMPLE_MDNS_HOSTNAME
is “my-esp32”, it will be resolvable asmy-esp32.local
.mdns_instance_name_set(EXAMPLE_MDNS_INSTANCE)
: Sets a default human-readable instance name for the device. This name can be used by services if not overridden.
app_main()
: Initializes NVS, Wi-Fi, and then callsstart_mdns_service()
after Wi-Fi connection is established.
Build, Flash, and Observe:
- Replace Wi-Fi credentials.
- Build and flash the project.
- Open the serial monitor. You should see logs indicating mDNS initialization and hostname setting.
- From another computer or smartphone connected to the same local network:
- Try pinging
my-esp32.local
(or whatever hostname you set). You should get replies from the ESP32’s IP address. - Use an mDNS browser tool. You should see “my-esp32.local” listed.
- Try pinging
Tip: If you are in AP mode,
mdns_init()
should be called after the AP interface is up and has an IP. The mDNS service will then work on the AP’s network.
Example 2: Advertising a Service using DNS-SD (HTTP Server)
This example builds upon the previous one. Assume the ESP32 is running a web server on port 80. We will advertise this service using DNS-SD.
Modify start_mdns_service()
in main.c
(add the service part):
// ... (previous includes and Wi-Fi code) ...
// In start_mdns_service() function:
void start_mdns_service(void)
{
// Initialize mDNS service
esp_err_t err = mdns_init();
if (err) {
ESP_LOGE(TAG, "mDNS Init failed: %d", err);
return;
}
ESP_LOGI(TAG, "mDNS Initalized.");
// Set hostname
ESP_ERROR_CHECK(mdns_hostname_set(EXAMPLE_MDNS_HOSTNAME));
ESP_LOGI(TAG, "mDNS hostname set to: [%s.local]", EXAMPLE_MDNS_HOSTNAME);
// Set default instance name
ESP_ERROR_CHECK(mdns_instance_name_set(EXAMPLE_MDNS_INSTANCE));
ESP_LOGI(TAG, "mDNS instance name set to: [%s]", EXAMPLE_MDNS_INSTANCE);
// Define TXT records for the service
mdns_txt_item_t serviceTxtData[3] = {
{"board", "ESP32"},
{"version", "1.0.0"},
{"path", "/"} // Example: default path for the web server
};
// Add the HTTP service
// Service name: "esp-web-server" (will appear as "esp-web-server._http._tcp.local")
// Protocol: "_http"
// Transport: "_tcp"
// Port: 80
ESP_ERROR_CHECK(mdns_service_add("esp-web-server", "_http", "_tcp", 80, serviceTxtData, 3));
ESP_LOGI(TAG, "mDNS service _http._tcp added with instance name 'esp-web-server' on port 80.");
ESP_LOGI(TAG, "Browse for HTTP services on your network or try accessing http://%s.local/", EXAMPLE_MDNS_HOSTNAME);
// You can add more services, e.g., an FTP server
// ESP_ERROR_CHECK(mdns_service_add("esp-ftp-server", "_ftp", "_tcp", 21, NULL, 0));
}
// ... (app_main and Wi-Fi init remain the same) ...
Explanation:
mdns_txt_item_t serviceTxtData[]
: An array to define TXT record key-value pairs.mdns_service_add("esp-web-server", "_http", "_tcp", 80, serviceTxtData, 3)
:"esp-web-server"
: This is the instance name of your service. Users might see this in a service browser."_http"
: The application protocol part of the service type."_tcp"
: The transport protocol part of the service type.80
: The port number the HTTP server is listening on.serviceTxtData
: Pointer to the array of TXT records.3
: The number of items in theserviceTxtData
array.
Build, Flash, and Observe:
- Build and flash.
- Open the serial monitor.
- Use an mDNS/DNS-SD browser tool on your computer (e.g., Bonjour Browser, Avahi Discovery).
- Look for services of type “HTTP Server” (or
_http._tcp
). - You should find an instance named “esp-web-server”.
- Inspecting it should show its target hostname (
my-esp32.local
), port (80), and the TXT records (“board=ESP32”, “version=1.0.0”, “path=/”).
- Look for services of type “HTTP Server” (or
Example 3: Basic Unicast DNS Server for AP Mode (Captive Portal Aid)
This example sets up the ESP32 in AP mode. The DHCP server on the AP will assign the ESP32’s AP IP as the DNS server for clients. A simple task will listen on UDP port 53 and respond to any A record query with the ESP32’s AP IP address.
This is a more advanced example requiring careful handling of network packets.
// Add these includes
#include "lwip/sockets.h" // For socket programming
#include "lwip/dns.h" // For DNS header structures (may need to define them minimally if not fully exposed)
// AP Configuration
#define AP_ESP_WIFI_SSID "ESP32_Config_AP"
#define AP_ESP_WIFI_PASS "esp32config"
#define AP_ESP_WIFI_CHANNEL 1
#define AP_MAX_STA_CONN 4
// AP static IP (also used as DNS server IP for clients)
#define AP_IP_ADDR "192.168.4.1"
#define AP_NETMASK_ADDR "255.255.255.0"
#define AP_GW_ADDR "192.168.4.1"
#define DNS_PORT 53
#define DNS_MAX_LEN 512 // Max DNS packet length
// Simplified DNS Header Structure
typedef struct {
uint16_t id; // Identification
uint16_t flags; // Flags
uint16_t qdcount; // Question count
uint16_t ancount; // Answer count
uint16_t nscount; // Authority RR count
uint16_t arcount; // Additional RR count
} dns_header_t;
// Simplified DNS Question Structure (QNAME is variable)
// For this example, we don't parse QNAME deeply, just respond to any A query
static const char *TAG_AP_DNS = "ap_dns_server";
static esp_netif_t *ap_netif_g = NULL; // Global to store AP netif for IP
void dns_server_task(void *pvParameters) {
uint8_t rx_buffer[DNS_MAX_LEN];
struct sockaddr_in client_addr;
socklen_t client_addr_len = sizeof(client_addr);
int sock_fd;
struct sockaddr_in server_addr;
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(DNS_PORT);
server_addr.sin_addr.s_addr = htonl(INADDR_ANY); // Listen on AP interface IP
sock_fd = socket(AF_INET, SOCK_DGRAM, IPPROTO_IP);
if (sock_fd < 0) {
ESP_LOGE(TAG_AP_DNS, "Failed to create DNS socket");
vTaskDelete(NULL);
return;
}
if (bind(sock_fd, (struct sockaddr *)&server_addr, sizeof(server_addr)) < 0) {
ESP_LOGE(TAG_AP_DNS, "Failed to bind DNS socket: %s", strerror(errno));
close(sock_fd);
vTaskDelete(NULL);
return;
}
ESP_LOGI(TAG_AP_DNS, "DNS server listening on port %d", DNS_PORT);
esp_netif_ip_info_t ip_info_ap;
ESP_ERROR_CHECK(esp_netif_get_ip_info(ap_netif_g, &ip_info_ap)); // Get AP's current IP
while (1) {
int len = recvfrom(sock_fd, rx_buffer, sizeof(rx_buffer), 0, (struct sockaddr *)&client_addr, &client_addr_len);
if (len < 0) {
ESP_LOGE(TAG_AP_DNS, "DNS recvfrom failed: %s", strerror(errno));
continue;
}
if (len < sizeof(dns_header_t)) {
ESP_LOGW(TAG_AP_DNS, "Received too short DNS packet");
continue;
}
dns_header_t *dns_hdr = (dns_header_t *)rx_buffer;
// Basic check: is it a standard query? (QR=0, Opcode=0)
// We only care about the question count being > 0
if ((ntohs(dns_hdr->flags) & 0x8000) == 0 && dns_hdr->qdcount > 0) { // It's a query
ESP_LOGI(TAG_AP_DNS, "Received DNS query (ID: 0x%X) from %s", ntohs(dns_hdr->id), inet_ntoa(client_addr.sin_addr));
// Prepare response: copy query ID, set flags for response
dns_hdr->flags = htons(0x8180); // Standard response, No error, Authoritative Answer
dns_hdr->ancount = dns_hdr->qdcount; // Respond with one answer per question (simplified)
// For simplicity, we assume only one question and it's an A record query.
// A robust server would parse QTYPE.
// Construct the answer part (directly after the question part)
// Question part ends after QNAME (variable), QTYPE (2), QCLASS (2)
// Find end of question section to append answer
uint8_t *p_qname_start = rx_buffer + sizeof(dns_header_t);
uint8_t *p_qname_end = p_qname_start;
while (*p_qname_end != 0 && (p_qname_end < rx_buffer + len)) { // Find end of QNAME
p_qname_end++;
}
if (*p_qname_end != 0) { // Malformed QNAME
ESP_LOGW(TAG_AP_DNS, "Malformed QNAME in DNS query");
continue;
}
p_qname_end++; // Skip the null terminator of QNAME
uint8_t *p_answer_start = p_qname_end + 4; // Skip QTYPE (2 bytes) and QCLASS (2 bytes)
// Check if there's enough space for our simple answer
// Answer: Name (2, pointer), Type (2), Class (2), TTL (4), RDLENGTH (2), RDATA (4 for IPv4) = 16 bytes
if ((p_answer_start + 16) > (rx_buffer + DNS_MAX_LEN)) {
ESP_LOGW(TAG_AP_DNS, "Not enough buffer space for DNS answer");
continue;
}
// Answer:
// Name (pointer to the QNAME in the question section, 0xC00C assumes QNAME starts at offset 12)
*p_answer_start++ = 0xC0;
*p_answer_start++ = 0x0C;
// Type A (1)
*p_answer_start++ = 0x00;
*p_answer_start++ = 0x01;
// Class IN (1)
*p_answer_start++ = 0x00;
*p_answer_start++ = 0x01;
// TTL (e.g., 60 seconds)
uint32_t ttl = htonl(60);
memcpy(p_answer_start, &ttl, sizeof(ttl));
p_answer_start += sizeof(ttl);
// RDLENGTH (4 for IPv4)
*p_answer_start++ = 0x00;
*p_answer_start++ = 0x04;
// RDATA (AP's IP address)
memcpy(p_answer_start, &ip_info_ap.ip.addr, sizeof(ip_info_ap.ip.addr));
p_answer_start += sizeof(ip_info_ap.ip.addr);
int response_len = p_answer_start - rx_buffer;
sendto(sock_fd, rx_buffer, response_len, 0, (struct sockaddr *)&client_addr, client_addr_len);
ESP_LOGI(TAG_AP_DNS, "Sent DNS response to %s with IP " IPSTR, inet_ntoa(client_addr.sin_addr), IP2STR(&ip_info_ap.ip));
}
}
close(sock_fd);
vTaskDelete(NULL);
}
void wifi_init_softap_with_dns(void) {
ESP_ERROR_CHECK(esp_netif_init());
ESP_ERROR_CHECK(esp_event_loop_create_default());
ap_netif_g = esp_netif_create_default_wifi_ap(); // Store ap_netif globally
assert(ap_netif_g);
// Configure static IP for AP
esp_netif_ip_info_t ip_info;
inet_pton(AF_INET, AP_IP_ADDR, &ip_info.ip);
inet_pton(AF_INET, AP_GW_ADDR, &ip_info.gw);
inet_pton(AF_INET, AP_NETMASK_ADDR, &ip_info.netmask);
ESP_ERROR_CHECK(esp_netif_dhcps_stop(ap_netif_g)); // Stop DHCP server before setting IP
ESP_ERROR_CHECK(esp_netif_set_ip_info(ap_netif_g, &ip_info));
ESP_LOGI(TAG_AP_DNS, "AP Static IP configured: " IPSTR, IP2STR(&ip_info.ip));
// Configure DHCP server to give AP's IP as DNS server
esp_netif_dns_info_t dns_server_info;
inet_pton(AF_INET, AP_IP_ADDR, &dns_server_info.ip.u_addr.ip4); // Set DNS server to AP's own IP
dns_server_info.ip.type = ESP_IPADDR_TYPE_V4;
dhcps_offer_t dhcps_dns_opt = OFFER_DNS; // Offer DNS server option
ESP_ERROR_CHECK(esp_netif_dhcps_option(ap_netif_g, ESP_NETIF_OP_SET, ESP_NETIF_DOMAIN_NAME_SERVER, &dns_server_info, sizeof(dns_server_info)));
ESP_ERROR_CHECK(esp_netif_dhcps_option(ap_netif_g, ESP_NETIF_OP_SET, ESP_NETIF_DHCP_OPTION_OFFER_DNS, &dhcps_dns_opt, sizeof(dhcps_dns_opt)));
ESP_LOGI(TAG_AP_DNS, "DHCP server configured to offer AP IP as DNS.");
ESP_ERROR_CHECK(esp_netif_dhcps_start(ap_netif_g)); // Start DHCP server
ESP_LOGI(TAG_AP_DNS, "DHCP server started.");
wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT();
ESP_ERROR_CHECK(esp_wifi_init(&cfg));
// No specific Wi-Fi event handler needed for this simple DNS server example,
// but you'd have one for AP client connections in a real app.
wifi_config_t wifi_ap_config = {
.ap = {
.ssid = AP_ESP_WIFI_SSID,
.ssid_len = strlen(AP_ESP_WIFI_SSID),
.password = AP_ESP_WIFI_PASS,
.max_connection = AP_MAX_STA_CONN,
.authmode = WIFI_AUTH_WPA2_PSK,
.channel = AP_ESP_WIFI_CHANNEL,
},
};
if (strlen(AP_ESP_WIFI_PASS) == 0) {
wifi_ap_config.ap.authmode = WIFI_AUTH_OPEN;
}
ESP_ERROR_CHECK(esp_wifi_set_mode(WIFI_MODE_AP));
ESP_ERROR_CHECK(esp_wifi_set_config(WIFI_IF_AP, &wifi_ap_config));
ESP_ERROR_CHECK(esp_wifi_start());
ESP_LOGI(TAG_AP_DNS, "SoftAP started. SSID: %s Password: %s", AP_ESP_WIFI_SSID, AP_ESP_WIFI_PASS);
// Start the DNS server task
xTaskCreate(dns_server_task, "dns_server_task", 4096, NULL, 5, NULL);
}
// In app_main:
// void app_main(void) {
// // NVS Init
// // ...
// wifi_init_softap_with_dns();
// // Optionally, start mDNS as well if you want .local resolution on the AP network
// // start_mdns_service(); // Ensure mDNS operates on ap_netif_g if called here
// }
Note on Example 3: This is a very simplified DNS server. It doesn’t fully parse QTYPE and assumes any query is for an A record. A production captive portal DNS server might be more robust. The esp_netif_dhcps_option
for ESP_NETIF_DOMAIN_NAME_SERVER
sets the DNS server IP that the DHCP server offers to clients. The dns_server_task
then implements that server.
Build, Flash, and Observe (Example 3):
- In
app_main
, callwifi_init_softap_with_dns()
. - Build and flash.
- Connect a client device (laptop/phone) to the “ESP32_Config_AP” Wi-Fi network.
- The client should get an IP from the 192.168.4.x range, and its DNS server should be 192.168.4.1.
- On the client, try opening any website (e.g.,
www.google.com
). The DNS query will go to the ESP32. The ESP32’s DNS server task should respond with192.168.4.1
. - If the ESP32 also runs a web server on
192.168.4.1:80
, the client’s browser will attempt to load a page from the ESP32, effectively redirecting them.
Variant Notes
ESP32 Variant | Wi-Fi | Ethernet (Built-in MAC) | mDNS/DNS-SD (IP-based) Support | Unicast DNS Server (IP-based) Support |
---|---|---|---|---|
ESP32 (Classic) | Yes | Yes (requires external PHY) | Yes | Yes |
ESP32-S2 | Yes | No | Yes (via Wi-Fi) | Yes (via Wi-Fi) |
ESP32-S3 | Yes | No (some modules might add Ethernet via SPI) | Yes (via Wi-Fi) | Yes (via Wi-Fi) |
ESP32-C3 | Yes (Wi-Fi 4) | No | Yes (via Wi-Fi) | Yes (via Wi-Fi) |
ESP32-C6 | Yes (Wi-Fi 6) | No | Yes (via Wi-Fi) | Yes (via Wi-Fi) |
ESP32-H2 | No (IEEE 802.15.4 only) | No | Via Thread (uses different mechanisms like SRP, not covered in this chapter’s Wi-Fi examples) | N/A for IP-based DNS server in typical Thread usage. |
Common Mistakes & Troubleshooting Tips
Mistake / Issue | Symptom(s) | Troubleshooting / Solution |
---|---|---|
mDNS Not Working (.local hostname unreachable) | Cannot ping my-esp32.local. Device not appearing in mDNS browsers. ping: “Unknown host”. |
1. Initialization Order: Ensure mdns_init() is called after Wi-Fi/network interface is initialized and has an IP.
2. Hostname Set: Verify mdns_hostname_set() is called after mdns_init(). 3. Network Connection: Confirm ESP32 is connected to the same local network as the querying device. 4. Firewall: Check PC/router firewall isn’t blocking UDP port 5353. 5. ESP-IDF Logs: Look for errors from the “mdns” component in the serial monitor. |
Service Not Appearing in DNS-SD Browser | Custom service (e.g., _http._tcp) not listed in tools like Bonjour Browser or Avahi. Hostname might be visible, but not the specific service. |
1. mdns_service_add() Call: Ensure mdns_service_add() is called correctly after mdns_init().
2. Service Parameters: • Service Type: Correct format (e.g., “_http”, not “http”). • Protocol: Correct format (e.g., “_tcp” or “_udp”). • Port: Matches the actual port your service is listening on. 3. TXT Records: If using TXT records, ensure the mdns_txt_item_t array is correctly formatted and the count is accurate. 4. Return Value: Check the return value of mdns_service_add() for errors (ESP_OK means success). 5. Instance Name: A unique and valid instance name helps. |
Unicast DNS Server Issues (Example 3 – Captive Portal) | Clients connected to ESP32 AP cannot resolve any hostnames, or redirection to ESP32’s IP doesn’t work. Browser shows “Server not found”. |
1. DHCP DNS Setting: Verify the ESP32’s DHCP server (in AP mode) is correctly assigning the ESP32’s AP IP address as the DNS server to clients. Use esp_netif_dhcps_option() with ESP_NETIF_DOMAIN_NAME_SERVER.
2. Socket Operations: • Socket Create/Bind: Ensure UDP socket on port 53 is created and bound to INADDR_ANY or the AP’s IP without errors. Check logs for “Failed to bind DNS socket”. • recvfrom/sendto: Check for errors during packet reception/transmission. 3. DNS Packet Crafting: • Header Flags: Ensure response flags (QR, AA, RCODE) are correctly set. For a simple redirect, QR=1, AA=1, RCODE=0. • Answer Section: IP address in RDATA must be in network byte order and match the ESP32’s AP IP. Name pointer (0xC00C) must be correct. 4. Wireshark: Use Wireshark on a client to inspect DNS queries sent to the ESP32 and the responses (if any) from it. |
Resource Issues (Crashes, Instability, WDT) | ESP32 crashes, watchdog timer (WDT) resets, or behaves erratically when mDNS or DNS server tasks are running. |
1. Task Stack Size: The mDNS service and custom DNS server tasks require sufficient stack. Default might be too small. Increase stack size in xTaskCreate() (e.g., to 4096 or more).
2. Memory Usage: Advertising many services or very large TXT records consumes RAM. Monitor free heap (esp_get_free_heap_size()). 3. Blocking Calls: Ensure no long-blocking operations in the mDNS event handler or DNS server task that could starve other tasks or trigger WDT. |
Using .local with Unicast DNS | Trying to resolve my-device.local through a public DNS server like 8.8.8.8. |
1. mDNS Specific: .local hostnames are specifically for mDNS on the local network link.
2. Client OS Behavior: Most modern OSes will automatically use mDNS for .local queries if an mDNS client (like Bonjour, Avahi) is running. 3. No Global Resolution: Standard unicast DNS servers on the internet do not resolve .local names. |
Exercises
- Custom Service Advertisement:
- Define and advertise a custom service type using DNS-SD, for example,
_mytempsensor._tcp
for a fictional temperature sensor. - Include TXT records like “location=livingroom”, “unit=celsius”.
- Use an mDNS/DNS-SD browser tool to verify that your custom service is advertised correctly.
- Define and advertise a custom service type using DNS-SD, for example,
- Modify Unicast DNS Server Response:
- In Example 3 (Unicast DNS Server), modify the
dns_server_task
so that it only responds with the ESP32’s IP if the queried hostname is, for example,config.setup
oresp.config
. For all other hostnames, make it respond with a “Name Error” (RCODE=3 in DNS header flags). - Test by trying to resolve
config.setup
andwww.randomsite.com
from a client connected to the ESP32’s AP.
- In Example 3 (Unicast DNS Server), modify the
- Service Browsing (Conceptual Outline):
- Research the ESP-IDF mDNS functions for querying services:
mdns_query_ptr()
,mdns_query_srv()
,mdns_query_txt()
, andmdns_query_results_free()
. - Write pseudocode or a high-level plan for a task on the ESP32 that would:
- Periodically query for PTR records of
_http._tcp.local.
. - For each discovered service instance, query for its SRV and TXT records.
- Log the discovered service name, hostname, IP address (by resolving the SRV target), port, and TXT data.
- Periodically query for PTR records of
- (Advanced Challenge: Implement this task.)
- Research the ESP-IDF mDNS functions for querying services:
Summary
- mDNS (Multicast DNS) enables zero-configuration resolution of
.local
hostnames on the local network using UDP multicast on port 5353. - The ESP-IDF
mdns
component allows an ESP32 to be discovered viamdns_hostname_set()
. - DNS-SD (DNS-based Service Discovery) uses DNS records (PTR, SRV, TXT), often over mDNS, to advertise and discover network services.
mdns_service_add()
is used to advertise services with specific types, ports, and metadata.- An ESP32 in AP mode can act as a simple unicast DNS server by listening on UDP port 53 and responding to queries, often to redirect clients to its own IP for captive portal functionality.
- These features are crucial for creating user-friendly, discoverable IoT devices.
- Functionality is generally consistent across ESP32 variants with network capabilities.
Further Reading
- ESP-IDF mDNS Documentation:
- Official RFCs:
- DNS and mDNS Tools:
- Wireshark: Invaluable for inspecting mDNS and DNS packets.
- Bonjour Browser (Windows/macOS): GUI tool for browsing mDNS/DNS-SD services.
- Avahi (Linux): Provides tools like
avahi-browse
andavahi-discover
. - dns-sd (macOS command line):
dns-sd -B _http._tcp
(browse) ordns-sd -R "My Web" _http._tcp . 80 path=/index.html
(register).
- LwIP Documentation: (For deeper understanding of the underlying TCP/IP stack)
Useful Tools:
Tool | Platform(s) | Description | Example Usage |
---|---|---|---|
Wireshark | Windows, macOS, Linux | Powerful network protocol analyzer. Can capture and dissect mDNS (UDP port 5353) and DNS (UDP/TCP port 53) packets. | Filter: udp.port == 5353 or dns |
Bonjour Browser | Windows, macOS (various third-party versions) | GUI tool for browsing services advertised via mDNS/DNS-SD on the local network. | Launch and see listed services by type. |
Avahi Tools (avahi-browse, avahi-discover) | Linux | Command-line and GUI tools for discovering and browsing mDNS/DNS-SD services. Part of the Avahi mDNS/DNS-SD implementation. | avahi-browse -a -r (browse all services and resolve) |
dns-sd command | macOS (built-in), can be installed on Linux | Command-line tool for registering, browsing, and resolving DNS-SD services. | Browse: dns-sd -B _http._tcp Register: dns-sd -R “My Web” _http._tcp . 80 path=/index.html |
ping | Windows, macOS, Linux | Standard command-line tool to test reachability of a host. Can be used with .local hostnames. | ping my-esp32.local |
NetAnalyzer Lite / Discovery – DNS-SD Browser (Mobile Apps) | iOS, Android | Various mobile apps exist that can browse mDNS/DNS-SD services on the Wi-Fi network. | Search app stores for “Bonjour browser” or “DNS-SD browser”. |