diag(provisioning): instrument captive flow + tighten DHCP/radio behavior
The previous protocol fix (302 → 200 portal HTML) didn't restore iOS captive-banner reliability under the lock-screen-camera join: the user joined, accepted the prompt, and got nothing on unlock. We're guessing without data, so this round adds instrumentation alongside three high-confidence behavioral fixes that are individually plausible explanations. Fixes: * Force the AP DHCP server to advertise the AP IP as DNS via esp_netif_dhcps_option(ESP_NETIF_DOMAIN_NAME_SERVER). Arduino-ESP32's softAP doesn't set this explicitly; if a client comes in with cached cellular DNS the captive DNS hijack gets bypassed and iOS resolves captive.apple.com to real internet — no captive signal ever fires. * WiFi.setSleep(false) so the AP radio doesn't park between beacons and drop probe packets that arrive during a sleep window. * Cache-Control: no-store on the portal response, so iOS doesn't carry a "this SSID was fine last time" determination across forget+rejoin cycles. Diagnostics (logged on serial at 115200, in AP mode only): * Every HTTP request: method, URI, Host, User-Agent. Tells us whether iOS is reaching us and which CNA path it's hitting. * WiFi AP events: STA-associated, IP-assigned, STA-disconnected. Tells us whether the join completed and DHCP succeeded. Repro: pio device monitor -e waveshare73-v1, forget the network on the phone, lock + scan + accept, watch the log. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -8,6 +8,7 @@
|
||||
#include <Preferences.h>
|
||||
#include <LittleFS.h>
|
||||
#include <qrcode.h>
|
||||
#include "esp_netif.h"
|
||||
#include "config.h"
|
||||
#include "epd.h"
|
||||
#include "operation.h"
|
||||
@@ -97,11 +98,37 @@ min-height:100vh;margin:0;background:#fdf6ee;color:#3a2e22;text-align:center}
|
||||
|
||||
// ── Web server handlers ───────────────────────────────────────────────────────
|
||||
|
||||
// Diagnostic — every probe we receive during provisioning gets logged
|
||||
// with method / URI / Host / User-Agent so a serial-monitor repro tells
|
||||
// us whether the client (iOS in particular) is actually hitting us, and
|
||||
// which CNA path it's using. Cheap; only runs in AP mode.
|
||||
static void log_provisioning_request() {
|
||||
String method = server.method() == HTTP_GET ? "GET"
|
||||
: server.method() == HTTP_POST ? "POST"
|
||||
: server.method() == HTTP_HEAD ? "HEAD"
|
||||
: "OTHER";
|
||||
Serial.print("[ap-http] ");
|
||||
Serial.print(method);
|
||||
Serial.print(" ");
|
||||
Serial.print(server.uri());
|
||||
Serial.print(" host=");
|
||||
Serial.print(server.hostHeader());
|
||||
Serial.print(" ua=");
|
||||
Serial.println(server.header("User-Agent"));
|
||||
}
|
||||
|
||||
static void handle_root() {
|
||||
log_provisioning_request();
|
||||
// no-store stops iOS from caching a CNA result across joins to the
|
||||
// same SSID name (e.g. after a forget+rejoin cycle) — we always want
|
||||
// the captive banner to fire, never the cached "this network was
|
||||
// fine last time" answer.
|
||||
server.sendHeader("Cache-Control", "no-store, must-revalidate");
|
||||
server.send_P(200, "text/html", PORTAL_HTML);
|
||||
}
|
||||
|
||||
static void handle_connect() {
|
||||
log_provisioning_request();
|
||||
if (!server.hasArg("ssid") || server.arg("ssid").isEmpty()) {
|
||||
server.send(400, "text/plain", "Missing ssid");
|
||||
return;
|
||||
@@ -112,6 +139,25 @@ static void handle_connect() {
|
||||
server.send_P(200, "text/html", CONNECTING_HTML);
|
||||
}
|
||||
|
||||
static void on_ap_event(WiFiEvent_t event, WiFiEventInfo_t info) {
|
||||
switch (event) {
|
||||
case ARDUINO_EVENT_WIFI_AP_START:
|
||||
Serial.println("[ap-event] AP started");
|
||||
break;
|
||||
case ARDUINO_EVENT_WIFI_AP_STACONNECTED:
|
||||
Serial.println("[ap-event] station associated");
|
||||
break;
|
||||
case ARDUINO_EVENT_WIFI_AP_STAIPASSIGNED:
|
||||
Serial.println("[ap-event] DHCP lease handed out");
|
||||
break;
|
||||
case ARDUINO_EVENT_WIFI_AP_STADISCONNECTED:
|
||||
Serial.println("[ap-event] station disconnected");
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// ── WiFi provisioning ─────────────────────────────────────────────────────────
|
||||
|
||||
static void enter_provisioning(const String& mac, bool retry = false) {
|
||||
@@ -147,10 +193,46 @@ static void enter_provisioning(const String& mac, bool retry = false) {
|
||||
server.on("/connecttest.txt", handle_root); // Windows 10+
|
||||
server.onNotFound(handle_root);
|
||||
|
||||
// WebServer doesn't capture arbitrary request headers unless told to
|
||||
const char* tracked_headers[] = { "User-Agent", "Host" };
|
||||
server.collectHeaders(tracked_headers, 2);
|
||||
|
||||
WiFi.onEvent(on_ap_event);
|
||||
WiFi.disconnect(true);
|
||||
WiFi.mode(WIFI_AP);
|
||||
WiFi.softAP(apSsid.c_str());
|
||||
|
||||
// Power-save can park the radio between beacons and drop iOS CNA
|
||||
// probes that arrive during a sleep window — keep the AP fully awake
|
||||
// so the captive-detect window is deterministic.
|
||||
WiFi.setSleep(false);
|
||||
|
||||
// Belt-and-braces DHCP DNS advertisement: ESP-IDF's softAP DHCP server
|
||||
// *should* offer the AP IP as DNS by default, but if the client comes
|
||||
// in with cached cellular DNS (8.8.8.8 etc) the captive DNS hijack
|
||||
// gets bypassed entirely and iOS resolves captive.apple.com to the
|
||||
// real internet. Stopping the DHCP server, setting DNS-info, flipping
|
||||
// the offer-DNS flag, then restarting forces every lease to carry our
|
||||
// address as the primary resolver.
|
||||
esp_netif_t *ap_netif = esp_netif_get_handle_from_ifkey("WIFI_AP_DEF");
|
||||
if (ap_netif) {
|
||||
esp_netif_dhcps_stop(ap_netif);
|
||||
|
||||
esp_netif_ip_info_t ip;
|
||||
esp_netif_get_ip_info(ap_netif, &ip);
|
||||
esp_netif_dns_info_t dns_info = {};
|
||||
dns_info.ip.u_addr.ip4.addr = ip.ip.addr;
|
||||
dns_info.ip.type = ESP_IPADDR_TYPE_V4;
|
||||
esp_netif_set_dns_info(ap_netif, ESP_NETIF_DNS_MAIN, &dns_info);
|
||||
|
||||
uint8_t offer_dns = 1;
|
||||
esp_netif_dhcps_option(ap_netif, ESP_NETIF_OP_SET,
|
||||
ESP_NETIF_DOMAIN_NAME_SERVER,
|
||||
&offer_dns, sizeof(offer_dns));
|
||||
|
||||
esp_netif_dhcps_start(ap_netif);
|
||||
}
|
||||
|
||||
server.begin();
|
||||
dns.setErrorReplyCode(DNSReplyCode::NoError);
|
||||
dns.start(53, "*", WiFi.softAPIP());
|
||||
|
||||
Reference in New Issue
Block a user