From 79eb26ac6f5018b500bc0ac469ad27ad863e4031 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 30 Sep 2026 22:21:02 +0200 Subject: [PATCH] End the web_server captive DNS when the provisioning window closes and report a DNS start failure --- esphome/components/web_server/web_server.cpp | 7 +++++-- .../components/web_server_base/captive_dns.h | 17 +++++++++++------ .../web_server_base/dns_server_esp32_idf.cpp | 7 ++++--- .../web_server_base/dns_server_esp32_idf.h | 3 ++- esphome/components/wifi/wifi_component.cpp | 7 ++++--- .../web_server/common-ap-fallback.yaml | 9 ++++++++- 6 files changed, 34 insertions(+), 16 deletions(-) diff --git a/esphome/components/web_server/web_server.cpp b/esphome/components/web_server/web_server.cpp index 97d8d78077..e0ad4f727a 100644 --- a/esphome/components/web_server/web_server.cpp +++ b/esphome/components/web_server/web_server.cpp @@ -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)); } diff --git a/esphome/components/web_server_base/captive_dns.h b/esphome/components/web_server_base/captive_dns.h index 592e5369b2..c1d1cb0795 100644 --- a/esphome/components/web_server_base/captive_dns.h +++ b/esphome/components/web_server_base/captive_dns.h @@ -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(); + return true; + auto server = make_unique(); #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) diff --git a/esphome/components/web_server_base/dns_server_esp32_idf.cpp b/esphome/components/web_server_base/dns_server_esp32_idf.cpp index 34c1af375e..7948539cdf 100644 --- a/esphome/components/web_server_base/dns_server_esp32_idf.cpp +++ b/esphome/components/web_server_base/dns_server_esp32_idf.cpp @@ -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() { diff --git a/esphome/components/web_server_base/dns_server_esp32_idf.h b/esphome/components/web_server_base/dns_server_esp32_idf.h index 8136187387..571d938501 100644 --- a/esphome/components/web_server_base/dns_server_esp32_idf.h +++ b/esphome/components/web_server_base/dns_server_esp32_idf.h @@ -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(); diff --git a/esphome/components/wifi/wifi_component.cpp b/esphome/components/wifi/wifi_component.cpp index 3274414b9e..ecbc3d991a 100644 --- a/esphome/components/wifi/wifi_component.cpp +++ b/esphome/components/wifi/wifi_component.cpp @@ -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); } }); diff --git a/tests/components/web_server/common-ap-fallback.yaml b/tests/components/web_server/common-ap-fallback.yaml index 3a074f4a61..0a4e1bc42d 100644 --- a/tests/components/web_server/common-ap-fallback.yaml +++ b/tests/components/web_server/common-ap-fallback.yaml @@ -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