Skip to content

Commit 2889c2a

Browse files
authored
feat(dns_server): Add DnsServer component enabling captive portal beahvior, udpate Provisioning example to use it (#594)
* feat(dns_server): Add `DnsServer` component for implementing basic captive portals * doc: update * update provisioning component / example to act as captive portal * improve API and update examples / docs * update docs * add missing sdkconfig defaults and partition for ci build * target s3 in ci by default
1 parent c43c725 commit 2889c2a

File tree

22 files changed

+607
-2
lines changed

22 files changed

+607
-2
lines changed

.github/workflows/build.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,8 @@ jobs:
6363
target: esp32
6464
- path: 'components/display_drivers/example'
6565
target: esp32
66+
- path: 'components/dns_server/example'
67+
target: esp32s3
6668
- path: 'components/drv2605/example'
6769
target: esp32
6870
- path: 'components/encoder/example'

.github/workflows/upload_components.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ jobs:
5454
components/csv
5555
components/display
5656
components/display_drivers
57+
components/dns_server
5758
components/drv2605
5859
components/encoder
5960
components/esp-box
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
idf_component_register(
2+
INCLUDE_DIRS "include"
3+
SRC_DIRS "src"
4+
REQUIRES base_component logger socket
5+
)

components/dns_server/README.md

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
# DNS Server
2+
3+
Simple DNS server component for implementing captive portals on ESP32 devices.
4+
5+
## Features
6+
7+
- Responds to all DNS queries with a configured IP address
8+
- Lightweight implementation suitable for embedded systems
9+
- Built on top of espp::UdpSocket for efficient UDP communication
10+
- Useful for captive portal implementations where all domains should resolve to the device
11+
12+
## Usage
13+
14+
```cpp
15+
#include "dns_server.hpp"
16+
17+
// Create DNS server that responds with the AP's IP
18+
espp::DnsServer::Config dns_config{
19+
.ip_address = "192.168.4.1",
20+
.log_level = espp::Logger::Verbosity::INFO
21+
};
22+
espp::DnsServer dns_server(dns_config);
23+
24+
// Start the DNS server
25+
if (dns_server.start()) {
26+
fmt::print("DNS server started\n");
27+
} else {
28+
fmt::print("Failed to start DNS server\n");
29+
}
30+
31+
// ... server runs in background ...
32+
33+
// Stop when done
34+
dns_server.stop();
35+
```
36+
37+
## How It Works
38+
39+
The DNS server listens on UDP port 53 (the standard DNS port) and responds to all A record queries with the configured IP address. This creates a "captive portal" effect where:
40+
41+
1. When a device connects to your WiFi AP, it tries to reach the internet
42+
2. All DNS queries are answered with your device's IP address
43+
3. The device's captive portal detection triggers
44+
4. The user is directed to your web interface
45+
46+
## Integration with Provisioning
47+
48+
This component is designed to work seamlessly with the `espp::Provisioning` component to create a complete captive portal experience for WiFi provisioning.
49+
50+
## API
51+
52+
### Configuration
53+
54+
- `ip_address`: The IP address to respond with for all DNS queries (typically your AP's IP)
55+
- `log_level`: Logging verbosity level
56+
57+
### Methods
58+
59+
- `start()`: Start the DNS server
60+
- `stop()`: Stop the DNS server
61+
- `is_running()`: Check if the server is currently running
62+
63+
## Limitations
64+
65+
- Only responds to A record queries (IPv4)
66+
- Does not support AAAA records (IPv6)
67+
- Minimal DNS implementation focused on captive portal use case
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
# The following lines of boilerplate have to be in your project's CMakeLists
2+
# in this exact order for cmake to work correctly
3+
cmake_minimum_required(VERSION 3.20)
4+
5+
set(ENV{IDF_COMPONENT_MANAGER} "0")
6+
include($ENV{IDF_PATH}/tools/cmake/project.cmake)
7+
8+
# add the component directories that we want to use
9+
set(EXTRA_COMPONENT_DIRS
10+
"../../../components/"
11+
)
12+
13+
set(
14+
COMPONENTS
15+
"main esptool_py nvs_flash dns_server wifi"
16+
CACHE STRING
17+
"List of components to include"
18+
)
19+
20+
project(dns_server_example)
21+
22+
set(CMAKE_CXX_STANDARD 20)
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
# DNS Server Example
2+
3+
This example demonstrates the use of the `dns_server` component to create a simple DNS server that responds to all queries with a single IP address. This is commonly used for captive portal implementations.
4+
5+
## How to use example
6+
7+
### Hardware Required
8+
9+
This example can be run on any ESP32 development board.
10+
11+
### Build and Flash
12+
13+
Build the project and flash it to the board, then run monitor tool to view serial output:
14+
15+
```
16+
idf.py -p PORT flash monitor
17+
```
18+
19+
(Replace PORT with the name of the serial port to use.)
20+
21+
## Example Output
22+
23+
```
24+
[DNS Server Example/I][0.739]: Starting DNS Server Example
25+
[DNS Server Example/I][0.739]: Starting WiFi AP: ESP-DNS-Test
26+
[DNS Server Example/I][0.889]: WiFi AP started successfully
27+
[DNS Server Example/I][0.889]: Connect to SSID: ESP-DNS-Test with password: testpassword
28+
[DNS Server Example/I][0.899]: AP IP Address: 192.168.4.1
29+
[DNS Server Example/I][0.899]: Starting DNS server on 192.168.4.1:53
30+
[DNS Server Example/I][0.909]: DNS server started successfully
31+
[DNS Server Example/I][0.909]: All DNS queries will resolve to: 192.168.4.1
32+
```
33+
34+
## Testing
35+
36+
1. Connect your phone or computer to the WiFi network "ESP-DNS-Test" with password "testpassword"
37+
2. Try to ping any domain: `ping google.com` - all domains should resolve to 192.168.4.1
38+
3. The captive portal detection should trigger automatically on most devices
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
idf_component_register(SRC_DIRS "."
2+
INCLUDE_DIRS ".")
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
#include <chrono>
2+
#include <system_error>
3+
#include <vector>
4+
5+
#include "dns_server.hpp"
6+
#include "logger.hpp"
7+
#include "wifi.hpp"
8+
9+
using namespace std::chrono_literals;
10+
11+
extern "C" void app_main(void) {
12+
espp::Logger logger({.tag = "DNS Server Example", .level = espp::Logger::Verbosity::INFO});
13+
14+
logger.info("Starting DNS Server Example");
15+
16+
#if CONFIG_ESP32_WIFI_NVS_ENABLED
17+
// Initialize NVS
18+
esp_err_t ret = nvs_flash_init();
19+
if (ret == ESP_ERR_NVS_NO_FREE_PAGES || ret == ESP_ERR_NVS_NEW_VERSION_FOUND) {
20+
ESP_ERROR_CHECK(nvs_flash_erase());
21+
ret = nvs_flash_init();
22+
}
23+
ESP_ERROR_CHECK(ret);
24+
#endif
25+
26+
// Initialize WiFi in AP mode
27+
std::string ap_ssid = "ESP-DNS-Test";
28+
std::string ap_password = "testpassword";
29+
30+
logger.info("Starting WiFi AP: {}", ap_ssid);
31+
32+
espp::WifiAp ap{espp::WifiAp::Config{
33+
.ssid = ap_ssid,
34+
.password = ap_password,
35+
.channel = 1,
36+
.max_number_of_stations = 4,
37+
}};
38+
39+
logger.info("WiFi AP started successfully");
40+
logger.info("Connect to SSID: {} with password: {}", ap_ssid, ap_password);
41+
42+
// Get the AP IP address
43+
std::string ap_ip = ap.get_ip_address();
44+
logger.info("AP IP Address: {}", ap_ip);
45+
46+
// Create and start DNS server
47+
logger.info("Starting DNS server on {}:53", ap_ip);
48+
49+
espp::DnsServer::Config dns_config{.ip_address = ap_ip,
50+
.log_level = espp::Logger::Verbosity::INFO};
51+
52+
espp::DnsServer dns_server(dns_config);
53+
std::error_code ec;
54+
if (!dns_server.start(ec)) {
55+
logger.error("Failed to start DNS server: {}", ec.message());
56+
return;
57+
}
58+
59+
logger.info("DNS server started successfully");
60+
logger.info("All DNS queries will resolve to: {}", ap_ip);
61+
logger.info("");
62+
logger.info("To test:");
63+
logger.info("1. Connect your device to WiFi network '{}'", ap_ssid);
64+
logger.info("2. Try pinging any domain (e.g., ping google.com)");
65+
logger.info("3. All domains should resolve to {}", ap_ip);
66+
67+
// Run forever
68+
while (true) {
69+
std::this_thread::sleep_for(1s);
70+
}
71+
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
# ESP-IDF Partition Table
2+
# Name, Type, SubType, Offset, Size, Flags
3+
nvs, data, nvs, 0x9000, 0x6000,
4+
phy_init, data, phy, 0xf000, 0x1000,
5+
factory, app, factory, 0x10000, 1500K,
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
# Flash size
2+
CONFIG_ESPTOOLPY_FLASHSIZE_4MB=y
3+
CONFIG_ESPTOOLPY_FLASHSIZE="4MB"
4+
5+
# CONFIG_ESP32_WIFI_NVS_ENABLED=n
6+
7+
CONFIG_PARTITION_TABLE_CUSTOM=y
8+
CONFIG_PARTITION_TABLE_CUSTOM_FILENAME="partitions.csv"
9+
CONFIG_PARTITION_TABLE_FILENAME="partitions.csv"
10+
CONFIG_PARTITION_TABLE_OFFSET=0x8000
11+
CONFIG_PARTITION_TABLE_MD5=y
12+
13+
# Common ESP-related
14+
#
15+
CONFIG_ESP_SYSTEM_EVENT_TASK_STACK_SIZE=4096
16+
CONFIG_ESP_MAIN_TASK_STACK_SIZE=8192

0 commit comments

Comments
 (0)