Files
ESP32_Serial_Swiss_Army_Knife/src/ssh_transport.c
T

1586 lines
49 KiB
C

/* SPDX-License-Identifier: GPL-3.0-only */
/* Role-aware SSH stream transport with two fixed broker sessions. */
#include "ssh_transport.h"
#include <errno.h>
#include <fcntl.h>
#include <inttypes.h>
#include <stdio.h>
#include <string.h>
#include "admin_ssh_console.h"
#include "esp_heap_caps.h"
#include "esp_log.h"
#include "esp_timer.h"
#include "freertos/FreeRTOS.h"
#include "freertos/semphr.h"
#include "freertos/task.h"
#include "lwip/inet.h"
#include "lwip/sockets.h"
#include "lwip/tcp.h"
#include "sdkconfig.h"
#include "secure_random.h"
#include "serial_service.h"
#include "ssh_security.h"
#include "user_database.h"
#include <wolfssl/wolfcrypt/memory.h>
#include <wolfssl/wolfcrypt/random.h>
#include <wolfssh/ssh.h>
#if defined(CONFIG_MBEDTLS_HARDWARE_AES) && CONFIG_MBEDTLS_HARDWARE_AES
#error "Concurrent mbedTLS/wolfSSH operation requires mbedTLS software AES"
#endif
#if !defined(NO_WOLFSSL_ESP32_CRYPT_AES) || \
!defined(NO_WOLFSSL_ESP32_CRYPT_HASH)
#error "wolfSSH AES/SHA must not use independently locked ESP32 crypto hardware"
#endif
#define SSH_TRANSPORT_TASK_STACK_SIZE 20480U
#define SSH_TRANSPORT_TASK_PRIORITY 5U
#define SSH_TRANSPORT_TASK_CORE 1
#define SSH_TRANSPORT_LOOP_DELAY_MS 10U
#define SSH_TRANSPORT_COMMAND_TIMEOUT_MS 10000U
#define SSH_TRANSPORT_RECONCILE_INTERVAL_US 250000LL
#define SSH_TRANSPORT_LISTEN_BACKLOG 2
#define SSH_TRANSPORT_ACCEPT_BUDGET 4U
#define SSH_TRANSPORT_MAX_PENDING_HANDSHAKES SSH_TRANSPORT_MAX_SESSIONS
#define SSH_TRANSPORT_MAX_AUTH_ATTEMPTS 3U
#define SSH_TRANSPORT_GENERATION_MAX 0x3fffffffU
#define SSH_TRANSPORT_WOLFSSH_READ_BUDGET 2048U
static const char *TAG = "ssh_transport";
typedef struct {
ssh_transport_session_state_t state;
uint32_t generation;
uint32_t session_id;
int socket_fd;
WOLFSSH *ssh;
session_broker_client_id_t broker_client_id;
ssh_transport_session_route_t route;
user_principal_t principal;
user_principal_t pending_principal;
bool principal_valid;
bool pending_principal_valid;
bool authenticated;
bool shell_requested;
uint8_t authentication_attempts;
word32 io_read_budget;
bool writer;
bool close_requested;
int64_t handshake_deadline_us;
int64_t last_reconcile_us;
size_t rx_offset;
size_t rx_length;
size_t tx_offset;
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];
} ssh_slot_t;
static portMUX_TYPE s_lock = portMUX_INITIALIZER_UNLOCKED;
static TaskHandle_t s_task;
static ssh_slot_t s_slots[SSH_TRANSPORT_MAX_SESSIONS];
static ssh_transport_session_snapshot_t
s_session_snapshots[SSH_TRANSPORT_MAX_SESSIONS];
static uint32_t s_external_close_id[SSH_TRANSPORT_MAX_SESSIONS];
static ssh_transport_counters_t s_counters;
static SemaphoreHandle_t s_command_mutex;
static bool s_initializing;
static bool s_initialized;
static bool s_running;
static bool s_transitioning;
static bool s_desired_running;
static bool s_cleanup_pending;
static uint32_t s_requested_sequence;
static uint32_t s_completed_sequence;
static esp_err_t s_command_result = ESP_ERR_INVALID_STATE;
static esp_err_t s_last_error = ESP_ERR_INVALID_STATE;
/* Owned exclusively by the transport task. */
static WOLFSSH_CTX *s_context;
static int s_listen_fd = -1;
static void add_counter(uint64_t *counter, uint64_t value)
{
taskENTER_CRITICAL(&s_lock);
*counter += value;
taskEXIT_CRITICAL(&s_lock);
}
static uint32_t next_generation(uint32_t generation)
{
return generation >= SSH_TRANSPORT_GENERATION_MAX ? 1U : generation + 1U;
}
static uint32_t make_session_id(size_t slot_index, uint32_t generation)
{
return (generation << 2U) | (uint32_t)(slot_index + 1U);
}
static void notify_task(void)
{
if (s_task != NULL) {
xTaskNotifyGive(s_task);
}
}
static admin_ssh_console_token_t admin_console_token(const ssh_slot_t *slot,
size_t slot_index)
{
return (admin_ssh_console_token_t){
.slot_index = (uint8_t)slot_index,
.session_id = slot->session_id,
.slot_generation = slot->generation,
};
}
static void publish_slot(const ssh_slot_t *slot, size_t slot_index)
{
ssh_transport_session_snapshot_t snapshot = {
.active = slot->state != SSH_TRANSPORT_SESSION_FREE,
.authenticated = slot->authenticated,
.principal_valid = slot->principal_valid,
.writer = slot->writer,
.close_requested = slot->close_requested,
.rx_pending = slot->rx_length > slot->rx_offset,
.tx_pending = slot->tx_length > slot->tx_offset,
.session_id = slot->session_id,
.generation = slot->generation,
.socket_fd = slot->socket_fd,
.broker_client_id = slot->broker_client_id,
.state = slot->state,
.route = slot->route,
.user_role = slot->principal_valid ? slot->principal.role : USER_ROLE_USER,
.auth_method = slot->principal_valid
? slot->principal.method
: USER_AUTH_METHOD_PASSWORD,
};
if (slot->route == SSH_TRANSPORT_ROUTE_ADMIN_CONSOLE) {
admin_ssh_console_session_snapshot_t admin_snapshot;
admin_ssh_console_token_t token = admin_console_token(slot, slot_index);
if (admin_ssh_console_get_session_snapshot(&token, &admin_snapshot) == ESP_OK) {
snapshot.admin_command_pending = admin_snapshot.command_pending;
snapshot.admin_output_pending = (uint32_t)admin_snapshot.output_length;
snapshot.tx_pending = snapshot.tx_pending || admin_snapshot.output_pending;
}
}
if (slot->principal_valid) {
memcpy(snapshot.username, slot->principal.username,
slot->principal.username_length);
}
memcpy(snapshot.peer, slot->peer, sizeof(snapshot.peer));
taskENTER_CRITICAL(&s_lock);
s_session_snapshots[slot_index] = snapshot;
taskEXIT_CRITICAL(&s_lock);
}
static bool consume_external_close(const ssh_slot_t *slot, size_t slot_index)
{
taskENTER_CRITICAL(&s_lock);
bool requested = s_external_close_id[slot_index] != 0U &&
s_external_close_id[slot_index] == slot->session_id;
if (requested || slot->state == SSH_TRANSPORT_SESSION_FREE) {
s_external_close_id[slot_index] = 0U;
}
taskEXIT_CRITICAL(&s_lock);
return requested;
}
static void *ssh_malloc(size_t size)
{
return heap_caps_malloc_prefer(size, 2,
MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT,
MALLOC_CAP_INTERNAL | MALLOC_CAP_8BIT);
}
static void ssh_free(void *pointer)
{
heap_caps_free(pointer);
}
static void *ssh_realloc(void *pointer, size_t size)
{
return heap_caps_realloc_prefer(pointer, size, 2,
MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT,
MALLOC_CAP_INTERNAL | MALLOC_CAP_8BIT);
}
static int ssh_seed(OS_Seed *seed, byte *output, word32 size)
{
(void)seed;
return secure_random_fill(output, size) == ESP_OK ? 0 : -1;
}
static int bounded_ssh_receive(WOLFSSH *ssh, void *data, word32 size,
void *context)
{
(void)ssh;
ssh_slot_t *slot = (ssh_slot_t *)context;
if (slot == NULL || data == NULL || slot->socket_fd < 0) {
return WS_CBIO_ERR_GENERAL;
}
if (slot->io_read_budget == 0U) {
return WS_CBIO_ERR_WANT_READ;
}
word32 request_size = size;
if (request_size > slot->io_read_budget) {
request_size = slot->io_read_budget;
}
int received = recv(slot->socket_fd, data, request_size, 0);
if (received > 0) {
slot->io_read_budget -= (word32)received;
return received;
}
if (received == 0) {
return WS_CBIO_ERR_CONN_CLOSE;
}
if (errno == EAGAIN || errno == EWOULDBLOCK) {
return WS_CBIO_ERR_WANT_READ;
}
if (errno == EINTR) {
return WS_CBIO_ERR_ISR;
}
if (errno == ECONNRESET) {
return WS_CBIO_ERR_CONN_RST;
}
if (errno == ECONNABORTED) {
return WS_CBIO_ERR_CONN_CLOSE;
}
return WS_CBIO_ERR_GENERAL;
}
static int allowed_auth_types(WOLFSSH *ssh, void *context)
{
(void)ssh;
(void)context;
return WOLFSSH_USERAUTH_PASSWORD | WOLFSSH_USERAUTH_PUBLICKEY;
}
static void clear_pending_principal(ssh_slot_t *slot)
{
if (slot != NULL) {
secure_wipe(&slot->pending_principal, sizeof(slot->pending_principal));
slot->pending_principal_valid = false;
}
}
static bool complete_authentication_attempt(ssh_slot_t *slot, bool failed)
{
add_counter(&s_counters.authentication_attempts, 1U);
if (failed) {
add_counter(&s_counters.authentication_failures, 1U);
}
if (slot != NULL && slot->authentication_attempts < UINT8_MAX) {
++slot->authentication_attempts;
}
if (!failed || slot == NULL ||
slot->authentication_attempts < SSH_TRANSPORT_MAX_AUTH_ATTEMPTS) {
return true;
}
slot->close_requested = true;
if (slot->socket_fd >= 0) {
(void)shutdown(slot->socket_fd, SHUT_RDWR);
}
return false;
}
static int authenticate_password(ssh_slot_t *slot,
WS_UserAuthData *authentication)
{
clear_pending_principal(slot);
if (authentication->sf.password.hasNewPassword != 0U) {
(void)complete_authentication_attempt(slot, true);
return slot->close_requested ? WOLFSSH_USERAUTH_REJECTED
: WOLFSSH_USERAUTH_INVALID_AUTHTYPE;
}
user_principal_t principal;
bool authenticated = false;
esp_err_t error = user_database_authenticate_password(
authentication->username, authentication->usernameSz,
authentication->sf.password.password,
authentication->sf.password.passwordSz,
&principal, &authenticated);
if (error == ESP_OK && authenticated) {
(void)complete_authentication_attempt(slot, false);
slot->principal = principal;
slot->principal_valid = true;
slot->authenticated = true;
return WOLFSSH_USERAUTH_SUCCESS;
}
secure_wipe(&principal, sizeof(principal));
bool retry = complete_authentication_attempt(slot, true);
if (!retry) {
return WOLFSSH_USERAUTH_REJECTED;
}
return error == ESP_OK ? WOLFSSH_USERAUTH_INVALID_PASSWORD
: WOLFSSH_USERAUTH_FAILURE;
}
static int authenticate_public_key(ssh_slot_t *slot,
WS_UserAuthData *authentication)
{
WS_UserAuthData_PublicKey *public_key = &authentication->sf.publicKey;
clear_pending_principal(slot);
user_principal_t principal;
bool authorized = false;
esp_err_t error = ESP_OK;
if (public_key->isCert != 0U) {
memset(&principal, 0, sizeof(principal));
} else {
error = user_database_authorize_ssh_public_key(
authentication->username, authentication->usernameSz,
public_key->publicKeyType, public_key->publicKeyTypeSz,
public_key->publicKey, public_key->publicKeySz,
&principal, &authorized);
}
if (error != ESP_OK || !authorized) {
secure_wipe(&principal, sizeof(principal));
if (public_key->hasSignature == 0U) {
return error == ESP_OK ? WOLFSSH_USERAUTH_INVALID_PUBLICKEY
: WOLFSSH_USERAUTH_FAILURE;
}
bool retry = complete_authentication_attempt(slot, true);
if (!retry) {
return WOLFSSH_USERAUTH_REJECTED;
}
return error == ESP_OK ? WOLFSSH_USERAUTH_INVALID_PUBLICKEY
: WOLFSSH_USERAUTH_FAILURE;
}
if (public_key->hasSignature != 0U) {
slot->pending_principal = principal;
slot->pending_principal_valid = true;
}
secure_wipe(&principal, sizeof(principal));
return WOLFSSH_USERAUTH_SUCCESS;
}
static int authenticate_user(byte authentication_type,
WS_UserAuthData *authentication,
void *context)
{
ssh_slot_t *slot = (ssh_slot_t *)context;
if (slot == NULL || authentication == NULL ||
authentication_type != authentication->type) {
clear_pending_principal(slot);
return WOLFSSH_USERAUTH_INVALID_AUTHTYPE;
}
if (authentication_type == WOLFSSH_USERAUTH_PASSWORD) {
return authenticate_password(slot, authentication);
}
if (authentication_type == WOLFSSH_USERAUTH_PUBLICKEY) {
return authenticate_public_key(slot, authentication);
}
clear_pending_principal(slot);
return WOLFSSH_USERAUTH_INVALID_AUTHTYPE;
}
static int authentication_result(byte result, WS_UserAuthData *authentication,
void *context)
{
ssh_slot_t *slot = (ssh_slot_t *)context;
if (slot == NULL || authentication == NULL ||
authentication->type != WOLFSSH_USERAUTH_PUBLICKEY ||
authentication->sf.publicKey.hasSignature == 0U) {
clear_pending_principal(slot);
return WS_ERROR;
}
if (result != WOLFSSH_USERAUTH_SUCCESS) {
(void)complete_authentication_attempt(slot, true);
clear_pending_principal(slot);
return WS_SUCCESS;
}
bool current = false;
esp_err_t error = slot->pending_principal_valid
? user_database_principal_is_current(
&slot->pending_principal, &current)
: ESP_ERR_INVALID_STATE;
if (error != ESP_OK || !current) {
(void)complete_authentication_attempt(slot, true);
clear_pending_principal(slot);
return WS_ERROR;
}
(void)complete_authentication_attempt(slot, false);
slot->principal = slot->pending_principal;
slot->principal_valid = true;
slot->authenticated = true;
clear_pending_principal(slot);
return WS_SUCCESS;
}
static int accept_shell(WOLFSSH_CHANNEL *channel, void *context)
{
(void)channel;
ssh_slot_t *slot = (ssh_slot_t *)context;
if (slot == NULL) {
return 1;
}
slot->shell_requested = true;
return 0;
}
static int reject_channel_request(WOLFSSH_CHANNEL *channel, void *context)
{
(void)channel;
(void)context;
add_counter(&s_counters.request_rejections, 1U);
return 1;
}
static bool wolfssh_would_block(WOLFSSH *ssh, int result)
{
int error = ssh != NULL ? wolfSSH_get_error(ssh) : result;
return result == WS_WANT_READ || result == WS_WANT_WRITE ||
result == WS_REKEYING || result == WS_WINDOW_FULL ||
result == WS_CHAN_RXD ||
error == WS_WANT_READ || error == WS_WANT_WRITE ||
error == WS_REKEYING || error == WS_WINDOW_FULL ||
error == WS_CHAN_RXD;
}
static void close_socket(int *socket_fd)
{
if (*socket_fd >= 0) {
shutdown(*socket_fd, SHUT_RDWR);
close(*socket_fd);
*socket_fd = -1;
}
}
static bool cleanup_slot(ssh_slot_t *slot)
{
if (slot->route == SSH_TRANSPORT_ROUTE_ADMIN_CONSOLE) {
size_t slot_index = (size_t)(slot - s_slots);
admin_ssh_console_token_t token = admin_console_token(slot, slot_index);
admin_ssh_console_close(&token);
}
if (slot->ssh != NULL) {
(void)wolfSSH_shutdown(slot->ssh);
wolfSSH_free(slot->ssh);
slot->ssh = NULL;
}
close_socket(&slot->socket_fd);
if (slot->broker_client_id != SESSION_BROKER_NO_CLIENT) {
esp_err_t error = session_broker_disconnect(slot->broker_client_id);
if (error != ESP_OK && error != ESP_ERR_NOT_FOUND) {
add_counter(&s_counters.broker_failures, 1U);
slot->state = SSH_TRANSPORT_SESSION_CLOSING;
return false;
}
slot->broker_client_id = SESSION_BROKER_NO_CLIENT;
add_counter(&s_counters.disconnections, 1U);
}
uint32_t generation = slot->generation;
memset(slot, 0, sizeof(*slot));
slot->state = SSH_TRANSPORT_SESSION_FREE;
slot->generation = generation;
slot->socket_fd = -1;
return true;
}
static void request_slot_close(ssh_slot_t *slot, bool revoked)
{
if (slot->state == SSH_TRANSPORT_SESSION_FREE) {
return;
}
if (!slot->close_requested && revoked) {
add_counter(&s_counters.session_revocations, 1U);
}
slot->close_requested = true;
}
static esp_err_t set_nonblocking(int socket_fd)
{
int flags = fcntl(socket_fd, F_GETFL, 0);
if (flags < 0 || fcntl(socket_fd, F_SETFL, flags | O_NONBLOCK) < 0) {
return ESP_FAIL;
}
return ESP_OK;
}
static esp_err_t create_context(void)
{
uint8_t private_key[SSH_SECURITY_PRIVATE_KEY_DER_CAPACITY] = {0};
size_t private_key_length = 0U;
esp_err_t error = ssh_security_copy_private_key(
private_key, sizeof(private_key), &private_key_length);
if (error != ESP_OK) {
secure_wipe(private_key, sizeof(private_key));
return error;
}
WOLFSSH_CTX *context = wolfSSH_CTX_new(WOLFSSH_ENDPOINT_SERVER, NULL);
if (context == NULL) {
secure_wipe(private_key, sizeof(private_key));
return ESP_ERR_NO_MEM;
}
int result = wolfSSH_CTX_UsePrivateKey_buffer(
context, private_key, (word32)private_key_length, WOLFSSH_FORMAT_ASN1);
secure_wipe(private_key, sizeof(private_key));
if (result != WS_SUCCESS) {
wolfSSH_CTX_free(context);
return ESP_FAIL;
}
wolfSSH_SetIORecv(context, bounded_ssh_receive);
wolfSSH_SetUserAuth(context, authenticate_user);
wolfSSH_SetUserAuthTypes(context, allowed_auth_types);
wolfSSH_SetUserAuthResult(context, authentication_result);
(void)wolfSSH_CTX_SetChannelReqShellCb(context, accept_shell);
(void)wolfSSH_CTX_SetChannelReqExecCb(context, reject_channel_request);
(void)wolfSSH_CTX_SetChannelReqSubsysCb(context, reject_channel_request);
(void)wolfSSH_CTX_SetBanner(
context, "ESP32 Serial Swiss Army Knife - authenticated serial transport\r\n");
s_context = context;
return ESP_OK;
}
static esp_err_t create_listener(void)
{
int socket_fd = socket(AF_INET, SOCK_STREAM, IPPROTO_IP);
if (socket_fd < 0) {
return ESP_FAIL;
}
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),
};
if (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);
return ESP_FAIL;
}
s_listen_fd = socket_fd;
return ESP_OK;
}
static esp_err_t start_runtime(void)
{
esp_err_t error = create_context();
if (error == ESP_OK) {
error = create_listener();
}
if (error != ESP_OK) {
close_socket(&s_listen_fd);
if (s_context != NULL) {
wolfSSH_CTX_free(s_context);
s_context = NULL;
}
}
return error;
}
static esp_err_t stop_runtime(void)
{
close_socket(&s_listen_fd);
for (size_t index = 0U; index < SSH_TRANSPORT_MAX_SESSIONS; ++index) {
request_slot_close(&s_slots[index], false);
}
bool all_free = false;
for (unsigned int attempt = 0U; attempt < 100U; ++attempt) {
all_free = true;
for (size_t index = 0U; index < SSH_TRANSPORT_MAX_SESSIONS; ++index) {
ssh_slot_t *slot = &s_slots[index];
if (slot->state != SSH_TRANSPORT_SESSION_FREE) {
all_free = false;
(void)cleanup_slot(slot);
publish_slot(slot, index);
}
}
if (all_free) {
break;
}
vTaskDelay(pdMS_TO_TICKS(10U));
}
all_free = true;
for (size_t index = 0U; index < SSH_TRANSPORT_MAX_SESSIONS; ++index) {
if (s_slots[index].state != SSH_TRANSPORT_SESSION_FREE) {
all_free = false;
break;
}
}
if (s_context != NULL) {
wolfSSH_CTX_free(s_context);
s_context = NULL;
}
return all_free ? ESP_OK : ESP_ERR_TIMEOUT;
}
static void complete_lifecycle(uint32_t sequence, bool running,
esp_err_t result)
{
taskENTER_CRITICAL(&s_lock);
s_running = running;
s_transitioning = false;
s_completed_sequence = sequence;
s_command_result = result;
s_last_error = result;
if (!running) {
s_cleanup_pending = result != ESP_OK;
}
if (result == ESP_OK) {
if (running) {
++s_counters.starts;
} else {
++s_counters.stops;
}
} else if (s_desired_running) {
++s_counters.start_failures;
}
taskEXIT_CRITICAL(&s_lock);
}
static void handle_lifecycle(void)
{
bool desired;
bool running;
bool transitioning;
bool cleanup_pending;
uint32_t sequence;
taskENTER_CRITICAL(&s_lock);
desired = s_desired_running;
running = s_running;
transitioning = s_transitioning;
cleanup_pending = s_cleanup_pending;
sequence = s_requested_sequence;
taskEXIT_CRITICAL(&s_lock);
if (!transitioning) {
return;
}
if (desired == running && !(cleanup_pending && !desired)) {
complete_lifecycle(sequence, running, ESP_OK);
return;
}
if (desired) {
esp_err_t error = start_runtime();
complete_lifecycle(sequence, error == ESP_OK, error);
if (error == ESP_OK) {
ESP_LOGI(TAG, "SSH listening on TCP port %u", SSH_TRANSPORT_PORT);
} else {
ESP_LOGE(TAG, "SSH startup failed: %s", esp_err_to_name(error));
}
} else {
esp_err_t error = stop_runtime();
complete_lifecycle(sequence, false, error);
if (error == ESP_OK) {
ESP_LOGI(TAG, "SSH stopped");
} else {
ESP_LOGE(TAG, "SSH listener stopped but session cleanup failed: %s",
esp_err_to_name(error));
}
}
}
static ssh_slot_t *find_free_slot(size_t *slot_index)
{
unsigned int pending_handshakes = 0U;
for (size_t index = 0U; index < SSH_TRANSPORT_MAX_SESSIONS; ++index) {
if (s_slots[index].state == SSH_TRANSPORT_SESSION_HANDSHAKE) {
++pending_handshakes;
}
}
if (pending_handshakes >= SSH_TRANSPORT_MAX_PENDING_HANDSHAKES) {
return NULL;
}
for (size_t index = 0U; index < SSH_TRANSPORT_MAX_SESSIONS; ++index) {
if (s_slots[index].state == SSH_TRANSPORT_SESSION_FREE) {
*slot_index = index;
return &s_slots[index];
}
}
return NULL;
}
static void format_peer(const struct sockaddr_storage *address,
char *output, size_t output_size)
{
output[0] = '\0';
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));
}
} 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));
}
}
if (output[0] == '\0') {
strncpy(output, "unknown", output_size - 1U);
output[output_size - 1U] = '\0';
}
}
static void accept_connections(void)
{
if (s_listen_fd < 0 || s_context == NULL) {
return;
}
for (unsigned int accepted_count = 0U;
accepted_count < SSH_TRANSPORT_ACCEPT_BUDGET;
++accepted_count) {
struct sockaddr_storage peer_address;
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) {
add_counter(&s_counters.io_failures, 1U);
}
return;
}
add_counter(&s_counters.tcp_connections, 1U);
size_t slot_index = 0U;
ssh_slot_t *slot = find_free_slot(&slot_index);
if (slot == NULL) {
add_counter(&s_counters.capacity_rejections, 1U);
close(socket_fd);
continue;
}
if (set_nonblocking(socket_fd) != ESP_OK) {
add_counter(&s_counters.io_failures, 1U);
close(socket_fd);
continue;
}
int enabled = 1;
(void)setsockopt(socket_fd, IPPROTO_TCP, TCP_NODELAY,
&enabled, sizeof(enabled));
uint32_t generation = next_generation(slot->generation);
memset(slot, 0, sizeof(*slot));
slot->state = SSH_TRANSPORT_SESSION_HANDSHAKE;
slot->generation = generation;
slot->session_id = make_session_id(slot_index, generation);
slot->socket_fd = socket_fd;
slot->handshake_deadline_us = esp_timer_get_time() +
(int64_t)SSH_TRANSPORT_HANDSHAKE_TIMEOUT_SECONDS * 1000000LL;
format_peer(&peer_address, slot->peer, sizeof(slot->peer));
slot->ssh = wolfSSH_new(s_context);
if (slot->ssh == NULL || wolfSSH_set_fd(slot->ssh, socket_fd) != WS_SUCCESS) {
add_counter(&s_counters.handshake_failures, 1U);
(void)cleanup_slot(slot);
publish_slot(slot, slot_index);
continue;
}
wolfSSH_SetIOReadCtx(slot->ssh, slot);
wolfSSH_SetUserAuthCtx(slot->ssh, slot);
wolfSSH_SetUserAuthResultCtx(slot->ssh, slot);
wolfSSH_SetChannelReqCtx(slot->ssh, slot);
publish_slot(slot, slot_index);
}
}
static bool slot_principal_is_current(const ssh_slot_t *slot)
{
bool current = false;
return slot->principal_valid &&
user_database_principal_is_current(&slot->principal, &current) == ESP_OK &&
current;
}
static void disconnect_failed_admission(ssh_slot_t *slot)
{
if (slot->broker_client_id == SESSION_BROKER_NO_CLIENT) {
return;
}
esp_err_t error = session_broker_disconnect(slot->broker_client_id);
if (error == ESP_OK || error == ESP_ERR_NOT_FOUND) {
slot->broker_client_id = SESSION_BROKER_NO_CLIENT;
slot->writer = false;
add_counter(&s_counters.disconnections, 1U);
} else {
add_counter(&s_counters.broker_failures, 1U);
}
}
static esp_err_t connect_broker(ssh_slot_t *slot, size_t slot_index)
{
if (!serial_service_is_running()) {
esp_err_t error = serial_service_start();
if (error != ESP_OK && serial_service_is_running()) {
error = ESP_OK;
}
if (error != ESP_OK) {
return error;
}
}
if (!slot_principal_is_current(slot)) {
return ESP_ERR_INVALID_STATE;
}
char name[SESSION_BROKER_CLIENT_NAME_MAX + 1U];
int written = snprintf(name, sizeof(name), "ssh-%u-%" PRIu32,
(unsigned int)slot_index, slot->generation);
if (written <= 0 || (size_t)written >= sizeof(name)) {
return ESP_ERR_INVALID_SIZE;
}
esp_err_t error = session_broker_connect(
SESSION_BROKER_CLIENT_SSH, name, &slot->broker_client_id);
if (error != ESP_OK) {
return error;
}
add_counter(&s_counters.broker_connections, 1U);
if (!slot_principal_is_current(slot)) {
disconnect_failed_admission(slot);
return ESP_ERR_INVALID_STATE;
}
add_counter(&s_counters.writer_requests, 1U);
error = session_broker_request_writer(slot->broker_client_id);
if (error == ESP_OK) {
slot->writer = true;
add_counter(&s_counters.writer_grants, 1U);
} else if (error == ESP_ERR_INVALID_STATE) {
slot->writer = false;
add_counter(&s_counters.writer_denials, 1U);
} else {
return error;
}
if (!slot_principal_is_current(slot)) {
disconnect_failed_admission(slot);
return ESP_ERR_INVALID_STATE;
}
return ESP_OK;
}
static void process_handshake(ssh_slot_t *slot, size_t slot_index)
{
if (esp_timer_get_time() >= slot->handshake_deadline_us) {
add_counter(&s_counters.handshake_timeouts, 1U);
request_slot_close(slot, false);
return;
}
slot->io_read_budget = SSH_TRANSPORT_WOLFSSH_READ_BUDGET;
int result = wolfSSH_accept(slot->ssh);
if (result == WS_SUCCESS) {
bool principal_current = false;
esp_err_t principal_error = slot->principal_valid
? user_database_principal_is_current(
&slot->principal, &principal_current)
: ESP_ERR_INVALID_STATE;
if (!slot->authenticated || !slot->principal_valid) {
request_slot_close(slot, false);
return;
}
if (principal_error != ESP_OK || !principal_current) {
request_slot_close(slot, true);
return;
}
if (!slot->shell_requested ||
wolfSSH_GetSessionType(slot->ssh) != WOLFSSH_SESSION_SHELL) {
add_counter(&s_counters.request_rejections, 1U);
request_slot_close(slot, false);
return;
}
esp_err_t error;
if (slot->principal.role == USER_ROLE_USER) {
error = connect_broker(slot, slot_index);
if (error != ESP_OK) {
add_counter(&s_counters.broker_failures, 1U);
request_slot_close(slot, false);
return;
}
slot->route = SSH_TRANSPORT_ROUTE_BROKER;
} else if (slot->principal.role == USER_ROLE_ADMIN) {
admin_ssh_console_token_t token = admin_console_token(slot, slot_index);
error = admin_ssh_console_open(&token, &slot->principal);
if (error != ESP_OK) {
add_counter(&s_counters.admin_console_admission_failures, 1U);
request_slot_close(slot, false);
return;
}
slot->route = SSH_TRANSPORT_ROUTE_ADMIN_CONSOLE;
add_counter(&s_counters.admin_console_admissions, 1U);
} else {
request_slot_close(slot, true);
return;
}
if (!slot_principal_is_current(slot)) {
if (slot->route == SSH_TRANSPORT_ROUTE_BROKER) {
disconnect_failed_admission(slot);
}
request_slot_close(slot, true);
return;
}
slot->state = SSH_TRANSPORT_SESSION_ACTIVE;
slot->last_reconcile_us = esp_timer_get_time();
add_counter(&s_counters.handshake_successes, 1U);
return;
}
if (!wolfssh_would_block(slot->ssh, result)) {
add_counter(&s_counters.handshake_failures, 1U);
request_slot_close(slot, false);
}
}
static bool reconcile_writer(ssh_slot_t *slot)
{
int64_t now = esp_timer_get_time();
if (now - slot->last_reconcile_us < SSH_TRANSPORT_RECONCILE_INTERVAL_US) {
return true;
}
slot->last_reconcile_us = now;
bool principal_current = false;
esp_err_t principal_error = slot->principal_valid
? user_database_principal_is_current(
&slot->principal, &principal_current)
: ESP_ERR_INVALID_STATE;
if (principal_error != ESP_OK || !principal_current) {
request_slot_close(slot, true);
return true;
}
session_broker_client_snapshot_t snapshot;
esp_err_t error = session_broker_get_client_snapshot(
slot->broker_client_id, &snapshot);
if (error == ESP_ERR_TIMEOUT) {
return true;
}
if (error != ESP_OK) {
return false;
}
if (slot->writer && !snapshot.is_writer) {
slot->rx_offset = 0U;
slot->rx_length = 0U;
add_counter(&s_counters.writer_revocations, 1U);
}
slot->writer = snapshot.is_writer;
return true;
}
static bool drain_broker_events(ssh_slot_t *slot)
{
for (unsigned int count = 0U; count < 4U; ++count) {
session_broker_event_t event;
esp_err_t error = session_broker_pop_event(slot->broker_client_id, &event);
if (error == ESP_ERR_TIMEOUT) {
return true;
}
if (error != ESP_OK) {
return false;
}
bool writer = event.writer_id == slot->broker_client_id;
if (slot->writer && !writer) {
slot->rx_offset = 0U;
slot->rx_length = 0U;
add_counter(&s_counters.writer_revocations, 1U);
}
slot->writer = writer;
}
return true;
}
static bool service_wolfssh_io(ssh_slot_t *slot)
{
word32 channel_id = 0U;
slot->io_read_budget = SSH_TRANSPORT_WOLFSSH_READ_BUDGET;
int result = wolfSSH_worker(slot->ssh, &channel_id);
(void)channel_id;
if (result == WS_SUCCESS || wolfssh_would_block(slot->ssh, result)) {
return true;
}
return false;
}
static bool flush_client_input(ssh_slot_t *slot)
{
if (slot->rx_offset >= slot->rx_length) {
slot->rx_offset = 0U;
slot->rx_length = 0U;
return true;
}
size_t remaining = slot->rx_length - slot->rx_offset;
if (!slot->writer) {
add_counter(&s_counters.rx_rejected_bytes, remaining);
slot->rx_offset = 0U;
slot->rx_length = 0U;
return true;
}
size_t accepted = 0U;
esp_err_t error = session_broker_write(
slot->broker_client_id, slot->rx_buffer + slot->rx_offset,
remaining, &accepted);
if (error == ESP_ERR_TIMEOUT) {
return true;
}
if (error == ESP_ERR_INVALID_STATE) {
slot->writer = false;
add_counter(&s_counters.rx_rejected_bytes, remaining);
slot->rx_offset = 0U;
slot->rx_length = 0U;
return true;
}
if (error != ESP_OK) {
return false;
}
slot->rx_offset += accepted;
add_counter(&s_counters.rx_accepted_bytes, accepted);
if (slot->rx_offset >= slot->rx_length) {
slot->rx_offset = 0U;
slot->rx_length = 0U;
}
return true;
}
static bool receive_client_input(ssh_slot_t *slot)
{
if (slot->rx_length != 0U) {
return true;
}
slot->io_read_budget = SSH_TRANSPORT_WOLFSSH_READ_BUDGET;
int result = wolfSSH_stream_read(slot->ssh, slot->rx_buffer,
sizeof(slot->rx_buffer));
if (result > 0) {
slot->rx_offset = 0U;
slot->rx_length = (size_t)result;
add_counter(&s_counters.rx_bytes, (uint64_t)result);
return flush_client_input(slot);
}
if (result == 0 || wolfssh_would_block(slot->ssh, result)) {
return true;
}
return false;
}
static bool flush_client_output(ssh_slot_t *slot)
{
if (slot->tx_offset >= slot->tx_length) {
slot->tx_offset = 0U;
slot->tx_length = 0U;
return true;
}
slot->io_read_budget = SSH_TRANSPORT_WOLFSSH_READ_BUDGET;
int result = wolfSSH_stream_send(
slot->ssh, slot->tx_buffer + slot->tx_offset,
(word32)(slot->tx_length - slot->tx_offset));
if (result > 0) {
slot->tx_offset += (size_t)result;
add_counter(&s_counters.tx_bytes, (uint64_t)result);
if (slot->tx_offset >= slot->tx_length) {
slot->tx_offset = 0U;
slot->tx_length = 0U;
}
return true;
}
if (result == 0 || wolfssh_would_block(slot->ssh, result)) {
return true;
}
return false;
}
static bool read_broker_output(ssh_slot_t *slot)
{
if (slot->tx_length != 0U) {
return true;
}
size_t received = 0U;
esp_err_t error = session_broker_read(
slot->broker_client_id, slot->tx_buffer,
sizeof(slot->tx_buffer), &received);
if (error == ESP_ERR_TIMEOUT) {
return true;
}
if (error != ESP_OK) {
return false;
}
slot->tx_offset = 0U;
slot->tx_length = received;
return received == 0U ? true : flush_client_output(slot);
}
static bool reconcile_admin_principal(ssh_slot_t *slot)
{
int64_t now = esp_timer_get_time();
if (now - slot->last_reconcile_us < SSH_TRANSPORT_RECONCILE_INTERVAL_US) {
return true;
}
slot->last_reconcile_us = now;
if (!slot_principal_is_current(slot) || slot->principal.role != USER_ROLE_ADMIN) {
request_slot_close(slot, true);
return false;
}
return true;
}
static bool flush_admin_input(ssh_slot_t *slot, size_t slot_index)
{
if (slot->rx_offset >= slot->rx_length) {
slot->rx_offset = 0U;
slot->rx_length = 0U;
return true;
}
admin_ssh_console_token_t token = admin_console_token(slot, slot_index);
size_t consumed = 0U;
bool accepted = admin_ssh_console_feed_input(
&token, slot->rx_buffer + slot->rx_offset,
slot->rx_length - slot->rx_offset, &consumed);
if (consumed > 0U) {
slot->rx_offset += consumed;
add_counter(&s_counters.rx_accepted_bytes, consumed);
}
if (slot->rx_offset >= slot->rx_length) {
slot->rx_offset = 0U;
slot->rx_length = 0U;
}
if (!accepted && consumed == 0U) {
add_counter(&s_counters.admin_console_input_rejections, 1U);
}
return true;
}
static bool receive_admin_input(ssh_slot_t *slot, size_t slot_index)
{
if (slot->rx_length != 0U) {
return flush_admin_input(slot, slot_index);
}
admin_ssh_console_token_t token = admin_console_token(slot, slot_index);
if (!admin_ssh_console_accepts_input(&token)) {
return true;
}
slot->io_read_budget = SSH_TRANSPORT_WOLFSSH_READ_BUDGET;
int result = wolfSSH_stream_read(slot->ssh, slot->rx_buffer,
sizeof(slot->rx_buffer));
if (result > 0) {
slot->rx_offset = 0U;
slot->rx_length = (size_t)result;
add_counter(&s_counters.rx_bytes, (uint64_t)result);
return flush_admin_input(slot, slot_index);
}
return result == 0 || wolfssh_would_block(slot->ssh, result);
}
static bool read_admin_output(ssh_slot_t *slot, size_t slot_index)
{
if (slot->tx_length != 0U) {
return true;
}
admin_ssh_console_token_t token = admin_console_token(slot, slot_index);
size_t received = 0U;
esp_err_t error = admin_ssh_console_read_output(&token, slot->tx_buffer,
sizeof(slot->tx_buffer), &received);
if (error != ESP_OK && error != ESP_ERR_NOT_FOUND) {
return false;
}
slot->tx_offset = 0U;
slot->tx_length = received;
return error == ESP_OK;
}
static void process_active(ssh_slot_t *slot, size_t slot_index)
{
bool healthy = service_wolfssh_io(slot);
if (healthy && slot->route == SSH_TRANSPORT_ROUTE_BROKER) {
healthy = drain_broker_events(slot) && reconcile_writer(slot) &&
flush_client_output(slot) && read_broker_output(slot) &&
flush_client_input(slot) && receive_client_input(slot);
} else if (healthy && slot->route == SSH_TRANSPORT_ROUTE_ADMIN_CONSOLE) {
healthy = reconcile_admin_principal(slot) && flush_client_output(slot) &&
read_admin_output(slot, slot_index) && flush_client_output(slot) &&
receive_admin_input(slot, slot_index);
} else if (healthy) {
healthy = false;
}
if (!healthy) {
add_counter(&s_counters.io_failures, 1U);
request_slot_close(slot, false);
}
}
static void process_slots(void)
{
for (size_t index = 0U; index < SSH_TRANSPORT_MAX_SESSIONS; ++index) {
ssh_slot_t *slot = &s_slots[index];
if (consume_external_close(slot, index)) {
request_slot_close(slot, true);
}
if (slot->state == SSH_TRANSPORT_SESSION_FREE) {
publish_slot(slot, index);
continue;
}
if (slot->close_requested || slot->state == SSH_TRANSPORT_SESSION_CLOSING) {
(void)cleanup_slot(slot);
publish_slot(slot, index);
continue;
}
if (slot->state == SSH_TRANSPORT_SESSION_HANDSHAKE) {
process_handshake(slot, index);
} else if (slot->state == SSH_TRANSPORT_SESSION_ACTIVE) {
process_active(slot, index);
}
publish_slot(slot, index);
}
bool all_free = true;
for (size_t index = 0U; index < SSH_TRANSPORT_MAX_SESSIONS; ++index) {
if (s_slots[index].state != SSH_TRANSPORT_SESSION_FREE) {
all_free = false;
break;
}
}
if (all_free) {
taskENTER_CRITICAL(&s_lock);
if (!s_running) {
s_cleanup_pending = false;
}
taskEXIT_CRITICAL(&s_lock);
}
}
static void transport_task(void *context)
{
(void)context;
for (;;) {
handle_lifecycle();
bool running;
taskENTER_CRITICAL(&s_lock);
running = s_running;
taskEXIT_CRITICAL(&s_lock);
if (running) {
accept_connections();
}
process_slots();
(void)ulTaskNotifyTake(pdTRUE, pdMS_TO_TICKS(SSH_TRANSPORT_LOOP_DELAY_MS));
}
}
esp_err_t ssh_transport_init(void)
{
taskENTER_CRITICAL(&s_lock);
if (s_initialized) {
taskEXIT_CRITICAL(&s_lock);
return ESP_OK;
}
if (s_initializing) {
taskEXIT_CRITICAL(&s_lock);
return ESP_ERR_INVALID_STATE;
}
s_initializing = true;
taskEXIT_CRITICAL(&s_lock);
esp_err_t error = secure_random_init();
if (error != ESP_OK) {
error = ESP_ERR_INVALID_STATE;
goto fail;
}
if (wolfSSL_SetAllocators(ssh_malloc, ssh_free, ssh_realloc) != 0 ||
wolfSSH_Init() != WS_SUCCESS || wc_SetSeed_Cb(ssh_seed) != 0) {
error = ESP_FAIL;
goto fail;
}
SemaphoreHandle_t command_mutex = xSemaphoreCreateMutex();
if (command_mutex == NULL) {
error = ESP_ERR_NO_MEM;
goto fail;
}
error = admin_ssh_console_init();
if (error != ESP_OK) {
vSemaphoreDelete(command_mutex);
goto fail;
}
for (size_t index = 0U; index < SSH_TRANSPORT_MAX_SESSIONS; ++index) {
s_slots[index].state = SSH_TRANSPORT_SESSION_FREE;
s_slots[index].socket_fd = -1;
s_session_snapshots[index].socket_fd = -1;
s_session_snapshots[index].state = SSH_TRANSPORT_SESSION_FREE;
}
BaseType_t created = xTaskCreatePinnedToCore(
transport_task, "ssh_transport", SSH_TRANSPORT_TASK_STACK_SIZE, NULL,
SSH_TRANSPORT_TASK_PRIORITY, &s_task, SSH_TRANSPORT_TASK_CORE);
if (created != pdPASS) {
s_task = NULL;
vSemaphoreDelete(command_mutex);
error = ESP_ERR_NO_MEM;
goto fail;
}
taskENTER_CRITICAL(&s_lock);
s_command_mutex = command_mutex;
s_initialized = true;
s_initializing = false;
s_last_error = ESP_OK;
taskEXIT_CRITICAL(&s_lock);
return ESP_OK;
fail:
taskENTER_CRITICAL(&s_lock);
s_initializing = false;
taskEXIT_CRITICAL(&s_lock);
return error;
}
static esp_err_t request_running_locked(bool desired)
{
uint32_t sequence;
taskENTER_CRITICAL(&s_lock);
if (!s_initialized) {
taskEXIT_CRITICAL(&s_lock);
return ESP_ERR_INVALID_STATE;
}
if (s_transitioning) {
taskEXIT_CRITICAL(&s_lock);
return ESP_ERR_TIMEOUT;
}
if (desired && s_cleanup_pending) {
taskEXIT_CRITICAL(&s_lock);
return ESP_ERR_INVALID_STATE;
}
if (s_running == desired && !(s_cleanup_pending && !desired)) {
taskEXIT_CRITICAL(&s_lock);
return ESP_OK;
}
s_desired_running = desired;
s_transitioning = true;
s_requested_sequence = next_generation(s_requested_sequence);
sequence = s_requested_sequence;
taskEXIT_CRITICAL(&s_lock);
notify_task();
TickType_t deadline = xTaskGetTickCount() +
pdMS_TO_TICKS(SSH_TRANSPORT_COMMAND_TIMEOUT_MS);
for (;;) {
uint32_t completed;
esp_err_t result;
taskENTER_CRITICAL(&s_lock);
completed = s_completed_sequence;
result = s_command_result;
taskEXIT_CRITICAL(&s_lock);
if (completed == sequence) {
return result;
}
if ((int32_t)(xTaskGetTickCount() - deadline) >= 0) {
return ESP_ERR_TIMEOUT;
}
vTaskDelay(pdMS_TO_TICKS(10U));
}
}
static esp_err_t request_running(bool desired)
{
if (s_command_mutex == NULL) {
return ESP_ERR_INVALID_STATE;
}
xSemaphoreTake(s_command_mutex, portMAX_DELAY);
esp_err_t result = request_running_locked(desired);
xSemaphoreGive(s_command_mutex);
return result;
}
esp_err_t ssh_transport_start(void)
{
return request_running(true);
}
esp_err_t ssh_transport_stop(void)
{
return request_running(false);
}
esp_err_t ssh_transport_replace_host_key(bool reset)
{
if (s_command_mutex == NULL) {
return ESP_ERR_INVALID_STATE;
}
xSemaphoreTake(s_command_mutex, portMAX_DELAY);
bool was_running;
bool cleanup_pending;
taskENTER_CRITICAL(&s_lock);
if (!s_initialized || s_transitioning) {
taskEXIT_CRITICAL(&s_lock);
xSemaphoreGive(s_command_mutex);
return ESP_ERR_INVALID_STATE;
}
was_running = s_running;
cleanup_pending = s_cleanup_pending;
taskEXIT_CRITICAL(&s_lock);
esp_err_t error = ESP_OK;
if (was_running || cleanup_pending) {
error = request_running_locked(false);
}
if (error == ESP_OK) {
error = reset ? ssh_security_reset() : ssh_security_rotate();
}
if (error != ESP_OK) {
if (was_running) {
(void)request_running_locked(true);
}
xSemaphoreGive(s_command_mutex);
return error;
}
if (was_running || reset) {
error = request_running_locked(true);
}
xSemaphoreGive(s_command_mutex);
return error;
}
esp_err_t ssh_transport_get_snapshot(ssh_transport_snapshot_t *snapshot)
{
if (snapshot == NULL) {
return ESP_ERR_INVALID_ARG;
}
taskENTER_CRITICAL(&s_lock);
if (!s_initialized) {
taskEXIT_CRITICAL(&s_lock);
return ESP_ERR_INVALID_STATE;
}
TaskHandle_t task = s_task;
memset(snapshot, 0, sizeof(*snapshot));
snapshot->initialized = s_initialized;
snapshot->running = s_running;
snapshot->transitioning = s_transitioning;
snapshot->port = SSH_TRANSPORT_PORT;
snapshot->last_error = s_last_error;
snapshot->task_core_id = SSH_TRANSPORT_TASK_CORE;
snapshot->task_stack_size = SSH_TRANSPORT_TASK_STACK_SIZE;
snapshot->counters = s_counters;
for (size_t index = 0U; index < SSH_TRANSPORT_MAX_SESSIONS; ++index) {
snapshot->sessions[index] = s_session_snapshots[index];
if (snapshot->sessions[index].active) {
++snapshot->active_sessions;
}
}
taskEXIT_CRITICAL(&s_lock);
if (task != NULL) {
snapshot->task_stack_free_minimum =
(uint32_t)uxTaskGetStackHighWaterMark(task);
}
return ESP_OK;
}
esp_err_t ssh_transport_clear_counters(void)
{
taskENTER_CRITICAL(&s_lock);
if (!s_initialized) {
taskEXIT_CRITICAL(&s_lock);
return ESP_ERR_INVALID_STATE;
}
memset(&s_counters, 0, sizeof(s_counters));
taskEXIT_CRITICAL(&s_lock);
return ESP_OK;
}
esp_err_t ssh_transport_disconnect(uint32_t session_id)
{
if (session_id == 0U) {
return ESP_ERR_INVALID_ARG;
}
bool found = false;
taskENTER_CRITICAL(&s_lock);
if (!s_initialized) {
taskEXIT_CRITICAL(&s_lock);
return ESP_ERR_INVALID_STATE;
}
for (size_t index = 0U; index < SSH_TRANSPORT_MAX_SESSIONS; ++index) {
if (s_session_snapshots[index].active &&
s_session_snapshots[index].session_id == session_id) {
s_external_close_id[index] = session_id;
found = true;
break;
}
}
taskEXIT_CRITICAL(&s_lock);
if (found) {
notify_task();
return ESP_OK;
}
return ESP_ERR_NOT_FOUND;
}
esp_err_t ssh_transport_revoke_user(const uint8_t *username,
size_t username_length)
{
if (!user_database_username_valid(username, username_length)) {
return ESP_ERR_INVALID_ARG;
}
bool found = false;
taskENTER_CRITICAL(&s_lock);
if (!s_initialized) {
taskEXIT_CRITICAL(&s_lock);
return ESP_ERR_INVALID_STATE;
}
for (size_t index = 0U; index < SSH_TRANSPORT_MAX_SESSIONS; ++index) {
const ssh_transport_session_snapshot_t *session =
&s_session_snapshots[index];
if (session->active && session->principal_valid &&
strlen(session->username) == username_length &&
memcmp(session->username, username, username_length) == 0) {
s_external_close_id[index] = session->session_id;
found = true;
}
}
taskEXIT_CRITICAL(&s_lock);
if (found) {
notify_task();
}
return ESP_OK;
}
esp_err_t ssh_transport_revoke_sessions(void)
{
taskENTER_CRITICAL(&s_lock);
if (!s_initialized) {
taskEXIT_CRITICAL(&s_lock);
return ESP_ERR_INVALID_STATE;
}
for (size_t index = 0U; index < SSH_TRANSPORT_MAX_SESSIONS; ++index) {
if (s_session_snapshots[index].active) {
s_external_close_id[index] =
s_session_snapshots[index].session_id;
}
}
taskEXIT_CRITICAL(&s_lock);
notify_task();
return ESP_OK;
}