Skip to content

WebSocket Protocol

Stentor uses WebSocket connections for real-time bidirectional communication between system components. Two independent WebSocket hubs handle different concerns:

  • CockpitHub streams events to operator UI clients (beacon updates, task output, console logs)
  • RelayHub manages relay agent connections (commands, heartbeats, C2 events)

Both hubs use JSON message framing over standard WebSocket connections (RFC 6455). This page documents the connection procedures, message formats, event types, and recovery mechanisms for each hub.


CockpitHub (Operator WebSocket)

CockpitHub is the real-time event stream between the Stentor server and operator browser clients. Every operator UI session opens a WebSocket connection to receive beacon updates, task progress, console output, and system events as they happen.

Connection

Endpoint: GET /api/v1/cockpit/ws

CockpitHub supports two authentication methods. Ticket-based auth is the recommended approach -- it avoids exposing long-lived JWTs in WebSocket URLs (which may appear in server logs and proxy caches).

First, obtain a short-lived ticket by calling the REST API with your access token:

# Step 1: Get a WebSocket ticket (valid for 30 seconds, single-use)
TICKET=$(curl -s -X POST https://stentor.app/api/v1/auth/ws-ticket \
  -H "Authorization: Bearer $ACCESS_TOKEN" | jq -r '.ticket')

# Step 2: Connect with the ticket
wscat -c "wss://stentor.app/api/v1/cockpit/ws?ticket=$TICKET"
// JavaScript: Ticket-based WebSocket connection
async function connectCockpit(apiUrl, accessToken) {
  // 1. Fetch a single-use ticket
  const res = await fetch(`${apiUrl}/v1/auth/ws-ticket`, {
    method: 'POST',
    headers: { 'Authorization': `Bearer ${accessToken}` },
  });
  const { ticket } = await res.json();

  // 2. Connect using the ticket (not the JWT)
  const wsUrl = apiUrl.replace(/^http/, 'ws');
  const ws = new WebSocket(`${wsUrl}/v1/cockpit/ws?ticket=${ticket}`);

  ws.onopen = () => console.log('Connected to CockpitHub');
  ws.onmessage = (e) => {
    const event = JSON.parse(e.data);
    console.log(`[seq=${event.seq}] ${event.type}:`, event.payload);
  };

  return ws;
}

Ticket Properties

  • Single-use: Each ticket can only be used once (consumed on validation)
  • 30-second TTL: Tickets expire 30 seconds after generation
  • Opaque: Tickets are 64-character hex strings (32 random bytes)
  • Background cleanup runs every 60 seconds to purge expired tickets

Pass the access token directly as a query parameter:

wscat -c "wss://stentor.app/api/v1/cockpit/ws?token=$ACCESS_TOKEN"

Deprecation Notice

The ?token= parameter is deprecated. JWTs in URLs can leak through server access logs, browser history, and HTTP referer headers. Migrate to ticket-based auth.

Error Responses

If authentication fails, the server responds with a JSON error before the WebSocket upgrade:

Code Error Description
TICKET_EXPIRED ticket expired Ticket TTL exceeded (30s)
TICKET_NOT_FOUND ticket not found Ticket already used or never existed
TICKET_INVALID invalid ticket Ticket validation error
TOKEN_EXPIRED token expired JWT access token expired
TOKEN_INVALID_SIGNATURE invalid token signature JWT signature verification failed
TOKEN_WRONG_TYPE invalid token type Token is not an access token
TOKEN_INVALID invalid token General JWT validation failure
AUTH_MISSING ticket or token required No authentication provided

Message Format

All messages from CockpitHub are SequencedEvents -- each event is wrapped with a monotonically increasing sequence number:

{
  "seq": 42,
  "type": "beacon_update",
  "payload": {
    "beacon_id": "a1b2c3d4-...",
    "hostname": "WORKSTATION-01",
    "username": "CORP\\jsmith",
    "status": "active"
  }
}
Field Type Description
seq uint64 Monotonically increasing sequence number (starts at 1)
type string Event type identifier (see Event Types below)
payload object Type-specific payload (varies per event type)

The seq field enables clients to detect dropped messages. If your client receives seq=40 followed by seq=43, sequences 41 and 42 were dropped (see Drop Recovery).


