Skip to content

Relay Management

Relays are the bridge between the Stentor server and target environments. They run on attack infrastructure (typically Kali Linux) and host C2 listeners, generate payloads, process implant check-ins, and route traffic via WebSocket back to the server. This guide covers building, deploying, registering, monitoring, and troubleshooting relay agents.


Architecture

The relay sits between the Stentor backend and the target network. It maintains a persistent WebSocket connection to the server for command dispatch, and runs local C2 listeners that implants connect to.

graph LR
    subgraph Backend
        A[Stentor Server<br/>:8082]
    end

    subgraph Relay["Relay (Kali)"]
        B[WebSocket Client]
        C[C2 Listener<br/>HTTP/HTTPS]
        D[DNS C2 Server]
        E[SMB Pipe Server]
        F[Payload Generator]
    end

    subgraph Target["Target Network"]
        G[Implant<br/>Windows]
    end

    A <-->|WebSocket| B
    G -->|HTTPS| C
    G -->|DNS queries| D
    G -->|SMB named pipe| E
    C -->|Events| B
    B -->|Commands| C

Traffic flow:

  1. The operator issues a command via the Stentor UI or API.
  2. The backend sends a WebSocket command to the relay (e.g., queue_task, start_listener, generate_payload).
  3. The relay executes the command locally -- starts a listener, queues a task for a beacon, or generates a payload binary.
  4. When an implant checks in to a relay-hosted listener, the relay sends a WebSocket event back to the backend (e.g., beacon_new, beacon_checkin, task_complete).
  5. The backend updates its database and pushes real-time updates to the operator UI via the CockpitHub WebSocket.

Building the Relay

Build the relay binary from the project root. The relay is a single static Go binary with no external dependencies.

# From the Stentor project root
GOOS=linux GOARCH=amd64 CGO_ENABLED=0 go build \
  -ldflags="-s -w -X main.Version=1.1.0" \
  -o binaries/stentor-relay \
  ./relay/cmd/relay/

Build flags explained:

Flag Purpose
GOOS=linux Cross-compile for Linux (relay runs on Kali)
GOARCH=amd64 Target 64-bit x86 architecture
CGO_ENABLED=0 Static binary with no C library dependencies
-s -w Strip debug info and DWARF symbols (smaller binary)
-X main.Version=1.1.0 Embed version string for identification

Version identification

The relay logs its version on startup. Use meaningful version strings to track which build is deployed: Stentor Kali Relay Agent starting... Version: 1.1.0


Deployment

Transfer to Relay Host

The relay binary is typically deployed via SCP through a jump host (Proxmox) to the Kali relay VM:

# Step 1: Dev machine -> Proxmox host
scp -o StrictHostKeyChecking=no binaries/stentor-relay \
  root@<proxmox-host>:/tmp/stentor-relay

# Step 2: Proxmox host -> Kali relay
ssh root@<proxmox-host> 'scp -o StrictHostKeyChecking=no \
  /tmp/stentor-relay root@<kali-ip>:/opt/stentor-relay/stentor-relay'

Lab deployment example

# Using the default lab infrastructure
scp binaries/stentor-relay root@<proxmox-ip>:/tmp/stentor-relay
ssh root@<proxmox-ip> 'sshpass -p "<relay-password>" scp -o StrictHostKeyChecking=no \
  /tmp/stentor-relay [email protected]:/opt/stentor-relay/stentor-relay'

Configuration

The relay is configured entirely via environment variables, typically stored in /opt/stentor-relay/.env. The relay reads these on startup through the systemd EnvironmentFile directive.

Required Variables

Variable Description Example
BACKEND_URL Server WebSocket endpoint wss://stentor.app/ws/relay
RELAY_ID Relay UUID (must match database record) aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee
RELAY_SECRET Shared secret for WebSocket authentication <random-string>

C2 Listener Variables

Variable Default Description
C2_PORT 443 Port for the default HTTPS C2 listener
C2_CERT_PATH -- TLS certificate for HTTPS listeners
C2_KEY_PATH -- TLS private key for HTTPS listeners
C2_STAGING_AUTH_TOKEN -- Bearer token for staging endpoint (empty = unauthenticated)

