Complete Phase 12 dual-stack networking

Add IPv6-aware Wi-Fi state, HTTPS/SSH listeners, mDNS service
reconciliation, and browser Wi-Fi administration.

Include a guarded build-local fix for mDNS 1.12.0 membership handling,
focused regression suites, and Phase 12 acceptance documentation.
This commit is contained in:
2026-09-20 22:35:34 +02:00
parent ece4ba77e3
commit 8902b25d78
52 changed files with 3042 additions and 163 deletions
+171
View File
@@ -0,0 +1,171 @@
/* SPDX-License-Identifier: GPL-3.0-only */
#include <assert.h>
#include <stdio.h>
#include <string.h>
#include "mdns_service.c"
static int held, inits, frees, names, adds, removes;
static int fail_init, fail_name, fail_instance, fail_record;
static bool https, ssh;
static char hostname[64];
static int critical, tcpip, actions, enables, fail_action, drop_action;
static bool up = true, valid6 = true, present = true, zero6;
static uint32_t ipv4 = 1;
static unsigned ready;
static int64_t clock_us;
static esp_netif_t sta;
void fake_enter(portMUX_TYPE *m) { (void)m; assert(!critical); critical = 1; }
void fake_exit(portMUX_TYPE *m) { (void)m; assert(critical); critical = 0; }
int64_t esp_timer_get_time(void) { assert(!held && !critical); return clock_us; }
esp_netif_t *esp_netif_get_handle_from_ifkey(const char *key) {
assert(!held && !critical && !strcmp(key, "WIFI_STA_DEF")); return present ? &sta : NULL;
}
bool esp_netif_is_netif_up(esp_netif_t *n) { assert(tcpip && n == &sta); return up; }
esp_err_t esp_netif_get_ip_info(esp_netif_t *n, esp_netif_ip_info_t *ip) {
assert(tcpip && n == &sta); ip->ip.addr = ipv4; return ESP_OK;
}
int esp_netif_get_all_ip6(esp_netif_t *n, esp_ip6_addr_t *ip) {
assert(tcpip && n == &sta); memset(ip, 0, sizeof(*ip)); ip->addr[0] = zero6 ? 0 : 0xfe80;
return valid6 ? 1 : 0;
}
esp_err_t esp_netif_tcpip_exec(esp_err_t (*fn)(void *), void *arg) {
assert(!held && !critical && !tcpip); tcpip = 1;
esp_err_t error = fn(arg); tcpip = 0; return error;
}
esp_err_t mdns_netif_action(esp_netif_t *n, mdns_event_actions_t action) {
assert(!held && !critical && !tcpip && n == &sta); ++actions;
if (fail_action) return ESP_ERR_NO_MEM;
if (drop_action) return ESP_OK; /* Actual upstream full-queue behavior. */
if (action & MDNS_EVENT_ENABLE_IP4) { ready |= 1; ++enables; }
if (action & MDNS_EVENT_ENABLE_IP6) { ready |= 2; ++enables; }
if (action & MDNS_EVENT_DISABLE_IP4) ready &= ~1U;
if (action & MDNS_EVENT_DISABLE_IP6) ready &= ~2U;
return ESP_OK;
}
static void test_families(void) {
assert(ready == 3);
int before = actions;
for (int i = 0; i < 20; ++i) assert(mdns_service_reconcile() == ESP_OK);
assert(actions == before); /* Healthy dual-stack poll has no action. */
ipv4 = 0; /* Lost IPv4, surviving IPv6: A readiness must be removed. */
drop_action = 1;
assert(mdns_service_reconcile() == ESP_OK && ready == 3);
drop_action = 0;
assert(mdns_service_reconcile() == ESP_OK && ready == 2);
before = enables;
for (int i = 0; i < 20; ++i) assert(mdns_service_reconcile() == ESP_OK);
assert(enables == before); /* Disables do not restart the healthy family. */
zero6 = true; /* Defensive rejection even if an API supplied a zero entry. */
assert(mdns_service_reconcile() == ESP_OK && ready == 0);
zero6 = false;
valid6 = false;
assert(mdns_service_reconcile() == ESP_OK && ready == 0);
valid6 = true; /* Missed GOT_IP6 and silently lost repair submission. */
drop_action = 1;
assert(mdns_service_reconcile() == ESP_OK && ready == 0);
drop_action = 0;
clock_us += MDNS_FAMILY_REPAIR_US;
assert(mdns_service_reconcile() == ESP_OK && ready == 2);
ipv4 = 1; fail_action = 1;
assert(mdns_service_reconcile() == ESP_ERR_NO_MEM && ready == 2);
fail_action = 0;
assert(mdns_service_reconcile() == ESP_OK && ready == 3);
ready = 0; /* Late upstream disconnect action after the last sample. */
clock_us += MDNS_FAMILY_REPAIR_US;
assert(mdns_service_reconcile() == ESP_OK && ready == 3);
up = false;
assert(mdns_service_reconcile() == ESP_OK && ready == 0);
up = true;
assert(mdns_service_reconcile() == ESP_OK && ready == 3);
mdns_service_stop();
assert(ready == 0); /* Even if stale nonzero addresses remain in netif. */
assert(mdns_service_start() == ESP_OK && ready == 3);
present = false;
assert(mdns_service_reconcile() == ESP_ERR_INVALID_STATE);
present = true;
assert(mdns_service_reconcile() == ESP_OK);
}
SemaphoreHandle_t xSemaphoreCreateMutex(void) { return &held; }
int xSemaphoreTake(SemaphoreHandle_t m, int wait) {
(void)m;
assert(!critical);
if (held) { assert(!wait); return 0; }
held = 1; return pdTRUE;
}
int xSemaphoreGive(SemaphoreHandle_t m) { (void)m; assert(held); held = 0; return 1; }
esp_err_t mdns_config_validate(const mdns_config_t *c) { return c && c->suffix_len ? ESP_OK : ESP_ERR_INVALID_ARG; }
void mdns_config_defaults(mdns_config_t *c) { memset(c, 0, sizeof(*c)); strcpy(c->suffix, "default"); c->suffix_len = 7; }
esp_err_t mdns_config_load(mdns_config_t *c, bool *stored) { mdns_config_defaults(c); *stored = false; return ESP_OK; }
esp_err_t mdns_config_save(const mdns_config_t *c) { (void)c; return ESP_OK; }
esp_err_t mdns_init(void) { assert(!held && !critical); ++inits; return fail_init ? ESP_ERR_NO_MEM : ESP_OK; }
void mdns_free(void) { assert(!held && !critical); ++frees; }
esp_err_t mdns_hostname_set(const char *n) { assert(!held && !critical); ++names; if (fail_name) return ESP_ERR_NO_MEM; strcpy(hostname, n); return ESP_OK; }
esp_err_t mdns_instance_name_set(const char *n) { assert(!held && !critical && n); return fail_instance ? ESP_ERR_NO_MEM : ESP_OK; }
esp_err_t mdns_service_add(const char *instance, const char *type, const char *proto, uint16_t port, void *txt, size_t count) {
assert(!held && !critical && !instance && !txt && !count && !strcmp(proto, "_tcp"));
++adds;
if (fail_record) return ESP_ERR_NO_MEM;
bool *record = !strcmp(type, "_https") ? &https : &ssh;
assert(port == (record == &https ? 443 : 22));
assert(!*record); *record = true; return ESP_OK;
}
esp_err_t mdns_service_remove(const char *type, const char *proto) {
assert(!held && !critical && !strcmp(proto, "_tcp")); ++removes;
if (fail_record) return ESP_ERR_NO_MEM;
bool *record = !strcmp(type, "_https") ? &https : &ssh;
assert(*record); *record = false; return ESP_OK;
}
int main(int argc, char **argv) {
assert(argc == 2);
fail_init = !strcmp(argv[1], "init-failure");
fail_name = !strcmp(argv[1], "hostname-failure");
fail_instance = !strcmp(argv[1], "instance-failure");
mdns_service_set_https_available(true);
mdns_service_set_ssh_available(false);
assert(!inits && !adds);
assert(mdns_service_reconcile() == ESP_ERR_INVALID_STATE);
mdns_config_t c; mdns_config_defaults(&c);
assert(mdns_service_init(&c) == ESP_OK);
assert(mdns_service_reconcile() == ESP_OK && !inits);
if (fail_init || fail_name || fail_instance) {
assert(mdns_service_start() == ESP_ERR_NO_MEM);
assert(frees == (fail_init ? 0 : 1));
fail_init = fail_name = fail_instance = 0;
for (int i = 0; i < 10; ++i) {
assert(mdns_service_start() == ESP_ERR_NO_MEM);
assert(mdns_service_reconcile() == ESP_ERR_NO_MEM);
assert(mdns_service_reannounce() == ESP_ERR_NO_MEM);
mdns_service_stop();
}
assert(inits == 1 && !adds);
} else {
assert(mdns_service_start() == ESP_OK && https && !ssh);
for (int i = 0; i < 10; ++i) assert(mdns_service_start() == ESP_OK);
assert(inits == 1 && names == 1 && adds == 1);
test_families();
held = 1; /* Notifications must never acquire the service mutex. */
mdns_service_set_https_available(false);
mdns_service_set_ssh_available(true);
held = 0;
fail_record = 1;
assert(mdns_service_reconcile() == ESP_ERR_NO_MEM && https && !ssh);
fail_record = 0;
assert(mdns_service_reconcile() == ESP_OK && !https && ssh);
mdns_service_stop();
mdns_service_set_ssh_available(false);
assert(mdns_service_reconcile() == ESP_OK && !ssh);
strcpy(c.suffix, "offline"); c.suffix_len = 7;
assert(mdns_service_set_config(&c) == ESP_OK);
fail_name = 1;
assert(mdns_service_start() == ESP_ERR_NO_MEM);
fail_name = 0;
assert(mdns_service_start() == ESP_OK && !strcmp(hostname, "sak-offline"));
mdns_service_set_https_available(true);
mdns_service_set_https_available(false);
int before = adds;
assert(mdns_service_reconcile() == ESP_OK && adds == before);
assert(inits == 1 && !frees);
}
printf("PASS %s\n", argv[1]);
return 0;
}