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:
+56
-14
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user