Optional Service Variables

Variable Default Description
LOG_LEVEL info Logging verbosity: debug, info, warn, error
RECONNECT_INTERVAL_SECONDS 5 Delay between WebSocket reconnection attempts
HEARTBEAT_INTERVAL_SECONDS 30 Heartbeat frequency to the backend
SMTP_PORT 25 Embedded SMTP server port (0 = disabled)
SMTP_EXTERNAL_HOST -- External SMTP relay for forwarding
SMTP_EXTERNAL_PORT 587 External SMTP relay port
TRACKING_PORT 80 HTTP tracking server port (0 = disabled)
DNS_PORT 0 DNS C2 server port (0 = disabled)
DNS_DOMAIN -- Domain suffix for DNS C2 queries (required if DNS_PORT > 0)
DNS_TTL 60 TTL for DNS C2 responses in seconds
PROFILER_PORT 0 System Profiler HTTP server port (0 = disabled)
HEALTH_PORT 9090 HTTP health endpoint port (0 = disabled)
STATE_DIR /var/lib/stentor-relay Listener state persistence directory

DNS C2 requirement

If DNS_PORT is set to a non-zero value, DNS_DOMAIN must also be configured. The relay will fail to start if DNS port is enabled without a domain.

Example .env file:

# Required
BACKEND_URL=wss://stentor.app/ws/relay
RELAY_ID=aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee
RELAY_SECRET=your-shared-secret-here

# C2 listener
C2_PORT=8443
C2_CERT_PATH=/opt/stentor-relay/certs/cert.pem
C2_KEY_PATH=/opt/stentor-relay/certs/key.pem

# Optional services
LOG_LEVEL=info
SMTP_PORT=0
TRACKING_PORT=0
DNS_PORT=0
PROFILER_PORT=0

Systemd Service

Create a systemd service for automatic startup and restart on failure.

Service file (/etc/systemd/system/stentor-relay.service):

[Unit]
Description=Stentor Relay Agent
After=network.target

[Service]
Type=simple
User=root
WorkingDirectory=/opt/stentor-relay
ExecStart=/opt/stentor-relay/stentor-relay
EnvironmentFile=/opt/stentor-relay/.env
Restart=always
RestartSec=5

[Install]
WantedBy=multi-user.target

Service management commands:

# Install and enable the service
sudo systemctl daemon-reload
sudo systemctl enable stentor-relay

# Start the relay
sudo systemctl start stentor-relay

# Check status
sudo systemctl status stentor-relay

# View logs (real-time)
journalctl -u stentor-relay -f

# View recent logs
journalctl -u stentor-relay --since "1 hour ago"

# Restart after configuration change
sudo systemctl restart stentor-relay

Startup verification

On successful startup, the relay logs its configuration and connection status:

Stentor Kali Relay Agent starting...
  Version: 1.1.0
  Relay ID: aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee
  Backend URL: wss://stentor.app/ws/relay
  C2 Port: 8443
C2 server listening on :8443
Relay Agent ready, awaiting commands from Backend


Registration

Before a relay can connect via WebSocket, it must exist in the Stentor database. The WebSocket handler at /ws/relay calls relayRepo.GetByID() and returns a 404 Not Found if the relay UUID is not in the relays table.

Critical: Register before connecting

The relay must be registered in the database before starting the relay service. If the relay is not found in the database, the WebSocket handshake will fail with a 404 error and the relay will repeatedly fail to connect.

Via API

Create a relay record through the REST API. The response includes the generated id -- use this as the RELAY_ID in the relay's .env file.

curl -s -X POST https://stentor.app/api/v1/relays \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Kali Relay",
    "description": "Primary attack relay",
    "ip_address": "10.0.0.50"
  }' | jq
{
  "id": "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee",
  "name": "Kali Relay",
  "description": "Primary attack relay",
  "ip_address": "10.0.0.50",
  "status": "offline",
  "created_at": "2025-01-15T12:00:00Z"
}

Via Direct SQL

Useful after a database rebuild (e.g., --rebuild-db deployment) when you need to re-register a relay with its existing UUID:

