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
+56 -14
View File
@@ -21,6 +21,7 @@
#include "lwip/inet.h"
#include "lwip/sockets.h"
#include "lwip/tcp.h"
#include "mdns_service.h"
#include "sdkconfig.h"
#include "secure_random.h"
#include "serial_service.h"
@@ -80,7 +81,7 @@ typedef struct {
size_t tx_length;
uint8_t rx_buffer[SSH_TRANSPORT_IO_BUFFER_SIZE];
uint8_t tx_buffer[SSH_TRANSPORT_IO_BUFFER_SIZE];
char peer[48];
char peer[SSH_TRANSPORT_PEER_CAPACITY];
} ssh_slot_t;
static portMUX_TYPE s_lock = portMUX_INITIALIZER_UNLOCKED;
@@ -675,7 +676,7 @@ static esp_err_t create_context(void)
static esp_err_t create_listener(void)
{
int socket_fd = socket(AF_INET, SOCK_STREAM, IPPROTO_IP);
int socket_fd = socket(AF_INET6, SOCK_STREAM, IPPROTO_TCP);
if (socket_fd < 0) {
return ESP_FAIL;
}
@@ -683,12 +684,17 @@ static esp_err_t create_listener(void)
int enabled = 1;
(void)setsockopt(socket_fd, SOL_SOCKET, SO_REUSEADDR,
&enabled, sizeof(enabled));
struct sockaddr_in address = {
.sin_family = AF_INET,
.sin_port = htons(SSH_TRANSPORT_PORT),
.sin_addr.s_addr = htonl(INADDR_ANY),
/* IDF lwIP binds :: to IPADDR_TYPE_ANY when V6ONLY is disabled.
* One listener preserves the shared accept budget and socket footprint. */
int ipv6_only = 0;
struct sockaddr_in6 address = {
.sin6_family = AF_INET6,
.sin6_port = htons(SSH_TRANSPORT_PORT),
.sin6_addr = IN6ADDR_ANY_INIT,
};
if (bind(socket_fd, (struct sockaddr *)&address, sizeof(address)) < 0 ||
if (setsockopt(socket_fd, IPPROTO_IPV6, IPV6_V6ONLY,
&ipv6_only, sizeof(ipv6_only)) < 0 ||
bind(socket_fd, (struct sockaddr *)&address, sizeof(address)) < 0 ||
listen(socket_fd, SSH_TRANSPORT_LISTEN_BACKLOG) < 0 ||
set_nonblocking(socket_fd) != ESP_OK) {
close(socket_fd);
@@ -717,12 +723,14 @@ static esp_err_t start_runtime(void)
s_context = NULL;
}
}
mdns_service_set_ssh_available(error == ESP_OK);
return error;
}
static esp_err_t stop_runtime(void)
{
close_socket(&s_listen_fd);
mdns_service_set_ssh_available(false);
for (size_t index = 0U; index < SSH_TRANSPORT_MAX_SESSIONS; ++index) {
request_slot_close(&s_slots[index], false);
}
@@ -850,28 +858,56 @@ static ssh_slot_t *find_free_slot(size_t *slot_index)
static void format_peer(const struct sockaddr_storage *address,
char *output, size_t output_size)
{
if (output_size == 0U) return;
output[0] = '\0';
int written = -1;
if (address->ss_family == AF_INET) {
const struct sockaddr_in *ipv4 = (const struct sockaddr_in *)address;
char host[INET_ADDRSTRLEN] = {0};
if (inet_ntop(AF_INET, &ipv4->sin_addr, host, sizeof(host)) != NULL) {
(void)snprintf(output, output_size, "%s:%u", host,
(unsigned int)ntohs(ipv4->sin_port));
written = snprintf(output, output_size, "%s:%u", host,
(unsigned int)ntohs(ipv4->sin_port));
}
} else if (address->ss_family == AF_INET6) {
const struct sockaddr_in6 *ipv6 = (const struct sockaddr_in6 *)address;
char host[INET6_ADDRSTRLEN] = {0};
if (inet_ntop(AF_INET6, &ipv6->sin6_addr, host, sizeof(host)) != NULL) {
(void)snprintf(output, output_size, "[%s]:%u", host,
(unsigned int)ntohs(ipv6->sin6_port));
/* lwIP accept supplies the interface zone in sin6_scope_id.
* Retain it for link-local peers; never infer an interface by name. */
if (ipv6->sin6_scope_id != 0U) {
written = snprintf(output, output_size, "[%s%%%" PRIu32 "]:%u",
host, (uint32_t)ipv6->sin6_scope_id,
(unsigned int)ntohs(ipv6->sin6_port));
} else {
written = snprintf(output, output_size, "[%s]:%u", host,
(unsigned int)ntohs(ipv6->sin6_port));
}
}
}
if (output[0] == '\0') {
/* A truncated scoped endpoint must not look like a usable address. */
if (written < 0 || (size_t)written >= output_size) {
strncpy(output, "unknown", output_size - 1U);
output[output_size - 1U] = '\0';
}
}
static void listener_failed(void)
{
close_socket(&s_listen_fd);
mdns_service_set_ssh_available(false);
for (size_t index = 0U; index < SSH_TRANSPORT_MAX_SESSIONS; ++index) {
request_slot_close(&s_slots[index], false);
}
taskENTER_CRITICAL(&s_lock);
s_running = false;
s_cleanup_pending = true;
s_last_error = ESP_FAIL;
if (s_management_generation < UINT32_MAX) ++s_management_generation;
taskEXIT_CRITICAL(&s_lock);
/* process_slots retires the context only after every session is free.
* Do not alter an already admitted lifecycle command or auto-restart. */
}
static void accept_connections(void)
{
if (s_listen_fd < 0 || s_context == NULL) {
@@ -881,13 +917,19 @@ static void accept_connections(void)
for (unsigned int accepted_count = 0U;
accepted_count < SSH_TRANSPORT_ACCEPT_BUDGET;
++accepted_count) {
struct sockaddr_storage peer_address;
struct sockaddr_storage peer_address = {0};
socklen_t peer_length = sizeof(peer_address);
int socket_fd = accept(s_listen_fd, (struct sockaddr *)&peer_address,
&peer_length);
if (socket_fd < 0) {
if (errno != EAGAIN && errno != EWOULDBLOCK) {
int error = errno;
if (error != EAGAIN && error != EWOULDBLOCK && error != EINTR) {
add_counter(&s_counters.io_failures, 1U);
/* Resource pressure/aborted peers do not invalidate a listener. */
if (error == EBADF || error == EINVAL || error == ENOTSOCK ||
error == EOPNOTSUPP) {
listener_failed();
}
}
return;
}