End the web_server captive DNS when the provisioning window closes and report a DNS start failure

This commit is contained in:
J. Nick Koston
2026-09-30 22:21:02 +02:00
parent c9cc901887
commit 79eb26ac6f
6 changed files with 34 additions and 16 deletions
+5 -2
View File
@@ -444,9 +444,12 @@ void WebServer::start_captive() {
if (this->dns_.is_running())
return;
network::IPAddress ip = wifi::global_wifi_component->wifi_soft_ap_ip();
this->dns_.start(ip);
this->enable_loop();
char ip_buf[network::IP_ADDRESS_BUFFER_SIZE];
if (!this->dns_.start(ip)) {
ESP_LOGW(TAG, "AP mode: captive DNS failed to start; open http://%s/ manually", ip.str_to(ip_buf));
return;
}
this->enable_loop();
ESP_LOGI(TAG, "AP mode: serving the web interface as captive portal at http://%s/", ip.str_to(ip_buf));
}
@@ -20,16 +20,21 @@ namespace esphome::web_server_base {
// The server object only exists while running, so an idle owner (AP not up) pays one pointer.
class CaptiveDNS {
public:
void start(const network::IPAddress &ip) {
/// Returns false when the DNS server could not start; nothing is kept, so a later start retries.
bool start(const network::IPAddress &ip) {
if (this->dns_server_ != nullptr)
return;
this->dns_server_ = make_unique<DNSServer>();
return true;
auto server = make_unique<DNSServer>();
#if defined(USE_ESP32)
this->dns_server_->start(ip);
const bool started = server->start(ip);
#elif defined(USE_ARDUINO)
this->dns_server_->setErrorReplyCode(DNSReplyCode::NoError);
this->dns_server_->start(53, ESPHOME_F("*"), ip);
server->setErrorReplyCode(DNSReplyCode::NoError);
const bool started = server->start(53, ESPHOME_F("*"), ip);
#endif
if (!started)
return false;
this->dns_server_ = std::move(server);
return true;
}
void stop() {
if (this->dns_server_ == nullptr)
@@ -46,7 +46,7 @@ struct DNSAnswer {
uint32_t ip_addr;
} __attribute__((packed));
void DNSServer::start(const network::IPAddress &ip) {
bool DNSServer::start(const network::IPAddress &ip) {
this->server_ip_ = ip;
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE
char ip_buf[network::IP_ADDRESS_BUFFER_SIZE];
@@ -57,7 +57,7 @@ void DNSServer::start(const network::IPAddress &ip) {
this->socket_ = socket::socket_ip_loop_monitored(SOCK_DGRAM, IPPROTO_UDP).release();
if (this->socket_ == nullptr) {
ESP_LOGE(TAG, "Socket create failed");
return;
return false;
}
// Set socket options
@@ -72,9 +72,10 @@ void DNSServer::start(const network::IPAddress &ip) {
if (err != 0) {
ESP_LOGE(TAG, "Bind failed: %d", errno);
this->destroy_socket_();
return;
return false;
}
ESP_LOGV(TAG, "Bound to port %d", DNS_PORT);
return true;
}
void DNSServer::stop() {
@@ -13,7 +13,8 @@ namespace esphome::web_server_base {
class DNSServer {
public:
void start(const network::IPAddress &ip);
/// Returns false when the socket could not be created or bound.
bool start(const network::IPAddress &ip);
void stop();
void process_next_request();
+4 -3
View File
@@ -638,13 +638,14 @@ void WiFiComponent::setup() {
#if defined(USE_PROVISIONING) && defined(USE_WIFI_AP)
// The access point is a provisioning surface: once the provisioning window has
// closed, shut it down (mirrors the teardown done on a successful connection).
// The captive portal registers its own closed-callback, and the fallback block
// in loop() is gated so neither is started again afterwards.
// closed, end both portals and shut it down (mirrors the teardown done on a
// successful connection). The fallback block in loop() is gated so none of them
// is started again afterwards.
if (provisioning::global_provisioning_manager != nullptr) {
provisioning::global_provisioning_manager->add_on_closed_callback([this]() {
if (this->ap_setup_) {
ESP_LOGD(TAG, "Provisioning window closed; disabling AP");
this->end_ap_portal_();
this->wifi_mode_({}, false);
}
});
@@ -1,5 +1,12 @@
# STA with AP fallback plus local: true opts the fallback into captive AP mode; exercises
# the runtime start (fallback branch in wifi loop) and end (on STA connect) paths.
# the runtime start (fallback branch in wifi loop) and end (on STA connect, and when the
# provisioning window closes) paths.
provisioning:
timeout: 1min
api:
encryption:
wifi:
ssid: MySSID
password: password1