Event Types

CockpitHub broadcasts the following event types to all connected operator clients.

beacon_update

A beacon's state has changed (new beacon registered, checkin, or status change).

{
  "seq": 1,
  "type": "beacon_update",
  "payload": {
    "beacon_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
    "machine_ip": "10.0.0.20",
    "hostname": "WORKSTATION-01",
    "username": "CORP\\jsmith",
    "pid": 4832,
    "last_seen": "2026-02-19T10:00:00Z",
    "status": "active"
  }
}
Payload Schema
Field Type Description
beacon_id string Beacon UUID
machine_ip string Target machine IP address
hostname string Target hostname
username string User context (DOMAIN\user format)
pid int Process ID of the beacon
last_seen string ISO 8601 timestamp of last check-in
status string "active" or "lost"

console_log

A log entry for the operator console (command input/output, info messages, errors).

{
  "seq": 2,
  "type": "console_log",
  "payload": {
    "tab_id": "beacon-a1b2c3d4",
    "timestamp": "2026-02-19T10:00:05Z",
    "type": "output",
    "text": "Directory listing of C:\\Users\\jsmith\\Desktop..."
  }
}
Payload Schema
Field Type Description
tab_id string Target console tab: "event-log" for global log, "beacon-{id}" for beacon-specific
timestamp string ISO 8601 timestamp
type string Entry type: "input", "output", "info", "success", "error"
text string Log message content

task_started

A task has been dispatched to a beacon for execution.

{
  "seq": 3,
  "type": "task_started",
  "payload": {
    "task_id": "f1e2d3c4-...",
    "beacon_id": "a1b2c3d4-...",
    "technique": "shell",
    "status": "started"
  }
}
Payload Schema
Field Type Description
task_id string Task UUID
beacon_id string Target beacon UUID
technique string Command/technique name (e.g., "shell", "ls", "hashdump")
status string Always "started" for this event

task_completed

A task has finished execution on a beacon.

{
  "seq": 4,
  "type": "task_completed",
  "payload": {
    "task_id": "f1e2d3c4-...",
    "beacon_id": "a1b2c3d4-...",
    "technique": "shell",
    "status": "completed",
    "output": "Volume in drive C has no label..."
  }
}
Payload Schema
Field Type Description
task_id string Task UUID
beacon_id string Target beacon UUID
technique string Command/technique name
status string "completed" or "failed"
output string Command output (empty if no output)

A lateral movement link has been established between two machines (rendered as edges in the pivot graph).

{
  "seq": 5,
  "type": "pivot_link",
  "payload": {
    "from": "machine-001",
    "to": "machine-002",
    "type": "pivot",
    "method": "smb"
  }
}
Payload Schema
Field Type Description
from string Source node ID
to string Target node ID
type string Link type: "initial" (C2 link) or "pivot" (lateral movement)
method string Movement method: "smb", "wmi", "winrm", "dcom" (empty for initial links)

lateral_movement_completed

A lateral movement operation has started, completed, or failed (triggers edge animations in the topology view).

{
  "seq": 6,
  "type": "lateral_movement_completed",
  "payload": {
    "task_id": "f1e2d3c4-...",
    "source_beacon_id": "a1b2c3d4-...",
    "source_machine_id": "machine-001",
    "target_ip": "10.10.10.50",
    "method": "psexec",
    "status": "completed"
  }
}
Payload Schema
Field Type Description
task_id string Task UUID
source_beacon_id string Beacon initiating the movement
source_machine_id string Source machine identifier
target_ip string Target machine IP
method string Lateral movement method (e.g., "psexec", "wmi", "winrm", "dcom")
status string "started", "completed", or "failed"

shell_output

Real-time output from an interactive shell session (routes to the correct xterm.js terminal tab).

{
  "seq": 7,
  "type": "shell_output",
  "payload": {
    "beacon_id": "a1b2c3d4-...",
    "output": "C:\\Users\\jsmith> ",
    "is_error": false,
    "timestamp": "2026-02-19T10:01:00Z"
  }
}
Payload Schema
Field Type Description
beacon_id string Beacon UUID (routes output to correct terminal tab)
output string Shell output text
is_error bool Whether this is stderr output
timestamp string ISO 8601 timestamp

