Add Serialized SSH Administrative Console

This commit is contained in:
2026-08-30 18:07:02 +02:00
parent 44e3962444
commit 0a1bbd6782
17 changed files with 1115 additions and 79 deletions
+148 -11
View File
@@ -9,6 +9,7 @@
#include <stdio.h>
#include <string.h>
#include "admin_ssh_console.h"
#include "esp_heap_caps.h"
#include "esp_log.h"
#include "esp_timer.h"
@@ -57,6 +58,7 @@ typedef struct {
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;
@@ -125,6 +127,16 @@ static void notify_task(void)
}
}
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 = {
@@ -140,11 +152,21 @@ static void publish_slot(const ssh_slot_t *slot, size_t slot_index)
.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);
@@ -442,6 +464,11 @@ static void close_socket(int *socket_fd)
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);
@@ -886,14 +913,33 @@ static void process_handshake(ssh_slot_t *slot, size_t slot_index)
return;
}
esp_err_t error = connect_broker(slot, slot_index);
if (error != ESP_OK) {
add_counter(&s_counters.broker_failures, 1U);
request_slot_close(slot, false);
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)) {
disconnect_failed_admission(slot);
if (slot->route == SSH_TRANSPORT_ROUTE_BROKER) {
disconnect_failed_admission(slot);
}
request_slot_close(slot, true);
return;
}
@@ -1086,12 +1132,98 @@ static bool read_broker_output(ssh_slot_t *slot)
return received == 0U ? true : flush_client_output(slot);
}
static void process_active(ssh_slot_t *slot)
static bool reconcile_admin_principal(ssh_slot_t *slot)
{
bool healthy = service_wolfssh_io(slot) &&
drain_broker_events(slot) && reconcile_writer(slot) &&
flush_client_output(slot) && read_broker_output(slot) &&
flush_client_input(slot) && receive_client_input(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);
@@ -1117,7 +1249,7 @@ static void process_slots(void)
if (slot->state == SSH_TRANSPORT_SESSION_HANDSHAKE) {
process_handshake(slot, index);
} else if (slot->state == SSH_TRANSPORT_SESSION_ACTIVE) {
process_active(slot);
process_active(slot, index);
}
publish_slot(slot, index);
}
@@ -1186,6 +1318,11 @@ esp_err_t ssh_transport_init(void)
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;