INSERT INTO relays (id, name, description, ip_address, status)
VALUES (
  'aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee',
  'Kali Relay',
  'Primary attack relay',
  '10.0.0.50',
  'online'
)
ON CONFLICT (id) DO NOTHING;

Execute via Docker:

ssh root@<proxmox> 'ssh [email protected] \
  "docker exec -i stentor-db psql -U stentor -d stentor_db"' <<< \
  "INSERT INTO relays (id, name, description, ip_address, status) \
   VALUES ('aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee', 'Kali Relay', \
   'Primary attack relay', '10.0.0.50', 'online') \
   ON CONFLICT (id) DO NOTHING;"

After registration, restart the relay service for it to establish the WebSocket connection:

sudo systemctl restart stentor-relay

WebSocket Protocol

The relay communicates with the backend over a persistent WebSocket connection at /ws/relay. Authentication uses custom headers.

Authentication

The relay authenticates using two headers on the WebSocket upgrade request:

Header Description
X-Relay-ID Relay UUID matching a database record
X-Relay-Secret Shared secret matching the server's RELAY_SECRET

Both headers can alternatively be passed as query parameters (relay_id and secret).

mTLS alternative

Relays can also authenticate using TLS client certificates (mTLS) instead of shared secret headers. See mTLS Authentication for setup instructions.

Message Types

All messages follow a JSON envelope format with type, id, correlation_id, timestamp, and payload fields.

Type Direction Description
command Server -> Relay Initiates actions (start listener, queue task, generate payload)
response Relay -> Server Returns command results with correlation ID
event Relay -> Server Async notifications (beacon new, task complete, listener status)
heartbeat Bidirectional Connection health monitoring
ack Relay -> Server Acknowledges command receipt before execution completes

Key Commands (Server -> Relay)

Command Description
start_listener Start a C2 listener on the relay
stop_listener Stop a running listener
queue_task Queue a task for delivery to a beacon
generate_payload Build an implant binary (EXE, DLL, shellcode, etc.)
send_email Send a phishing email via the relay's SMTP server
tunnel Forward SOCKS tunnel frames to a beacon
host_file Host a file at a custom URI on a listener
unhost_file Remove a hosted file from a listener

Key Events (Relay -> Server)

Event Description
beacon_new New implant registered
beacon_checkin Existing beacon checked in
beacon_dead Beacon missed check-ins and marked dead
task_complete Task execution completed with results
listener_started Listener successfully started
listener_stopped Listener shut down
listener_error Listener encountered an error

Full protocol reference

For complete message format documentation, field definitions, and sequence diagrams, see WebSocket Protocol.


Health Monitoring

Relay Status via API

Check the status of all registered relays:

curl -s https://stentor.app/api/v1/relays \
  -H "Authorization: Bearer $TOKEN" | jq '.[] | {id, name, status}'

The status field reflects the WebSocket connection state:

Status Meaning
connected Relay has an active WebSocket connection
disconnected Relay was previously connected but the connection dropped
offline Relay has never connected (freshly registered)

/healthz Endpoint

Each relay exposes an HTTP health endpoint on a dedicated port (default: 9090, configurable via HEALTH_PORT). This endpoint runs on a separate port from C2 listeners to avoid conflicts and is accessible without authentication.

curl http://<relay-ip>:9090/healthz | jq

Response fields:

{
  "status": "healthy",
  "uptime": 86400,
  "active_listeners": 2,
  "connected_beacons": 5,
  "last_checkin": "2025-06-15T14:30:00Z",
  "version": "1.1.0",
  "timestamp": "2025-06-15T14:31:00Z"
}
Field Type Description
status string Always "healthy" when the endpoint responds
uptime int64 Seconds since relay process started
active_listeners int Count of currently running C2 listeners
connected_beacons int Count of beacons registered with this relay
last_checkin string RFC 3339 timestamp of the most recent beacon check-in (omitted if no beacons have checked in)
version string Relay binary version string
timestamp string Current UTC time in RFC 3339 format

Disabling the health endpoint

Set HEALTH_PORT=0 in the relay's .env file to disable the /healthz endpoint entirely. This may be useful on relays where network exposure must be minimized.

Server-Side Health Polling

The Stentor server automatically polls the /healthz endpoint on all connected relays to keep health data up to date.