file_transfer_progress

Progress update for an ongoing file upload or download operation.

{
  "seq": 8,
  "type": "file_transfer_progress",
  "payload": {
    "transfer_id": "t1r2a3n4-...",
    "beacon_id": "a1b2c3d4-...",
    "direction": "download",
    "filename": "secrets.docx",
    "progress": 0.65,
    "bytes_done": 6553600,
    "bytes_total": 10085376,
    "status": "in_progress",
    "sha256": ""
  }
}
Payload Schema
Field Type Description
transfer_id string Unique transfer identifier (correlates progress events)
beacon_id string Beacon UUID
direction string "download" (beacon to server) or "upload" (server to beacon)
filename string Original filename
progress float Completion ratio (0.0 to 1.0)
bytes_done int64 Bytes transferred so far
bytes_total int64 Total file size in bytes
status string "in_progress", "completed", or "failed"
sha256 string File hash (populated on completion)

beacon_highlight_update

An operator has changed a beacon's highlight color (visual marker for collaboration).

{
  "seq": 9,
  "type": "beacon_highlight_update",
  "payload": {
    "beacon_id": "a1b2c3d4-...",
    "color": "#ef4444"
  }
}
Payload Schema
Field Type Description
beacon_id string Beacon UUID
color string CSS color value (hex), or empty string when highlight is removed

dialog_request

A CNA script is requesting operator input via a dialog prompt (prompt_text, prompt_confirm, prompt_file, dialog_show).

{
  "seq": 10,
  "type": "dialog_request",
  "payload": {
    "dialog_type": "prompt_text",
    "title": "Enter target username",
    "message": "Provide the username for lateral movement:",
    "default_value": "administrator"
  }
}

A CNA script is requesting navigation to a specific page (openBeaconConsole, openPayloadDialog, etc.).

{
  "seq": 11,
  "type": "navigate",
  "payload": {
    "target": "beacon_console",
    "beacon_id": "a1b2c3d4-..."
  }
}

cna_tab

A CNA script is creating a custom visualization tab (addTab, showVisualization).

{
  "seq": 12,
  "type": "cna_tab",
  "payload": {
    "tab_id": "custom-viz-1",
    "title": "Attack Graph",
    "content_type": "html",
    "content": "<div>...</div>"
  }
}

siem_alert

A SIEM alert forwarded to operator clients for real-time awareness.

{
  "seq": 13,
  "type": "siem_alert",
  "payload": {
    "rule": "Suspicious PowerShell execution",
    "severity": "high",
    "host": "WORKSTATION-01",
    "timestamp": "2026-02-19T10:02:00Z"
  }
}

dropped_messages

Sent when the server detects that messages were dropped for this client (see Drop Recovery).

{
  "seq": 50,
  "type": "dropped_messages",
  "payload": {
    "last_sent_seq": 42,
    "current_seq": 50,
    "dropped_count": 8
  }
}
Payload Schema
Field Type Description
last_sent_seq uint64 Last sequence number successfully sent to this client
current_seq uint64 Current server sequence number
dropped_count uint64 Number of messages that were dropped

Drop Recovery

CockpitHub uses a bounded send buffer (1024 messages per client). When a client's buffer is full (e.g., a slow connection or heavy event volume), the server cannot deliver new events. Instead of silently discarding messages, the server:

  1. Sends a dropped_messages event with the gap range (last_sent_seq through current_seq)
  2. If even the drop notification cannot be delivered, the client is disconnected

When your client receives a dropped_messages event, the recommended recovery strategy is:

ws.onmessage = (e) => {
  const event = JSON.parse(e.data);

  if (event.type === 'dropped_messages') {
    console.warn(
      `Dropped ${event.payload.dropped_count} messages ` +
      `(seq ${event.payload.last_sent_seq + 1} - ${event.payload.current_seq})`
    );

    // Full data refresh -- re-fetch all mutable state from REST API
    refreshBeacons();
    refreshTasks();
    refreshCredentials();
    return;
  }

  // Normal event processing
  handleEvent(event);
};

Sequence Number Tracking

Track the last received seq value. On reconnection, compare it with the server's current sequence (available via the REST API) to determine if a refresh is needed.


Client-to-Server Messages

