/* SPDX-License-Identifier: GPL-3.0-only */ #include #include #include #include #include #include "esp_netif_ip_addr.h" #define WIFI_AUTH_OPEN 0 #include "wifi_manager.h" #include "mdns_service.h" #define LWIP_NETIF_HOSTNAME 1 #define CONFIG_LWIP_IPV6 1 static char s_station_hostname[MDNS_CONFIG_SUFFIX_MAX_LEN + 5U]; #define LWIP_IPV6_AUTOCONFIG 1 #define LWIP_IPV6_NUM_ADDRESSES 3 #define IP6_ADDR_INVALID 0 #define TENTATIVE 1 #define PREFERRED 2 #define DEPRECATED 3 #define DUPLICATE 4 #define WIFI_MANAGER_ATTEMPT_US 12000000LL #define WIFI_MANAGER_STABLE_US 30000000LL #define WIFI_MANAGER_RECONCILE_US 1000000LL #define WIFI_MANAGER_DISCONNECT_SETTLE_US 1000000LL #define WIFI_MANAGER_INITIAL_BACKOFF_SECONDS 2 #define ESP_LOGW(...) (++warnings) #define portMAX_DELAY UINT32_MAX #define pdMS_TO_TICKS(x) (x) typedef uint32_t TickType_t; typedef struct { uint32_t addr; } ip4_t; typedef struct { ip4_t ip, netmask, gw; } esp_netif_ip_info_t; typedef struct { uint8_t ssid[33], primary; int8_t rssi; int authmode; } wifi_ap_record_t; typedef struct { int unused; } esp_netif_t; static esp_netif_t sta, other; static esp_netif_t *s_sta_netif = &sta; typedef const char *esp_event_base_t; static const char *IP_EVENT = "ip"; enum { IP_EVENT_STA_GOT_IP, IP_EVENT_STA_LOST_IP, IP_EVENT_GOT_IP6 }; typedef struct { esp_netif_t *esp_netif; esp_netif_ip_info_t ip_info; } ip_event_got_ip_t; typedef struct { esp_netif_t *esp_netif; } ip_event_got_ip6_t; #include "types.inc" static manager_shared_t s_shared; struct netif { int state[3]; bool autoconfig; esp_ip6_addr_t addresses[3]; const char *hostname; }; static struct netif netif; static bool up, associated, matching, in_tcpip, byte_ssid; static int64_t now; static esp_netif_ip_info_t ip4; static int mdns_starts, mdns_stops, disconnects, next_profiles, cycles, ap_disables; static int creations, messages, mdns_reconciles, mdns_reannounces, tcpip_calls; static int shared_locks, warnings; static esp_err_t tcpip_error, mdns_error, ip_info_error; static bool https_available, https_registered, tcpip_skip; static manager_message_t last_message; static char hostname[64], configured[64]; static esp_err_t hostname_error; static void lock_shared(void) { assert(shared_locks == 0); ++shared_locks; } static void unlock_shared(void) { assert(shared_locks == 1); --shared_locks; } static bool manager_is_started(void) { return s_shared.snapshot.started; } static void set_state(wifi_manager_state_t state) { s_shared.snapshot.state = state; } static void set_last_error(esp_err_t error) { s_shared.snapshot.last_error = error; } static int64_t esp_timer_get_time(void) { return now; } static bool esp_netif_is_netif_up(esp_netif_t *n) { assert(n == &sta && in_tcpip); return up; } static esp_err_t esp_netif_get_ip_info(esp_netif_t *n, esp_netif_ip_info_t *out) { assert(n == &sta && in_tcpip); *out = ip4; return ip_info_error; } static void *esp_netif_get_netif_impl(esp_netif_t *n) { assert(n == &sta && in_tcpip); return &netif; } static void netif_set_ip6_autoconfig_enabled(struct netif *n, int enabled) { assert(in_tcpip); n->autoconfig = enabled; } static int netif_ip6_addr_state(struct netif *n, int slot) { return n->state[slot]; } static void netif_create_ip6_linklocal_address(struct netif *n, int mac) { assert(in_tcpip && mac == 1); ++creations; n->state[0] = TENTATIVE; } static int esp_netif_get_all_preferred_ip6(esp_netif_t *n, esp_ip6_addr_t *out) { assert(n == &sta && in_tcpip); int count = 0; for (int i = 0; i < 3; ++i) if (netif.state[i] == PREFERRED) out[count++] = netif.addresses[i]; return count; } static esp_err_t esp_netif_tcpip_exec(esp_err_t (*fn)(void *), void *context) { assert(!in_tcpip && shared_locks == 0); ++tcpip_calls; if (tcpip_error != ESP_OK) return tcpip_error; if (tcpip_skip) return ESP_OK; /* IDF wrapper ignores failed enqueue. */ in_tcpip = true; esp_err_t e = fn(context); in_tcpip = false; return e; } static esp_err_t esp_wifi_sta_get_ap_info(wifi_ap_record_t *out) { memset(out, 0, sizeof(*out)); memcpy(out->ssid, "test", 4); if (byte_ssid) out->ssid[1] = 0; return associated ? ESP_OK : ESP_FAIL; } static bool connected_event_matches_active_profile(const manager_message_t *m) { assert(m->data.connected.ssid_len == 4); return matching; } static void copy_working_config(wifi_app_config_t *out) { *out = s_shared.config; } void wifi_config_secure_wipe(void *p, size_t n) { memset(p, 0, n); } esp_err_t mdns_service_start(void) { ++mdns_starts; return mdns_error; } esp_err_t mdns_service_reconcile(void) { assert(!in_tcpip && shared_locks == 0); ++mdns_reconciles; if (mdns_error == ESP_OK) https_registered = https_available; return mdns_error; } esp_err_t mdns_service_reannounce(void) { ++mdns_reannounces; return mdns_service_reconcile(); } void mdns_service_stop(void) { ++mdns_stops; } esp_err_t mdns_service_get_snapshot(mdns_service_snapshot_t *out) { memset(out, 0, sizeof(*out)); strcpy(out->hostname, configured); return hostname_error; } static void netif_set_hostname(struct netif *n, const char *name) { assert(n == &netif && in_tcpip); n->hostname = name; strcpy(hostname, name); } static void handle_sta_disconnected(manager_runtime_t *r, const manager_message_t *m) { (void)m; r->online = r->associated = false; ++disconnects; } static void mark_intentional_disconnect(manager_runtime_t *r) { ++disconnects; ++r->intentional_disconnects; r->online = r->associated = false; } static void start_next_profile(manager_runtime_t *r) { (void)r; ++next_profiles; } static void start_profile_cycle(manager_runtime_t *r) { (void)r; ++cycles; } static void start_radio_and_policy(manager_runtime_t *r) { (void)r; } static esp_err_t esp_wifi_disconnect(void) { ++disconnects; return ESP_OK; } static esp_err_t set_runtime_ap_enabled(manager_runtime_t *r, bool enable) { r->ap_enabled = enable; if (!enable) ++ap_disables; return ESP_OK; } static bool enqueue_message(const manager_message_t *m) { last_message = *m; ++messages; return true; } #include "production.inc" static char rendered[2048]; static int capture_printf(const char *format, ...) { size_t used = strlen(rendered); va_list args; va_start(args, format); int n = vsnprintf(rendered + used, sizeof(rendered) - used, format, args); va_end(args); assert(n >= 0 && (size_t)n < sizeof(rendered) - used); return n; } #define printf capture_printf #include "console.inc" #undef printf static void assert_empty_addresses(void) { const wifi_manager_ipv6_address_t zero[WIFI_MANAGER_IPV6_MAX_ADDRESSES] = {0}; assert(s_shared.snapshot.ipv6_count == 0); assert(!s_shared.snapshot.ipv6_linklocal && !s_shared.snapshot.ipv6_routable); assert(memcmp(zero, s_shared.snapshot.ipv6_addresses, sizeof(zero)) == 0); } static manager_runtime_t reset(void) { memset(&s_shared, 0, sizeof(s_shared)); memset(&netif, 0, sizeof(netif)); memset(&ip4, 0, sizeof(ip4)); up = associated = matching = true; now = 1000000; byte_ssid = false; mdns_starts = mdns_stops = disconnects = next_profiles = cycles = ap_disables = 0; creations = messages = mdns_reconciles = mdns_reannounces = tcpip_calls = 0; hostname_error = tcpip_error = mdns_error = ip_info_error = ESP_OK; warnings = 0; https_available = https_registered = tcpip_skip = false; strcpy(configured, "sak-test"); s_shared.snapshot.started = true; s_shared.snapshot.sta_ssid_len = 4; s_shared.config.ap_policy = WIFI_CONFIG_AP_POLICY_FALLBACK; manager_runtime_t r = { .radio_started = true, .attempt_deadline = now + 12000000 }; return r; } static void address(int slot, uint8_t first, uint8_t second, int state) { memset(&netif.addresses[slot], 0, sizeof(netif.addresses[slot])); uint8_t *bytes = (uint8_t *)netif.addresses[slot].addr; bytes[0] = first; bytes[1] = second; bytes[15] = 1; netif.state[slot] = state; } int main(void) { manager_runtime_t r = reset(); strcpy(configured, "sak-first"); assert(refresh_station_hostname() == ESP_OK); assert(!strcmp(hostname, "sak-first")); strcpy(configured, "sak-renamed"); assert(refresh_station_hostname() == ESP_OK); assert(!strcmp(hostname, "sak-renamed") && disconnects == 0); memset(configured, 'a', 59); memcpy(configured, "sak-", 4); configured[59] = 0; assert(refresh_station_hostname() == ESP_OK && strlen(hostname) == 59); assert(netif.hostname == s_station_hostname); /* No captured stack pointer. */ assert(!strcmp(netif.hostname, configured)); hostname_error = ESP_FAIL; assert(refresh_station_hostname() == ESP_FAIL); r = reset(); byte_ssid = true; ip4.ip.addr = 7; handle_got_ip(&r, NULL); assert(r.online); /* Embedded NUL keeps profile length. */ r = reset(); handle_got_ip(&r, NULL); assert(netif.autoconfig && creations == 1 && !r.online); handle_got_ip(&r, NULL); assert(creations == 1); /* Do not restart DAD. */ address(0, 0xfe, 0x80, PREFERRED); now = r.attempt_deadline; handle_expired_deadlines(&r); /* dropped CONNECTED and GOT_IP6 */ assert(r.online && r.attempt_deadline == 0 && disconnects == 0); assert(s_shared.snapshot.ipv6_linklocal && s_shared.snapshot.ip == 0); assert(mdns_starts == 1); int64_t stable = r.stable_deadline; now += 1000000; handle_expired_deadlines(&r); assert(r.stable_deadline == stable && mdns_starts == 1); now = stable; handle_expired_deadlines(&r); assert(ap_disables == 1); assert(runtime_wait_ticks(&r) <= 1000); /* Never sleep indefinitely online. */ address(1, 0xfd, 0x12, PREFERRED); handle_got_ip(&r, NULL); assert(s_shared.snapshot.ipv6_routable); ip4.ip.addr = 123; handle_got_ip(&r, NULL); assert(s_shared.snapshot.ip == 123 && s_shared.snapshot.counters.got_ip == 1); ip4.ip.addr = 0; handle_got_ip(&r, NULL); assert(r.online && !s_shared.snapshot.ip && mdns_stops == 0); address(0, 0xfe, 0x80, DEPRECATED); address(1, 0xfd, 0x12, DEPRECATED); now += 1000000; handle_expired_deadlines(&r); assert(!r.online && !s_shared.snapshot.ipv6_linklocal && !s_shared.snapshot.ipv6_routable); assert(mdns_stops == 1 && r.attempt_deadline > now); address(1, 0x20, 0x01, PREFERRED); handle_got_ip(&r, NULL); assert(r.online && s_shared.snapshot.ipv6_routable && mdns_starts == 2); r = reset(); address(0, 0xfe, 0x80, DUPLICATE); now = r.attempt_deadline; handle_expired_deadlines(&r); assert(!r.online && creations == 0 && disconnects == 1); assert(r.advance_after_disconnect && r.disconnect_deadline > now); r = reset(); address(0, 0xfe, 0x80, TENTATIVE); ip4.ip.addr = 7; handle_got_ip(&r, NULL); assert(r.online && !s_shared.snapshot.ipv6_linklocal); ip4.ip.addr = 0; associated = false; now += 1000000; handle_expired_deadlines(&r); assert(disconnects == 1 && !r.online); r = reset(); address(0, 0xfe, 0x80, PREFERRED); r.stop_pending = true; handle_got_ip(&r, NULL); assert(!r.online); r.stop_pending = false; r.advance_after_disconnect = true; handle_got_ip(&r, NULL); assert(!r.online); r.advance_after_disconnect = false; matching = false; handle_got_ip(&r, NULL); assert(!r.online); matching = true; up = false; handle_got_ip(&r, NULL); assert(!r.online); up = true; handle_got_ip(&r, NULL); assert(r.online); netif.state[0] = IP6_ADDR_INVALID; /* missed disconnect+connect: re-create LL */ handle_got_ip(&r, NULL); assert(creations == 1 && !r.online); ip_event_got_ip6_t event6 = { .esp_netif = &other }; ip_event_callback(NULL, IP_EVENT, IP_EVENT_GOT_IP6, &event6); assert(messages == 0); event6.esp_netif = &sta; ip_event_callback(NULL, IP_EVENT, IP_EVENT_GOT_IP6, &event6); assert(messages == 1 && last_message.type == MESSAGE_STA_GOT_IP6); ip_event_got_ip_t event4 = { .esp_netif = &other }; ip_event_callback(NULL, IP_EVENT, IP_EVENT_STA_GOT_IP, &event4); assert(messages == 1); ip_event_callback(NULL, IP_EVENT, IP_EVENT_GOT_IP6, NULL); assert(messages == 1); r = reset(); mdns_error = ESP_FAIL; start_mdns_announcement(&r); start_mdns_announcement(&r); assert(warnings == 1 && r.mdns_reannounce_pending); for (int i = 0; i < 5; ++i) { now += WIFI_MANAGER_RECONCILE_US; handle_expired_deadlines(&r); } assert(warnings == 1); /* No per-pass warning on persistent failure. */ r = reset(); r.radio_started = false; r.attempt_deadline = 0; s_shared.snapshot.started = false; associated = up = false; assert(runtime_wait_ticks(&r) == 0); /* Boot with Wi-Fi disabled. */ https_registered = true; handle_expired_deadlines(&r); assert(mdns_reconciles == 1 && !https_registered && creations == 0); assert(runtime_wait_ticks(&r) == 1000); for (int i = 0; i < 100; ++i) handle_expired_deadlines(&r); assert(mdns_reconciles == 1); /* Busy owner queue cannot cause retry spam. */ mdns_error = ESP_FAIL; https_available = true; now += WIFI_MANAGER_RECONCILE_US; handle_expired_deadlines(&r); assert(mdns_reconciles == 2 && !https_registered); mdns_error = ESP_OK; now += WIFI_MANAGER_RECONCILE_US; handle_expired_deadlines(&r); assert(mdns_reconciles == 3 && https_registered && !r.radio_started); /* Failed offline rename is retried without a lifecycle command. */ tcpip_error = ESP_FAIL; strcpy(configured, "sak-retry"); now += WIFI_MANAGER_RECONCILE_US; handle_expired_deadlines(&r); assert(strcmp(hostname, configured) != 0); tcpip_error = ESP_OK; now += WIFI_MANAGER_RECONCILE_US; handle_expired_deadlines(&r); assert(!strcmp(hostname, configured) && netif.hostname == s_station_hostname); assert(disconnects == 0 && !r.radio_started); r = reset(); address(0, 0xfe, 0x80, PREFERRED); handle_got_ip(&r, NULL); assert(r.online); r.mdns_reannounce_pending = true; mdns_error = ESP_FAIL; handle_expired_deadlines(&r); assert(r.mdns_reannounce_pending && mdns_reannounces == 1); mdns_error = ESP_OK; now += WIFI_MANAGER_RECONCILE_US; handle_expired_deadlines(&r); assert(!r.mdns_reannounce_pending && mdns_reannounces == 2); tcpip_error = ESP_FAIL; now = r.stable_deadline; handle_expired_deadlines(&r); assert(!r.online && !s_shared.snapshot.ipv6_linklocal && ap_disables == 0); assert(mdns_stops == 1 && r.attempt_deadline > now); tcpip_error = ESP_OK; now += WIFI_MANAGER_RECONCILE_US; handle_expired_deadlines(&r); assert(r.online && s_shared.snapshot.ipv6_linklocal && disconnects == 0); tcpip_skip = true; assert(refresh_station_hostname() == ESP_FAIL); handle_got_ip(&r, NULL); assert(!r.online && s_shared.snapshot.last_error == ESP_FAIL); tcpip_skip = false; handle_got_ip(&r, NULL); assert(r.online); /* Netif-down with a stale driver association must withdraw readiness. */ up = false; handle_got_ip(&r, NULL); assert(!r.online && !s_shared.snapshot.ipv6_linklocal); assert_empty_addresses(); r = reset(); address(0, 0xfe, 0x80, PREFERRED); address(1, 0xfd, 0x12, PREFERRED); address(2, 0x20, 0x01, PREFERRED); netif.addresses[2].addr[1] = esp_netif_htonl(0x12345678); netif.addresses[2].addr[2] = esp_netif_htonl(0x9abcdef0); netif.addresses[2].zone = 42; handle_got_ip(&r, NULL); assert(s_shared.snapshot.ipv6_count == 3); for (int i = 0; i < 3; ++i) assert(memcmp(s_shared.snapshot.ipv6_addresses[i].addr, netif.addresses[i].addr, 16) == 0); print_ipv6_addresses(&s_shared.snapshot); assert(strstr(rendered, "IPv6 preferred link-local: fe80:0000:0000:0000:0000:0000:0000:0001\n")); assert(strstr(rendered, "IPv6 preferred ULA: fd12:0000:0000:0000:0000:0000:0000:0001\n")); assert(strstr(rendered, "IPv6 preferred GUA: 2001:0000:1234:5678:9abc:def0:0000:0001\n")); assert(strstr(rendered, "client's interface") && !strstr(rendered, "%42")); netif.state[0] = TENTATIVE; netif.state[1] = DUPLICATE; netif.state[2] = DEPRECATED; handle_got_ip(&r, NULL); assert_empty_addresses(); rendered[0] = 0; print_ipv6_addresses(&s_shared.snapshot); assert(strstr(rendered, "IPv6 preferred addresses: none") && !strstr(rendered, "IPv6 preferred GUA:")); for (int failure = 0; failure < 5; ++failure) { address(0, 0xfe, 0x80, PREFERRED); address(1, 0xfd, 0x12, PREFERRED); handle_got_ip(&r, NULL); assert(s_shared.snapshot.ipv6_count == 2); if (failure == 0) tcpip_error = ESP_FAIL; if (failure == 1) tcpip_skip = true; if (failure == 2) up = false; if (failure == 4) ip_info_error = ESP_FAIL; if (failure == 3) clear_station_network_snapshot(); else handle_got_ip(&r, NULL); assert_empty_addresses(); tcpip_error = ip_info_error = ESP_OK; tcpip_skip = false; up = true; } printf("snapshot=%zu settings=%zu shared=%zu observation=%zu bytes\n", sizeof(wifi_manager_snapshot_t), sizeof(wifi_manager_settings_t), sizeof(manager_shared_t), sizeof(station_addresses_t)); puts("wifi_phase12: hostname, IPv4/IPv6 lifecycle, deadlines, offline mDNS retries, TCP/IP ownership and event filtering PASS"); }