Polling behavior:

  • The server polls every 30 seconds for all relays with status connected and a non-empty IP address.
  • Uses a standard HTTP GET request (not the WebSocket channel) for simplicity and isolation.
  • Each poll request has a 5-second timeout to avoid blocking on unresponsive relays.
  • Health data is persisted to the database using nullable fields -- nil values indicate the relay has not yet been polled.

No configuration required

Server-side health polling starts automatically when the backend launches. There are no server-side environment variables to configure -- the server always polls on port 9090.

Health Status in UI

The relay selector in the Stentor UI displays a health indicator dot next to each relay name:

Indicator Condition Meaning
🟢 Green Health data received within 60 seconds Relay is healthy and responsive
🟡 Yellow Health data received within 5 minutes Relay may be experiencing issues
⚪ Gray Health data older than 5 minutes or not yet received Relay is stale or has never reported health

The UI also shows the beacon count and listener count alongside the relay name when health data is available.

WebSocket Heartbeat

The relay sends periodic heartbeat messages to the backend at the configured interval (default: every 30 seconds). The heartbeat payload includes the relay ID and current status (idle, busy, or error).

If the backend does not receive a heartbeat within the expected interval, it marks the relay as potentially disconnected. The RelayHub automatically updates the relay status in the database when connections are established or lost.

Heartbeat vs. health polling

The WebSocket heartbeat monitors the connection between relay and server (is the WebSocket alive?). The /healthz poll monitors the relay's operational state (how many listeners, beacons, uptime). Both are independent mechanisms.

Log Monitoring

Monitor relay health in real-time via systemd journal:

# Real-time log stream
journalctl -u stentor-relay -f

# Filter for errors only
journalctl -u stentor-relay -p err

# Logs from the last boot
journalctl -u stentor-relay -b

Key log messages to watch for:

Log Message Meaning
Relay Agent ready, awaiting commands Successful startup
C2 server listening on :8443 C2 listener active
WebSocket client error Connection to backend failed
Received signal SIGTERM, shutting down Graceful shutdown initiated
Command X completed successfully Task executed
Command X failed Task execution error

Beacon Failover & Relay Resilience

Stentor supports multi-relay failover for beacon resilience. If a relay goes down, beacons can automatically switch to a backup relay and transparently recover to the primary when it returns.

Failover URL List

Beacons can be built with multiple C2 URLs. The first URL is always the primary; additional URLs are backup relays. Single-URL deployments are completely unaffected -- failover is backward compatible.

Configure failover URLs at build time via environment variable or linker flag:

# Environment variable (comma-separated)
IMPLANT_C2_URLS=https://relay1.example.com:8443,https://relay2.example.com:8443

# Or via Go ldflags at build time
-ldflags="-X main.DefaultC2URLs=https://relay1.example.com:8443,https://relay2.example.com:8443"

Failover Behavior

When a beacon encounters consecutive failures communicating with its current relay, it automatically switches to the next URL in the list:

Parameter Default Description
MaxFailures 3 Consecutive failure count before switching to the next relay

After 3 consecutive failures on the current host, the beacon advances to the next URL in the failover list. If the last URL also fails, it wraps around to the first.

graph LR
    B[Beacon] -->|3 failures| R1[Relay 1<br/>Primary]
    R1 -.->|down| X1[X]
    B -->|switch| R2[Relay 2<br/>Backup]
    R2 -->|working| OK[Continue]

Primary-Preference Recovery

While operating on a backup relay, the beacon periodically probes the primary to check if it has recovered. This ensures beacons always return to the preferred relay when possible.

Parameter Default Description
PrimaryCheckInterval 5 Successful requests on a backup before probing the primary
PrimaryCheckTimeout 5s Timeout for the primary recovery probe

Recovery flow:

  1. Beacon is running on a backup relay after failover.
  2. After 5 successful requests on the backup, the beacon sends a lightweight probe (minimal check-in) to the primary URL.
  3. If the primary responds within 5 seconds, the beacon transparently switches back to the primary.
  4. If the primary is still down, the beacon stays on the backup and retries after another 5 successful requests.

Transparent to operators