CockpitHub also accepts messages from connected clients. The ReadPump processes incoming JSON messages and routes them by type.

tunnel_local_response

Tunnel frames sent back from operator browsers for rportfwd_local (reverse port forward through the operator's network).

{
  "type": "tunnel_local_response",
  "beacon_id": "a1b2c3d4-...",
  "bind_port": 8080,
  "frames": [
    {
      "conn_id": 1,
      "type": 2,
      "data": "<base64>"
    }
  ]
}
Field Type Description
type string Always "tunnel_local_response"
beacon_id string Beacon UUID that owns the reverse port forward
bind_port uint16 Local bind port on the operator's machine
frames array Tunnel frames (connection data flowing back to the beacon)

RelayHub (Relay WebSocket)

RelayHub manages persistent WebSocket connections between the Stentor server and relay agents. Relays are Linux processes (typically running on Kali) that host C2 listeners, process beacon check-ins, and execute payload generation.

Connection

Endpoint: GET /ws/relay

Relays authenticate using a shared secret and their registered UUID. Both can be provided via headers (preferred) or query parameters.

wscat -c "wss://stentor.app/ws/relay" \
  -H "X-Relay-ID: aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee" \
  -H "X-Relay-Secret: your-shared-secret"
wscat -c "wss://stentor.app/ws/relay?relay_id=aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee&secret=your-shared-secret"

Prerequisites

  1. Relay must exist in the database -- The server validates the relay UUID against the relays table. If the relay is not registered, the connection returns HTTP 404.
  2. Shared secret must match -- The X-Relay-Secret header (or secret query param) must match the server's configured RELAY_SECRET environment variable.

Error Responses

Status Error Description
400 relay_id required No relay ID provided
400 invalid relay_id format Relay ID is not a valid UUID
401 invalid relay secret Secret does not match server configuration
404 relay not found Relay UUID not registered in database
500 internal error Database query failure

Database Registration Required

After a database rebuild, relay records are lost. You must re-insert the relay into the relays table before the relay can reconnect:

INSERT INTO relays (id, name, description, ip_address, status)
VALUES ('aaaaaaaa-...', 'Kali Relay', 'Relay agent', '10.0.0.50', 'online')
ON CONFLICT (id) DO NOTHING;

Message Format

All RelayHub messages use the RelayMessage envelope:

{
  "type": "command",
  "id": "550e8400-e29b-41d4-a716-446655440000",
  "correlation_id": null,
  "timestamp": "2026-02-19T10:00:00Z",
  "payload": { ... }
}
Field Type Description
type string Message type: "command", "response", "heartbeat", "event", "ack"
id uuid Unique message identifier
correlation_id uuid? For responses: the id of the original command (enables request/response matching)
timestamp string ISO 8601 creation timestamp
payload object Type-specific payload (see sections below)

Message Types

command (Backend to Relay)

Commands are sent from the server to a relay to initiate actions (start listeners, queue tasks, generate payloads).

{
  "type": "command",
  "id": "cmd-uuid",
  "timestamp": "2026-02-19T10:00:00Z",
  "payload": {
    "command": "start_listener",
    "args": {
      "listener_id": "6ea88162-...",
      "name": "HTTPS Relay",
      "type": "https",
      "bind_address": "0.0.0.0",
      "port": 8443
    }
  }
}

Payload Schema (RelayCommandPayload):

Field Type Description
command string Command name (see Command Reference)
args object Command-specific arguments (JSON object)

response (Relay to Backend)

Responses carry the result of a previously received command. The correlation_id links the response to the original command's id.

{
  "type": "response",
  "id": "resp-uuid",
  "correlation_id": "cmd-uuid",
  "timestamp": "2026-02-19T10:00:01Z",
  "payload": {
    "success": true,
    "error": "",
    "data": { "listener_id": "6ea88162-...", "status": "running" }
  }
}

Payload Schema (RelayResponsePayload):

Field Type Description
success bool Whether the command succeeded
error string Error message (empty on success)
data object? Response data (command-specific, present on success)

heartbeat (Bidirectional)

Health monitoring messages sent in both directions to maintain the connection and report relay status.

{
  "type": "heartbeat",
  "id": "hb-uuid",
  "timestamp": "2026-02-19T10:00:30Z",
  "payload": {
    "relay_id": "aaaaaaaa-...",
    "status": "idle"
  }
}

Payload Schema (RelayHeartbeatPayload):

Field Type Description
relay_id string Relay instance UUID
status string Current relay state: "idle", "busy", or "error"

event (Relay to Backend)

Asynchronous events from the relay to the server (beacon registrations, task completions, phishing activity).

{
  "type": "event",
  "id": "evt-uuid",
  "timestamp": "2026-02-19T10:01:00Z",
  "payload": {
    "event": "beacon_new",
    "data": {
      "beacon_id": "a1b2c3d4-...",
      "hostname": "WORKSTATION-01",
      "username": "jsmith",
      "ip": "10.0.0.20",
      "os": "Windows 10",
      "arch": "x64",
      "pid": 4832
    }
  }
}

Payload Schema (RelayEventPayload):

Field Type Description
event string Event name (see Event Reference)
data object Event-specific data

ack (Relay to Backend)

Acknowledgment sent by the relay immediately upon receiving a command, before execution begins. This confirms receipt while the actual result arrives later as a response.

{
  "type": "ack",
  "id": "ack-uuid",
  "timestamp": "2026-02-19T10:00:00Z",
  "payload": {
    "command_id": "cmd-uuid",
    "received": "2026-02-19T10:00:00Z"
  }
}

Payload Schema (RelayAckPayload):

Field Type Description
command_id uuid ID of the command being acknowledged
received string ISO 8601 timestamp when command was received

Command Reference

Commands sent from the Stentor server to relay agents. Each command is wrapped in a RelayCommandPayload with the command name and arguments.

Listener Management

Command Description Key Arguments
start_listener Start a C2 listener on the relay listener_id, name, type, bind_address, port, profile_name, tls_cert, tls_key, dns_domain, smb_pipe_name
stop_listener Stop a running listener listener_id
host_file Host a file at a custom URI on a listener listener_id, uri, data, content_type, filename
unhost_file Remove a hosted file from a listener listener_id, uri
start_listener Arguments (StartListenerArgs)
Field Type Description
listener_id string Listener UUID
name string Display name
type string Protocol: "http", "https", "dns", "smb_pipe"
bind_address string Listen address (e.g., "0.0.0.0")
port int Listen port
profile_name string? Malleable C2 profile name
profile_config object? Profile configuration JSON
tls_cert string? TLS certificate PEM (for HTTPS)
tls_key string? TLS private key PEM (for HTTPS)
dns_domain string? DNS domain (for DNS listeners)
dns_ttl int? DNS record TTL
dns_resolver string? Custom DNS resolver
smb_pipe_name string? Named pipe name (for SMB listeners)
beacon_port int? Port override for beacon callback
guardrails object? Execution guardrail rules
wg_private_key string? WireGuard private key (for VPN)
wg_peer_public_key string? WireGuard peer public key
wg_tunnel_ip string? WireGuard tunnel IP
wg_peer_tunnel_ip string? WireGuard peer tunnel IP
wg_c2_port int? WireGuard C2 port

Task Operations

Command Description Key Arguments
queue_task Queue a task for beacon delivery on next check-in beacon_id, task_type, task_data
tunnel Send SOCKS tunnel frames to a beacon beacon_id, frames[]
covertvpn Manage covert VPN TAP interface (VPN-specific args)
queue_task Arguments (QueueTaskPayload)
Field Type Description
task_id uuid? Optional: Backend-provided task ID for correlation
beacon_id uuid Target beacon UUID
task_type string Task type (e.g., "shell", "upload", "inject")
task_data object Task-specific arguments

Payload Generation

Command Description Key Arguments
generate_payload Generate a payload binary (EXE, DLL, shellcode, etc.) type, c2_url, obfuscate, profile, evasion_options, architecture
generate_clr_shellcode Generate CLR loader shellcode for PowerShell assembly, script, arch
generate_assembly_shellcode Generate shellcode for .NET assembly execution assembly, args, arch
generate_browser_pivot_shellcode Generate WinINet proxy shellcode pipe_name, arch
sign_payload Authenticode-sign a payload binary payload, cert_pfx, password
generate_payload Arguments (GeneratePayloadArgs)
Field Type Description
type string Payload type: "exe", "dll", "shellcode", "lnk", "docm", "xlsm", "iso", "hta", "html_smuggling", "stager", "service", "msi"
c2_url string C2 callback URL
obfuscate bool Use garble for unique binary hash per build
profile string Malleable C2 profile name
architecture string? Target arch: "x64", "x86"
sleep int? Beacon sleep interval (seconds)
jitter int? Jitter percentage (0-100)
evasion_options object? AMSI bypass, ETW patch, sleep obfuscation toggles
guardrail_config object? Execution restrictions (IP, hostname, domain, username patterns)
kill_date string? RFC 3339 payload expiration date
exit_func string? Exit function: "process" or "thread"

Additional type-specific configs: iso_config, hta_config, html_smuggling_config, stager_config, dll_config, service_config, shellcode_config, msi_config, udrl_config, drip_config, proxy_config, host_rotation_config, sleep_mask_config, udc2_config.

Phishing Operations

Command Description Key Arguments
send_email Send a phishing email via the relay template_id, from_email, to_email, tracking_id, tracking_base_url
preview_email Preview a rendered email template template_id, target_email
host_payload Host a payload on the tracking server payload_id, data, content_type

Event Reference

Events sent from relay agents to the Stentor server. Each event is wrapped in a RelayEventPayload.

Beacon Lifecycle Events

Event Description Key Data Fields
beacon_new New beacon registered from a target beacon_id, hostname, username, ip, os, arch, pid, listener_id
beacon_checkin Existing beacon checked in beacon_id, hostname, username, ip, os, arch, pid
beacon_dead Beacon missed check-ins and is marked dead beacon_id
beacon_new / beacon_checkin Payload (BeaconEventPayload)
Field Type Description
beacon_id uuid Beacon UUID
hostname string Target hostname
username string User context
ip string Target IP address
os string Operating system (e.g., "Windows 10")
arch string Architecture ("x64", "x86")
pid int Process ID
listener_id string? Originating listener UUID

Task Events

Event Description Key Data Fields
task_complete Task execution finished on a beacon task_id, beacon_id, success, output, error, artifacts[]
task_complete Payload (TaskCompletePayload)
Field Type Description
task_id uuid Task UUID
beacon_id uuid Beacon that executed the task
task_type string? Task type (optional for backward compat)
success bool Whether execution succeeded
output string Command output
error string? Error message (on failure)
artifacts array? Forensic artifacts produced (files, registry keys)

Listener Lifecycle Events

Event Description Key Data Fields
listener_started Listener successfully started listener_id, status, port
listener_stopped Listener stopped listener_id, status
listener_error Listener encountered an error listener_id, status, error
Listener Status Payload (ListenerStatusPayload)
Field Type Description
listener_id string Listener UUID
status string "running", "stopped", or "error"
error string? Error description (for listener_error)
port int? Actual bound port (for listener_started)

Tunnel Events

Event Description Key Data Fields
tunnel_response SOCKS tunnel frames from a beacon beacon_id, frames[]
Tunnel Event Payload (TunnelEventPayload)
Field Type Description
beacon_id uuid Beacon UUID
frames array Array of TunnelFrame objects

TunnelFrame:

Field Type Description
conn_id uint32 Connection multiplexer ID
type uint8 Frame type: 1=connect, 2=data, 3=close, 4=error
data bytes Payload (address for connect, raw bytes for data)

Guardrail Events

Event Description Key Data Fields
guardrail_violation A guardrail rule was triggered on the relay Rule-specific violation details

Phishing Events

Event Description Key Data Fields
email_sent Phishing email sent successfully tracking_id, to_email, timestamp, success
email_opened Tracking pixel loaded by target tracking_id, timestamp, user_agent, ip
link_clicked Phishing link clicked by target tracking_id, timestamp, user_agent, ip
creds_captured Credentials captured from landing page tracking_id, username, password, timestamp
payload_downloaded Payload file downloaded by target payload_id, timestamp, user_agent, ip
keystroke_captured Keystrokes captured from cloned site tracking_id, keystrokes, timestamp

CNA Events

Event Description Key Data Fields
profiler_hit System Profiler visit collected external_ip, internal_ip, user_agent, platform, applications, token
web_hit HTTP request served by web server method, uri, remote_addr, user_agent, response_code, response_size, handler

Synchronous Commands

Some operations require waiting for a relay's response before proceeding (e.g., payload generation must return the binary data). The SendCommandSync mechanism implements synchronous request/response over the asynchronous WebSocket channel.

sequenceDiagram
    participant Server as Stentor Server
    participant Relay as Relay Agent

    Server->>Server: Create command with unique ID
    Server->>Server: Register pending request (ID -> response channel)
    Server->>Relay: Send command message
    Relay->>Server: Send ack (command_id)
    Note over Relay: Execute command...
    Relay->>Server: Send response (correlation_id = command ID)
    Server->>Server: Match correlation_id to pending request
    Server->>Server: Deliver response via channel
    Note over Server: Returns data to caller

Timeout handling: If the relay does not respond within the specified timeout (typically 30-60 seconds for payload generation), the request returns an ErrRelayTimeout error. The pending request is cleaned up regardless of outcome.

Correlation matching: The relay includes the original command's id as the correlation_id in its response. The server's handleMessage function checks incoming responses against the pendingRequests map -- if a match is found, the response is delivered to the waiting goroutine and the pending entry is removed.


Connection Lifecycle

CockpitHub Lifecycle

sequenceDiagram
    participant Client as Operator Browser
    participant API as REST API
    participant Hub as CockpitHub

    Client->>API: POST /v1/auth/login
    API-->>Client: access_token

    Client->>API: POST /v1/auth/ws-ticket
    Note over Client: Authorization: Bearer <token>
    API-->>Client: { ticket: "a3f2..." }

    Client->>Hub: GET /v1/cockpit/ws?ticket=a3f2...
    Hub->>Hub: Validate ticket (single-use, 30s TTL)
    Hub-->>Client: 101 Switching Protocols

    Hub->>Hub: Register client, fire event_join CNA event

    loop Real-time Events
        Hub->>Client: SequencedEvent { seq, type, payload }
    end

    Hub->>Client: Ping (every 54s)
    Client->>Hub: Pong (must respond within 60s)

    alt Client Send Buffer Full
        Hub->>Client: dropped_messages event
        Note over Client: Re-fetch all data from REST API
    end

    alt Disconnection
        Hub->>Hub: Unregister client, fire event_quit CNA event
        Hub->>Hub: Clean up operator tunnel forwards
    end

Reconnection strategy: When the WebSocket connection drops, clients should:

  1. Wait briefly (exponential backoff with jitter: initial 1s, max 30s)
  2. Request a new ticket via POST /v1/auth/ws-ticket
  3. Reconnect with the new ticket
  4. Compare the last received seq with the server's current sequence
  5. If there is a gap, perform a full data refresh

RelayHub Lifecycle

sequenceDiagram
    participant DB as PostgreSQL
    participant Relay as Relay Agent
    participant Hub as RelayHub

    Note over DB: Relay must be registered in relays table

    Relay->>Hub: GET /ws/relay
    Note over Relay: X-Relay-ID + X-Relay-Secret headers
    Hub->>Hub: Validate secret
    Hub->>DB: GetByID(relay_uuid)
    DB-->>Hub: Relay record (or 404)
    Hub-->>Relay: 101 Switching Protocols

    Hub->>DB: UpdateStatus(relay_id, "connected")
    Hub->>Hub: Register client (replaces existing if same ID)

    loop Heartbeat Loop
        Relay->>Hub: heartbeat { relay_id, status: "idle" }
    end

    loop Commands & Events
        Hub->>Relay: command { command: "start_listener", args: {...} }
        Relay->>Hub: ack { command_id: "..." }
        Relay->>Hub: response { correlation_id: "...", success: true }
        Relay->>Hub: event { event: "beacon_new", data: {...} }
    end

    alt Disconnection
        Hub->>Hub: Unregister client
        Hub->>DB: UpdateStatus(relay_id, "disconnected")
    end

Relay reconnection: If the relay process restarts or the WebSocket connection drops:

  1. The relay attempts to reconnect with the same UUID and secret
  2. If a client with the same ID is already registered, the old connection is replaced
  3. The relay status is updated to "connected" in the database
  4. Active listeners must be re-started after reconnection (state is not preserved)