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:
2026-09-06 14:41:41 +02:00
parent e5dce12ed4
commit aeb2043396
37 changed files with 3651 additions and 91 deletions
+23 -3
View File
@@ -17,6 +17,9 @@ extern "C" {
/* Fits the longest supported ECDSA P-256 OpenSSH key import command. */
#define ADMIN_SSH_CONSOLE_COMMAND_LINE_CAPACITY 256U
#define ADMIN_CONSOLE_TRANSPORT_SSH 0U
#define ADMIN_CONSOLE_TRANSPORT_WEB 1U
typedef struct {
uint8_t slot_index;
uint32_t session_id;
@@ -36,12 +39,15 @@ typedef enum {
/* Small owner boundary; module/API names are retained for existing SSH callers.
* Exactly two shared console slots, not two per transport. slot_index addresses
* this pool; owners coordinate admission and must not reuse an identity while
* this pool; open_available atomically selects a free slot. Owners must not reuse an identity while
* old work can exist. transport is a firmware-assigned namespace (0 = SSH).
* An occupied or still-executing slot cannot be replaced by open_owned().
*
* The immutable adapter lives for firmware lifetime. Callbacks run on the
* control task OUTSIDE console locks, never on the dispatcher or socket owner.
* control task OUTSIDE console locks for drained/perform. Required is_current
* runs on the dispatcher outside console locks; it must be bounded and validate
* full transport identity, originating-session liveness and principal binding,
* without calling socket libraries or handlers. Core separately checks accounts.
* drained must be nonblocking, validate the full identity and include pending
* owner output. perform must revalidate identity and marshal lifecycle work to
* its owner, never call socket libraries here. Neither callback may call console
@@ -53,7 +59,10 @@ typedef enum {
* parallel. Shared completion scratch is nonblocking/serialized by the core.
* The owner alone consumes output, maintains authentication/session liveness,
* and calls close on disconnect/revocation. Core copies/rechecks principals at
* admission and dispatch, but does not implement transport-specific expiry.
* admission and dispatch. Dispatch and prompts also check owner currentness;
* blocked prompts recheck every 250ms (plus check/scheduling latency). This does
* not cancel or roll back arbitrary executing handlers. Admission remains the
* owner's responsibility; is_current need not accept unpublished admission.
* Close wakes prompts; executing state is retained until the handler returns.
* Output remains bounded (5s write backpressure); deferred work waits at most
* 10s for application drain plus 200ms, NOT peer-delivery confirmation.
@@ -61,11 +70,22 @@ typedef enum {
*/
typedef struct {
uint32_t supported_actions;
bool (*is_current)(const admin_ssh_console_token_t *token,
const user_principal_t *principal);
bool (*drained)(const admin_ssh_console_token_t *token);
esp_err_t (*perform)(const admin_ssh_console_token_t *token,
admin_ssh_deferred_action_type_t action, uint32_t argument);
} admin_console_owner_t;
/* Selects any inactive, nonexecuting slot from the shared two-slot pool.
* Input slot_index is ignored; only slot_index changes, and only on success.
* Caller supplies transport/session_id/slot_generation and must retain the
* returned token. Full pool returns ESP_ERR_INVALID_STATE, like open_owned.
*/
esp_err_t admin_ssh_console_open_available(admin_ssh_console_token_t *token,
const user_principal_t *principal,
const admin_console_owner_t *owner);
esp_err_t admin_ssh_console_open_owned(const admin_ssh_console_token_t *token,
const user_principal_t *principal,
const admin_console_owner_t *owner);