Failover and recovery happen automatically without operator intervention. The beacon continues executing tasks normally throughout the process. Operators can monitor which relay a beacon is connected to via the Cockpit UI.

Listener State Persistence

Relays persist all active listener configurations to disk so they can automatically restore listeners after a restart or crash -- no operator intervention required.

How it works:

  • Listener state is saved to a JSON file in the STATE_DIR directory (default: /var/lib/stentor-relay).
  • Uses atomic writes (write to temp file, then rename) for crash safety -- partial writes never corrupt the state file.
  • State is saved after each listener start or stop, with a 200ms delay to confirm the operation succeeded.
  • On restart, the relay reads the state file and automatically re-starts all previously active listeners.

Configuration:

# Relay state persistence directory (in .env)
STATE_DIR=/var/lib/stentor-relay

Directory permissions

The STATE_DIR directory must be writable by the relay process. If running as root (typical for Kali deployments), the default /var/lib/stentor-relay works out of the box. For non-root deployments, ensure the directory exists and has appropriate permissions.

Failover Configuration Example

A complete failover deployment with two relays and listener state persistence:

# Beacon build with failover URLs (primary + backup)
IMPLANT_C2_URLS=https://relay1.example.com:8443,https://relay2.example.com:8443

# Relay 1 .env (primary)
BACKEND_URL=wss://stentor.app/ws/relay
RELAY_ID=aaaaaaaa-1111-1111-1111-111111111111
RELAY_SECRET=shared-secret
STATE_DIR=/var/lib/stentor-relay
HEALTH_PORT=9090

# Relay 2 .env (backup)
BACKEND_URL=wss://stentor.app/ws/relay
RELAY_ID=bbbbbbbb-2222-2222-2222-222222222222
RELAY_SECRET=shared-secret
STATE_DIR=/var/lib/stentor-relay
HEALTH_PORT=9090

mTLS Authentication

mTLS (mutual TLS) adds per-relay client certificate authentication to the WebSocket connection. It provides cryptographic identity verification as an alternative to shared secret headers.

Overview

mTLS is additive -- when configured, the relay presents a client certificate during the TLS handshake. The server verifies the certificate against a trusted CA and extracts the relay UUID from the certificate's Common Name (CN). Shared secret authentication remains available as a fallback for backward compatibility.

flowchart TD
    R[Relay connects to server] --> TLS{Client cert<br/>present?}
    TLS -->|Yes| V[Verify cert against CA pool]
    V --> CN[Extract relay UUID from CN]
    CN --> AUTH1[Authenticated via mTLS]
    TLS -->|No| REQ{RELAY_MTLS_REQUIRED<br/>= true?}
    REQ -->|Yes| REJECT[Reject connection]
    REQ -->|No| SECRET[Check X-Relay-Secret header]
    SECRET --> AUTH2[Authenticated via shared secret]

Certificate Generation

Generate a CA and relay client certificate using OpenSSL. The certificate CN must follow the format relay-{uuid} -- the server extracts the relay UUID from this field.

# 1. Generate CA key and certificate (one-time setup)
openssl genrsa -out ca.key 4096
openssl req -new -x509 -key ca.key -out ca.crt -days 3650 \
  -subj "/CN=Stentor Relay CA"

# 2. Generate relay client key and CSR
#    CN must be "relay-{uuid}" matching the relay's RELAY_ID
openssl genrsa -out relay.key 4096
openssl req -new -key relay.key -out relay.csr \
  -subj "/CN=relay-aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee"

# 3. Sign the relay certificate with the CA
openssl x509 -req -in relay.csr -CA ca.crt -CAkey ca.key \
  -CAcreateserial -out relay.crt -days 365

CN format is critical

The Common Name must be exactly relay-{uuid} (e.g., relay-25b2e4d7-8b7f-411f-8fe0-7faed06b90d1). The server parses this field to extract the relay UUID for identity verification. An incorrect CN format will cause authentication to fail with ErrInvalidCertCN.

Server Configuration

Configure the Stentor server to accept mTLS connections from relays:

Variable Default Description
RELAY_CA_CERT_PATH -- Path to CA certificate PEM file for verifying relay client certs
RELAY_MTLS_REQUIRED false Set to true to reject shared-secret auth and enforce mTLS-only

