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:
@@ -107,6 +107,23 @@ static void admin_tests(void) {
|
||||
user_principal_t administrator = alice; administrator.role = USER_ROLE_ADMIN;
|
||||
issued_t user = mint(&bob), admin = mint(&administrator), other = mint(&administrator);
|
||||
unsigned before = upgrades;
|
||||
issued_t ipv6_admin;
|
||||
const char *ipv6_origin = "https://[2001:db8::1]";
|
||||
assert(web_session_store_issue(&administrator, ipv6_origin, strlen(ipv6_origin),
|
||||
ipv6_admin.token, &ipv6_admin.view) == ESP_OK);
|
||||
for (unsigned mode = 0; mode < 3; ++mode) {
|
||||
begin(WEB_ADMIN_TICKET_URI, HTTP_POST, NULL);
|
||||
add("Host", mode == 2 ? "[2001:db8::2]" : "[2001:0db8:0:0:0:0:0:1]:443");
|
||||
add("Origin", mode == 1 ? "https://[2001:db8::1]" : "https://[2001:db8::2]");
|
||||
char cookies[100]; snprintf(cookies, sizeof(cookies), "__Host-sak-session=%s", ipv6_admin.token);
|
||||
add("Cookie", cookies); add("X-CSRF-Token", ipv6_admin.view.csrf);
|
||||
assert(web_admin_transport_ticket_handler(&req) == ESP_OK);
|
||||
assert(!strcmp(response_status, mode == 1 ? "200 OK" : mode == 2 ? "401 Unauthorized" : "403 Forbidden"));
|
||||
assert(upgrades == before);
|
||||
}
|
||||
web_session_store_invalidate(ipv6_admin.view.id);
|
||||
web_admin_tickets_revoke(0, NULL, 0);
|
||||
puts("PASS: admin ticket IPv6 canonical Origin accepted, cross-address Origin rejected without upgrade");
|
||||
for (unsigned mode = 0; mode < 5; ++mode) {
|
||||
admin_request(mode == 0 ? NULL : mode == 1 ? &user : &admin,
|
||||
WEB_ADMIN_TICKET_URI, true, mode != 2, mode != 3);
|
||||
|
||||
@@ -7,6 +7,13 @@ static unsigned mutations, projections, timer_creates, timer_starts;
|
||||
static uint32_t queued;
|
||||
static esp_err_t owner_error, mdns_queue_error;
|
||||
static bool stored_mdns = true;
|
||||
static bool snapshot_ipv6_case, snapshot_ipv4, snapshot_linklocal, snapshot_routable;
|
||||
static uint8_t snapshot_ipv6_count;
|
||||
static const uint8_t snapshot_ipv6_bytes[3][16] = {
|
||||
{0xfe,0x80,0,0,0,0,0,0,0xea,0x3d,0xc1,0xff,0xfe,0xfa,0x70,0x58},
|
||||
{0xfd,0x39,0xb3,0x78,0x78,0xf2,0xc7,0x15,0xea,0x3d,0xc1,0xff,0xfe,0xfa,0x70,0x58},
|
||||
{0x20,0x01,0x0d,0xb8,0,1,0,2,0,3,0,4,0,5,0,6},
|
||||
};
|
||||
static void (*timer_callback)(void *);
|
||||
static void (*owner_hook)(void);
|
||||
static void (*queue_hook)(void);
|
||||
@@ -65,8 +72,19 @@ esp_err_t wifi_manager_get_settings(wifi_manager_settings_t *out) {
|
||||
out->profiles[i].ssid_len=32; memset(out->profiles[i].ssid,0xff,32);
|
||||
out->profiles[i].priority=255; out->profiles[i].security=WIFI_CONFIG_SECURITY_MIXED;
|
||||
}
|
||||
out->runtime.active_profile=-1; out->runtime.last_error=INT32_MIN;
|
||||
out->runtime.ipv6_count=snapshot_ipv6_count;
|
||||
for(unsigned i=0;i<snapshot_ipv6_count && i<3;++i)
|
||||
memcpy(out->runtime.ipv6_addresses[i].addr,snapshot_ipv6_bytes[i],16);
|
||||
out->runtime.active_profile=-1; out->runtime.last_error=INT32_MIN;
|
||||
memset(&out->runtime.ip,255,4); out->runtime.ap_client_count=255;
|
||||
if(snapshot_ipv6_case) {
|
||||
const uint8_t ip[4]={192,0,2,8};
|
||||
out->runtime.started=true; out->runtime.state=WIFI_MANAGER_STATE_ONLINE;
|
||||
out->runtime.active_profile=0; out->runtime.ip=0;
|
||||
if(snapshot_ipv4) memcpy(&out->runtime.ip,ip,sizeof(ip));
|
||||
out->runtime.ipv6_linklocal=snapshot_linklocal;
|
||||
out->runtime.ipv6_routable=snapshot_routable;
|
||||
}
|
||||
return ESP_OK;
|
||||
}
|
||||
esp_err_t mdns_service_get_settings(mdns_service_snapshot_t *out) {
|
||||
@@ -75,7 +93,7 @@ esp_err_t mdns_service_get_settings(mdns_service_snapshot_t *out) {
|
||||
memset(out->suffix,'s',55); strcpy(out->hostname,"sak-"); memset(out->hostname+4,'s',55);
|
||||
return ESP_OK;
|
||||
}
|
||||
const char *wifi_manager_state_to_string(wifi_manager_state_t state) { (void)state; return "waiting-ip"; }
|
||||
const char *wifi_manager_state_to_string(wifi_manager_state_t state) { return state==WIFI_MANAGER_STATE_ONLINE ? "online" : "waiting-ip"; }
|
||||
static void network_begin(const issued_t *identity,const char *body) {
|
||||
begin("/api/settings/network-operation",body?HTTP_POST:HTTP_GET,body); same_origin();
|
||||
if(body) add("Content-Type","application/json");
|
||||
@@ -117,8 +135,35 @@ static void network_settings_tests(void) {
|
||||
network_begin(&user,NULL); network_expect("403 Forbidden",false);
|
||||
network_begin(&user,patch_body); network_expect("403 Forbidden",false);
|
||||
network_begin(&admin,NULL); req.uri="/api/settings/network"; network_expect("200 OK",true);
|
||||
assert(strlen(output)<2048 && strstr(output,"\\u00ff") && strstr(output,"\"generation\":4294967295"));
|
||||
printf("PASS Network maximum escaped snapshot: %zu bytes, no secret fields\n",strlen(output));
|
||||
assert(strlen(output)<WEB_NETWORK_SNAPSHOT_MAX && strstr(output,"\\u00ff") && strstr(output,"\"generation\":4294967295"));
|
||||
assert(strstr(output,"\"ipv6_addresses\":[]"));
|
||||
assert(strstr(output,"\"ipv6_linklocal\":false,\"ipv6_routable\":false"));
|
||||
snapshot_ipv6_case=true;
|
||||
for(unsigned flags=0;flags<8;++flags) {
|
||||
snapshot_ipv4=(flags&4)!=0; snapshot_linklocal=(flags&1)!=0; snapshot_routable=(flags&2)!=0;
|
||||
network_begin(&admin,NULL); req.uri="/api/settings/network"; network_expect("200 OK",true);
|
||||
assert(strstr(output,"\"state\":\"online\""));
|
||||
assert(strstr(output,snapshot_ipv4 ? "\"ip\":\"192.0.2.8\"" : "\"ip\":\"0.0.0.0\""));
|
||||
assert(strstr(output,snapshot_linklocal ? "\"ipv6_linklocal\":true" : "\"ipv6_linklocal\":false"));
|
||||
assert(strstr(output,snapshot_routable ? "\"ipv6_routable\":true" : "\"ipv6_routable\":false"));
|
||||
assert(strlen(output)<WEB_NETWORK_SNAPSHOT_MAX);
|
||||
}
|
||||
snapshot_ipv6_case=false;
|
||||
for(snapshot_ipv6_count=1;snapshot_ipv6_count<=3;++snapshot_ipv6_count) {
|
||||
network_begin(&admin,NULL); req.uri="/api/settings/network"; network_expect("200 OK",true);
|
||||
assert(strstr(output,"\"ipv6_addresses\":[\"fe80:0000:0000:0000:ea3d:c1ff:fefa:7058\""));
|
||||
assert((strstr(output,"fd39:b378:78f2:c715:ea3d:c1ff:fefa:7058")!=NULL)==(snapshot_ipv6_count>=2));
|
||||
assert((strstr(output,"2001:0db8:0001:0002:0003:0004:0005:0006")!=NULL)==(snapshot_ipv6_count==3));
|
||||
assert(strlen(output)<WEB_NETWORK_SNAPSHOT_MAX);
|
||||
}
|
||||
printf("PASS Network maximum escaped SSIDs plus three IPv6 addresses: %zu bytes, no secret fields\n",strlen(output));
|
||||
assert(strlen(output)>2048);
|
||||
/* Corrupt owner counts fail closed instead of reading beyond the bounded copy. */
|
||||
network_begin(&admin,NULL); req.uri="/api/settings/network"; network_expect("503 Service Unavailable",true);
|
||||
snapshot_ipv6_count=0;
|
||||
network_begin(&admin,NULL); req.uri="/api/settings/network"; network_expect("200 OK",true);
|
||||
assert(strstr(output,"\"ipv6_addresses\":[]") && !strstr(output,"fd39:"));
|
||||
puts("PASS Network IPv6-only/dual-stack: availability flags, bounded network-order address list and stale clearing, no writes");
|
||||
snapshot_fail=true; network_begin(&admin,NULL); network_expect("503 Service Unavailable",true); snapshot_fail=false;
|
||||
network_begin(&user,NULL); network_expect("403 Forbidden",true);
|
||||
network_begin(&admin,NULL); req.uri="/api/settings/network?secret=x"; network_expect("400 Bad Request",true);
|
||||
|
||||
@@ -19,7 +19,10 @@ static struct httpd_data server = {.config.max_resp_headers = 8};
|
||||
static struct sock_db socket_state;
|
||||
static struct resp_hdr response_headers[8];
|
||||
static char scratch[1024], cookie_values[2][200];
|
||||
#if defined(HOST_NETWORK) || defined(HOST_BROKER)
|
||||
#if defined(HOST_NETWORK)
|
||||
#include "web_network_settings.h"
|
||||
static char output[WEB_NETWORK_SNAPSHOT_MAX];
|
||||
#elif defined(HOST_BROKER)
|
||||
static char output[2048];
|
||||
#else
|
||||
static char output[1024];
|
||||
@@ -161,6 +164,21 @@ static void auth_reset(void) {
|
||||
int main(void) {
|
||||
assert(store_tests() == 0); auth_reset();
|
||||
char token[65], csrf[65], session[65], cookies[200];
|
||||
begin("/api/login-challenge", HTTP_GET, NULL);
|
||||
add("Host", "[2001:0DB8:0:0:0:0:0:1]:443"); add("X-Login-Bootstrap", "1");
|
||||
expect("200 OK"); token_from(cookie_values[0], token); csrf_from(csrf);
|
||||
for (unsigned mode = 0; mode < 3; ++mode) {
|
||||
begin("/api/login", HTTP_POST, good_body);
|
||||
add("Host", mode == 0 ? "[2001:db8::2]" : "[2001:db8::1]");
|
||||
add("Origin", mode == 1 ? "https://[2001:db8::2]" :
|
||||
mode == 0 ? "https://[2001:db8::2]" : "https://[2001:DB8::1]:443");
|
||||
add("Content-Type", "application/json"); add("X-CSRF-Token", csrf);
|
||||
snprintf(cookies, sizeof(cookies), "__Host-sak-prelogin=%s", token); add("Cookie", cookies);
|
||||
expect(mode == 2 ? "200 OK" : "403 Forbidden");
|
||||
assert(password_calls == (mode == 2 ? 1U : 0U));
|
||||
}
|
||||
puts("PASS: IPv6 challenge canonical binding, cross-address challenge replay and mismatched Origin rejected before password verification");
|
||||
auth_reset();
|
||||
challenge(token, csrf);
|
||||
begin("/api/login-challenge", HTTP_GET, NULL); add("Host", "device.example"); add("X-Login-Bootstrap", "1");
|
||||
snprintf(cookies, sizeof(cookies), "__Host-sak-prelogin=%s", token); add("Cookie", cookies);
|
||||
|
||||
Reference in New Issue
Block a user