feat: add bounded admin WebSocket backend (Phase 8D.5)
- Require current admin cookie sessions, Origin checks and single-use tickets - Reuse the shared console with session-aware authorization and slot allocation - Add HTTPD-owned I/O, bounded buffering and revocation cleanup - Prevent LRU eviction of serial clients and stale admin socket closure - Reject unsupported web-shell mutations before side effects - Add host regressions, a smoke client and resource accounting Validated by user sign-off after a 15-minute full-client soak at 230400 baud, with a few broker drops under heavy output. Browser UI remains for Phase 8D.6; numeric memory reserves remain open.
This commit is contained in:
+86
-18
@@ -66,6 +66,7 @@ typedef struct {
|
||||
bool pending_principal_valid;
|
||||
bool authenticated;
|
||||
bool shell_requested;
|
||||
uint8_t console_slot_index;
|
||||
uint8_t authentication_attempts;
|
||||
word32 io_read_budget;
|
||||
bool writer;
|
||||
@@ -87,6 +88,9 @@ 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];
|
||||
/* Published with snapshots; dispatcher never reads owner-task slot storage. */
|
||||
static user_principal_t s_console_principals[SSH_TRANSPORT_MAX_SESSIONS];
|
||||
static uint8_t s_console_slot_indices[SSH_TRANSPORT_MAX_SESSIONS];
|
||||
static ssh_transport_counters_t s_counters;
|
||||
static SemaphoreHandle_t s_command_mutex;
|
||||
static bool s_initializing;
|
||||
@@ -132,8 +136,9 @@ static void notify_task(void)
|
||||
static admin_ssh_console_token_t admin_console_token(const ssh_slot_t *slot,
|
||||
size_t slot_index)
|
||||
{
|
||||
(void)slot_index; /* Physical SSH index is not the shared console index. */
|
||||
return (admin_ssh_console_token_t){
|
||||
.slot_index = (uint8_t)slot_index,
|
||||
.slot_index = slot->console_slot_index,
|
||||
.session_id = slot->session_id,
|
||||
.slot_generation = slot->generation,
|
||||
};
|
||||
@@ -177,19 +182,74 @@ static void publish_slot(const ssh_slot_t *slot, size_t slot_index)
|
||||
|
||||
taskENTER_CRITICAL(&s_lock);
|
||||
s_session_snapshots[slot_index] = snapshot;
|
||||
s_console_slot_indices[slot_index] =
|
||||
snapshot.active && slot->route == SSH_TRANSPORT_ROUTE_ADMIN_CONSOLE
|
||||
? slot->console_slot_index : UINT8_MAX;
|
||||
if (slot->principal_valid) {
|
||||
s_console_principals[slot_index] = slot->principal;
|
||||
} else {
|
||||
secure_wipe(&s_console_principals[slot_index], sizeof(user_principal_t));
|
||||
}
|
||||
taskEXIT_CRITICAL(&s_lock);
|
||||
}
|
||||
|
||||
/* Control-task adapter: copied snapshots only, no runtime wolfSSH calls. */
|
||||
static bool admin_console_drained(const admin_ssh_console_token_t *token)
|
||||
/* Caller holds s_lock. Match session identity first, then the assigned console
|
||||
* binding; callbacks must never index physical SSH storage by console slot.
|
||||
*/
|
||||
static size_t admin_console_snapshot_index_locked(const admin_ssh_console_token_t *token)
|
||||
{
|
||||
if (token->transport != 0U || token->slot_index >= SSH_TRANSPORT_MAX_SESSIONS) {
|
||||
if (token == NULL || token->transport != ADMIN_CONSOLE_TRANSPORT_SSH ||
|
||||
token->session_id == 0U || token->slot_generation == 0U) {
|
||||
return SSH_TRANSPORT_MAX_SESSIONS;
|
||||
}
|
||||
for (size_t i = 0U; i < SSH_TRANSPORT_MAX_SESSIONS; ++i) {
|
||||
const ssh_transport_session_snapshot_t *slot = &s_session_snapshots[i];
|
||||
if (slot->active && slot->route == SSH_TRANSPORT_ROUTE_ADMIN_CONSOLE &&
|
||||
slot->session_id == token->session_id &&
|
||||
slot->generation == token->slot_generation &&
|
||||
s_console_slot_indices[i] != UINT8_MAX &&
|
||||
s_console_slot_indices[i] == token->slot_index) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
return SSH_TRANSPORT_MAX_SESSIONS;
|
||||
}
|
||||
|
||||
/* Dispatcher/control adapters: published state only, no runtime wolfSSH calls. */
|
||||
static bool admin_console_is_current(const admin_ssh_console_token_t *token,
|
||||
const user_principal_t *principal)
|
||||
{
|
||||
if (principal == NULL) {
|
||||
return false;
|
||||
}
|
||||
taskENTER_CRITICAL(&s_lock);
|
||||
const ssh_transport_session_snapshot_t *slot = &s_session_snapshots[token->slot_index];
|
||||
bool drained = slot->active && slot->session_id == token->session_id &&
|
||||
slot->generation == token->slot_generation && !slot->tx_pending;
|
||||
size_t index = admin_console_snapshot_index_locked(token);
|
||||
if (index == SSH_TRANSPORT_MAX_SESSIONS) {
|
||||
taskEXIT_CRITICAL(&s_lock);
|
||||
return false;
|
||||
}
|
||||
const ssh_transport_session_snapshot_t *slot = &s_session_snapshots[index];
|
||||
const user_principal_t *bound = &s_console_principals[index];
|
||||
bool current = slot->active && slot->authenticated && slot->principal_valid &&
|
||||
slot->state == SSH_TRANSPORT_SESSION_ACTIVE &&
|
||||
slot->route == SSH_TRANSPORT_ROUTE_ADMIN_CONSOLE && !slot->close_requested &&
|
||||
s_external_close_id[index] != token->session_id &&
|
||||
slot->session_id == token->session_id && slot->generation == token->slot_generation &&
|
||||
bound->user_id == principal->user_id && bound->auth_generation == principal->auth_generation &&
|
||||
bound->role == USER_ROLE_ADMIN && bound->role == principal->role &&
|
||||
bound->method == principal->method && bound->username_length == principal->username_length &&
|
||||
bound->username_length <= USER_DATABASE_USERNAME_CAPACITY &&
|
||||
memcmp(bound->username, principal->username, bound->username_length) == 0;
|
||||
taskEXIT_CRITICAL(&s_lock);
|
||||
return current;
|
||||
}
|
||||
|
||||
static bool admin_console_drained(const admin_ssh_console_token_t *token)
|
||||
{
|
||||
taskENTER_CRITICAL(&s_lock);
|
||||
size_t index = admin_console_snapshot_index_locked(token);
|
||||
bool drained = index < SSH_TRANSPORT_MAX_SESSIONS &&
|
||||
!s_session_snapshots[index].tx_pending;
|
||||
taskEXIT_CRITICAL(&s_lock);
|
||||
return drained;
|
||||
}
|
||||
@@ -220,21 +280,23 @@ static esp_err_t admin_console_perform(const admin_ssh_console_token_t *token,
|
||||
}
|
||||
}
|
||||
|
||||
static const admin_console_owner_t s_admin_console_owner = {
|
||||
.supported_actions = (1U << ADMIN_SSH_DEFER_REBOOT) |
|
||||
(1U << ADMIN_SSH_DEFER_STOP) | (1U << ADMIN_SSH_DEFER_DISCONNECT) |
|
||||
(1U << ADMIN_SSH_DEFER_HOST_KEY_ROTATE) |
|
||||
(1U << ADMIN_SSH_DEFER_HOST_KEY_RESET) | (1U << ADMIN_CONSOLE_DEFER_SELF_CLOSE),
|
||||
.drained = admin_console_drained,
|
||||
.is_current = admin_console_is_current,
|
||||
.perform = admin_console_perform,
|
||||
};
|
||||
|
||||
esp_err_t admin_ssh_console_open(const admin_ssh_console_token_t *token,
|
||||
const user_principal_t *principal)
|
||||
{
|
||||
static const admin_console_owner_t owner = {
|
||||
.supported_actions = (1U << ADMIN_SSH_DEFER_REBOOT) |
|
||||
(1U << ADMIN_SSH_DEFER_STOP) | (1U << ADMIN_SSH_DEFER_DISCONNECT) |
|
||||
(1U << ADMIN_SSH_DEFER_HOST_KEY_ROTATE) |
|
||||
(1U << ADMIN_SSH_DEFER_HOST_KEY_RESET) | (1U << ADMIN_CONSOLE_DEFER_SELF_CLOSE),
|
||||
.drained = admin_console_drained,
|
||||
.perform = admin_console_perform,
|
||||
};
|
||||
if (token == NULL || token->transport != 0U) {
|
||||
if (token == NULL || token->transport != ADMIN_CONSOLE_TRANSPORT_SSH) {
|
||||
return ESP_ERR_INVALID_ARG;
|
||||
}
|
||||
return admin_ssh_console_open_owned(token, principal, &owner);
|
||||
return admin_ssh_console_open_owned(token, principal, &s_admin_console_owner);
|
||||
}
|
||||
|
||||
static bool consume_external_close(const ssh_slot_t *slot, size_t slot_index)
|
||||
@@ -242,6 +304,10 @@ 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) {
|
||||
/* Keep close intent visible while the owner begins cleanup. */
|
||||
s_session_snapshots[slot_index].close_requested = true;
|
||||
}
|
||||
if (requested || slot->state == SSH_TRANSPORT_SESSION_FREE) {
|
||||
s_external_close_id[slot_index] = 0U;
|
||||
}
|
||||
@@ -982,12 +1048,14 @@ static void process_handshake(ssh_slot_t *slot, size_t slot_index)
|
||||
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);
|
||||
error = admin_ssh_console_open_available(&token, &slot->principal,
|
||||
&s_admin_console_owner);
|
||||
if (error != ESP_OK) {
|
||||
add_counter(&s_counters.admin_console_admission_failures, 1U);
|
||||
request_slot_close(slot, false);
|
||||
return;
|
||||
}
|
||||
slot->console_slot_index = token.slot_index;
|
||||
slot->route = SSH_TRANSPORT_ROUTE_ADMIN_CONSOLE;
|
||||
add_counter(&s_counters.admin_console_admissions, 1U);
|
||||
} else {
|
||||
|
||||
Reference in New Issue
Block a user