The server loads the CA certificate pool on startup and uses it to verify the certificate chain presented by connecting relays. It also checks certificate time validity (not expired, not yet valid) and verifies the ExtKeyUsageClientAuth extended key usage.

Relay Configuration

Configure the relay to present a client certificate when connecting to the server:

Variable Default Description
MTLS_CERT_PATH -- Path to relay client certificate PEM
MTLS_KEY_PATH -- Path to relay client private key PEM
MTLS_CA_CERT_PATH -- Optional CA cert for verifying server certificate

Both cert and key required

MTLS_CERT_PATH and MTLS_KEY_PATH must both be set. Setting only one will cause a configuration validation error on relay startup.

When MTLS_CA_CERT_PATH is not set on the relay side, it uses the system certificate store (or InsecureSkipVerify in development environments with self-signed server certificates).

Example relay .env with mTLS:

# Required
BACKEND_URL=wss://stentor.app/ws/relay
RELAY_ID=aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee
RELAY_SECRET=your-shared-secret-here

# mTLS authentication
MTLS_CERT_PATH=/opt/stentor-relay/certs/relay.crt
MTLS_KEY_PATH=/opt/stentor-relay/certs/relay.key
MTLS_CA_CERT_PATH=/opt/stentor-relay/certs/ca.crt

Migration from Shared Secret to mTLS

Follow these steps to migrate an existing relay from shared-secret authentication to mTLS without downtime:

Step 1: Generate certificates

# Generate CA and relay certificates (see Certificate Generation above)
openssl genrsa -out ca.key 4096
openssl req -new -x509 -key ca.key -out ca.crt -days 3650 \
  -subj "/CN=Stentor Relay CA"
openssl genrsa -out relay.key 4096
openssl req -new -key relay.key -out relay.csr \
  -subj "/CN=relay-<your-relay-uuid>"
openssl x509 -req -in relay.csr -CA ca.crt -CAkey ca.key \
  -CAcreateserial -out relay.crt -days 365

Step 2: Deploy CA cert to server

Copy ca.crt to the server and set RELAY_CA_CERT_PATH in the server's .env. Restart the backend.

Step 3: Deploy client cert/key to relay

Copy relay.crt and relay.key to the relay host (e.g., /opt/stentor-relay/certs/). Set MTLS_CERT_PATH and MTLS_KEY_PATH in the relay's .env.

Step 4: Restart relay

sudo systemctl restart stentor-relay

The relay will now authenticate via mTLS. The shared secret (X-Relay-Secret header) remains as a fallback.

Step 5: Verify mTLS is working

Check server logs for mTLS authentication messages:

# On the server
docker logs stentor-c2-backend 2>&1 | grep -i "mtls\|certificate"

Step 6: Enforce mTLS-only (optional)

Once all relays are using mTLS, optionally disable shared-secret authentication:

# In the server's .env
RELAY_MTLS_REQUIRED=true

Restart the backend. Relays without valid client certificates will no longer be able to connect.

Test before enforcing

Only set RELAY_MTLS_REQUIRED=true after confirming all relays are successfully authenticating via mTLS. Setting this flag with misconfigured relays will lock them out.


Troubleshooting

Symptom Cause Fix
404 on WebSocket connect Relay UUID not in database Register the relay via API or direct SQL before starting the service. Verify RELAY_ID in .env matches the database record.
401 Unauthorized Wrong RELAY_SECRET Verify the relay's RELAY_SECRET matches the server's .env configuration. Both sides must use the same shared secret.
Connection drops repeatedly Network instability or WebSocket buffer overflow Check relay logs with journalctl -u stentor-relay -f. Verify network connectivity to the backend. The relay auto-reconnects every 5 seconds by default.
Listener fails to start Port already in use or permission denied Check for conflicting processes: ss -tlnp \| grep <port>. Use sudo or ports >= 1024 to avoid permission issues.
Payload generation fails Missing Go toolchain or disk space Verify Go is installed on the relay: go version. Check available disk: df -h /opt/stentor-relay.
Beacon not appearing Listener not started or network ACL Verify listener status via API: GET /api/v1/listeners. Check firewall rules between target and relay IP.
"configuration validation failed" Missing or invalid environment variables Check the relay log for the specific validation error. Ensure all required variables (BACKEND_URL, RELAY_ID) are set. Verify BACKEND_URL starts with ws:// or wss://.
DNS C2 not responding DNS port conflict or missing domain Ensure DNS_DOMAIN is set when DNS_PORT > 0. Check for port conflicts: ss -ulnp \| grep <dns-port>.
SMTP send fails External relay misconfigured Verify SMTP_EXTERNAL_HOST, SMTP_EXTERNAL_PORT, and credentials. Test SMTP connectivity from the relay host.

Diagnostic Commands

# Check relay process
systemctl status stentor-relay

# View relay configuration (from logs)
journalctl -u stentor-relay | head -20

# Test WebSocket connectivity
curl -s -o /dev/null -w "%{http_code}" \
  -H "X-Relay-ID: <relay-id>" \
  -H "X-Relay-Secret: <secret>" \
  https://stentor.app/ws/relay

# Check listening ports on relay
ss -tlnp | grep stentor

# Verify network path to target
ping -c 3 <target-ip>

# Check TLS certificate expiry
openssl x509 -in /opt/stentor-relay/certs/cert.pem -noout -dates

Multi-Relay Architecture

Stentor supports multiple simultaneous relay connections for segmented or distributed operations.

graph TB
    subgraph Backend
        S[Stentor Server]
    end

    subgraph External["External Network"]
        R1[Relay 1<br/>Internet-facing<br/>HTTPS listener]
    end

    subgraph Internal["Internal Network"]
        R2[Relay 2<br/>Post-pivot<br/>SMB listener]
    end

    subgraph DMZ
        R3[Relay 3<br/>DNS C2 listener]
    end

    S <-->|WebSocket| R1
    S <-->|WebSocket| R2
    S <-->|WebSocket| R3

    I1[Implant A] -->|HTTPS| R1
    I2[Implant B] -->|SMB pipe| R2
    I3[Implant C] -->|DNS| R3

Key Concepts

  • Each listener is bound to a specific relay via the relay_id field in the listener configuration. When you create a listener, you specify which relay should host it.

  • Beacons check in to whichever relay hosts their listener. A beacon deployed via an HTTPS listener on Relay 1 will always communicate through Relay 1.

  • SOCKS tunnels can use any connected relay for traffic routing. The RelayHub selects the first available relay for tunnel frame delivery.

  • Multiple relays can serve different network segments. Use separate relays for:

    • Internet-facing infrastructure (HTTPS listeners)
    • Internal network pivots (SMB pipe listeners)
    • Covert channels (DNS C2)
    • Geographic distribution across different regions

Deployment Patterns

Pattern Relays Use Case
Single relay 1 Lab testing, simple engagements
Dual relay 2 External + internal network access
Multi-segment 3+ Complex environments with network segmentation
Geographic 2+ Distributed targets across multiple sites

Listener Assignment

When creating a listener, specify the target relay:

# Create HTTPS listener on Relay 1
curl -s -X POST https://stentor.app/api/v1/listeners \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "External HTTPS",
    "type": "https",
    "host": "10.0.0.50",
    "port": 8443,
    "relay_id": "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee"
  }'

# Create SMB listener on Relay 2 (internal)
curl -s -X POST https://stentor.app/api/v1/listeners \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Internal SMB",
    "type": "smb_pipe",
    "host": "192.168.1.50",
    "port": 445,
    "relay_id": "<internal-relay-uuid>"
  }'

Updating a Relay

To deploy a new version of the relay binary:

  1. Build the new binary on your development machine.
  2. Transfer via the jump host pattern described above.
  3. Restart the service:
# On the relay host (or via SSH)
sudo systemctl restart stentor-relay

# Verify the new version
journalctl -u stentor-relay | grep "Version:"

The relay re-establishes its WebSocket connection automatically on restart. Active listeners are automatically restored from persisted state (see Listener State Persistence) -- no manual restart required.

Automatic listener recovery

Listeners are now persisted to disk and auto-restored on relay restart. You no longer need to manually restart listeners after updating the relay binary.