HydraBooks

HydraNeckWebRTC Runbook

14.7 KB Pushed by api Updated 23 Mar 2026 Raw

HydraNeckWebRTC Runbook

Overview

HydraNeckWebRTC is a centralized WebRTC relay service. It runs moonlight-web-stream processes on worker machines and proxies WebRTC streams to browsers. A controller routes session requests to the least-loaded worker.

Architecture

Two hydracluster roles map to systemd services:

Both roles use the same binary (/usr/local/bin/hydraneckwebrtc). Hydranode handles downloading the binary and creating the systemd service for both roles.

Automated Setup (via hydracluster)

Worker nodes are fully automated through hydracluster recipes:

  1. Enroll a new Linux server in hydracluster
  2. Assign the hydraneckwebrtc role (and optionally hydraneckwebrtc-controller for the singleton)
  3. Trigger provisioning. The recipe will:
    • Verify WireGuard tunnel is active (enables wg-quick@<iface> for any config in /etc/wireguard/)
    • Install coturn via apt
    • Generate a random TURN credential (persisted at /root/.hydraneckwebrtc-turn-credential)
    • Write /etc/turnserver.conf with the node's IP and credential
    • Start coturn
    • Write /root/.hydraneckwebrtc/config.yaml with controller URL, token, and ice_servers
  4. Hydranode downloads the binary and creates the systemd service
  5. The worker starts and registers with the controller via heartbeat

hydracluster config

Add to ~/.hydracluster/config.yaml:

hydraneckwebrtc:
  controller_url: "https://hydraneckwebrtc.experiencenet.com"
  controller_token: "controller-admin-token"
  admin_token: "worker-admin-token"

Manual Installation

Prerequisites

Install binary

curl -o /usr/local/bin/hydraneckwebrtc \
  https://releases.experiencenet.com/hydraneckwebrtc/production/latest/hydraneckwebrtc-linux-amd64
chmod +x /usr/local/bin/hydraneckwebrtc

Install systemd services

# Worker
cp scripts/hydraneckwebrtc.service /etc/systemd/system/
# Controller (if running on this machine)
cp scripts/hydraneckwebrtc-controller.service /etc/systemd/system/
systemctl daemon-reload

Configuration

Config file: ~/.hydraneckwebrtc/config.yaml

Controller config

mode: controller
server:
  domain: hydraneckwebrtc.experiencenet.com
  admin_token: <secure-token>
workers:
  heartbeat_timeout: 60s

Worker config

mode: worker
server:
  listen: ":47990"
  admin_token: <secure-token>
controller:
  url: https://hydraneckwebrtc.experiencenet.com
  token: <controller-admin-token>
sessions:
  max: 15
  port_range_start: 8080
  return_url: https://hydraheadwebstream.experiencenet.com  # optional, default
sunshine:
  username: sunshine
  password: sunshine
ice_servers:
  - urls: ["stun:stun.l.google.com:19302"]
  - urls: ["turn:<server-public-ip>:3478"]
    username: "hydraturn"
    credential: "<turn-password>"
nps:
  url: https://hydranps.experiencenet.com  # optional, session records sent at session end
  token: <nps-admin-token>

Single-machine deployment (controller + worker on same server)

When both run on the same machine, the controller proxies /session/ paths to the worker. Stream URLs use the controller's domain, so browsers connect to the controller which forwards to the worker internally.

Worker config for colocated setup (listens on a local port, no domain needed):

server:
  listen: ":8090"
  admin_token: <same-as-controller>
controller:
  url: https://hydraneckwebrtc.experiencenet.com
  token: <same-as-controller>
sessions:
  max: 15
  port_range_start: 8080
sunshine:
  username: sunshine
  password: sunshine

Dev mode

Use server.listen instead of server.domain for plain HTTP:

server:
  listen: ":8080"
  admin_token: dev-token

Common Operations

Start the services

# Controller
systemctl enable --now hydraneckwebrtc-controller

# Worker
systemctl enable --now hydraneckwebrtc

Check health

# Controller
curl -sf https://hydraneckwebrtc.experiencenet.com/api/v1/health | jq .

# Worker (shows WireGuard status, active sessions, capacity)
curl -sf http://localhost:47990/api/v1/health | jq .

Worker health response includes:

Quick readiness check (single command):

curl -sf https://hydraneckwebrtc.experiencenet.com/api/v1/health | jq '{
  status: .status,
  wireguard: .extra.wireguard,
  sessions: "\(.extra.active_sessions)/\(.extra.max_sessions)",
  process_id: .extra.process_id,
  uptime: .extra.uptime
}'

List workers (from controller)

curl -H "Authorization: Bearer <token>" \
  https://hydraneckwebrtc.experiencenet.com/api/v1/workers

Create a session

curl -X POST -H "Authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d '{"body_ip":"10.0.1.50","sunshine_user":"sunshine","sunshine_pass":"sunshine"}' \
  https://hydraneckwebrtc.experiencenet.com/api/v1/sessions

Optional fields: experience (name), exe_path (Windows path on the body), district. When exe_path is set, the worker registers the app in Sunshine and launches it via hydrabody before starting the stream (both non-fatal). When experience is set, it is stored for experience rating.

Returns session_id, stream_url, and worker_id.

Stream-ended overlay

When a browser connects to /session/{id}/..., the worker reverse proxy injects a script into HTML responses. This script detects WebRTC disconnects and shows a "Stream ended" overlay with a "Start new stream" link back to sessions.return_url (defaults to https://hydraheadwebstream.experiencenet.com).

The injected script also intercepts the moonlight-web-stream exit button, calling POST /session/{id}/quit (unauthenticated) to clean up the session server-side before redirecting.

The overlay includes a "Report a problem" link that opens https://issues.experiencenet.com/report in a new tab with session context prefilled as query parameters: project, category, title, session_id, experience, district, body, head, body_ip, disconnect_count, duration_ms, end_reason, and ice_candidate_type.

The overlay includes a star rating widget. When the user rates, POST /session/{id}/rating is sent to the worker, which forwards it to the experience library.

Browser mic relay

The injected script adds a mic toggle button (bottom-right of the stream page). When enabled:

  1. Browser captures mic via getUserMedia({audio: true})
  2. Creates a send-only WebRTC PeerConnection for the audio track
  3. Signals via POST /session/{id}/mic/offer (unauthenticated, browser-facing, proxied through controller)
  4. Worker creates a pion/webrtc PeerConnection (receive-only), forwards raw RTP packets via UDP to body_ip:47995 over WireGuard
  5. On the body, hydravoice receives RTP and renders to VB-Cable via ffmpeg
  6. UE reads from the "CABLE Output" virtual mic

Mic is automatically stopped when the stream ends.

List sessions

# All sessions across workers (via controller)
curl -H "Authorization: Bearer <token>" \
  https://hydraneckwebrtc.experiencenet.com/api/v1/sessions

Delete a session

curl -X DELETE -H "Authorization: Bearer <token>" \
  https://hydraneckwebrtc.experiencenet.com/api/v1/sessions/<session-id>

On deletion (or cleanup), if the session had an experience launched via exe_path, the worker calls POST /api/v1/stop on the body to kill the experience processes.

Update the binary

hydraneckwebrtc update
# Or auto-update runs every 6 hours

Check version

hydraneckwebrtc version
hydraneckwebrtc check-update

Scaling

  1. Start with one machine running both controller + worker (two systemd services)
  2. Add worker machines: assign hydraneckwebrtc role in hydracluster, trigger provision
  3. Recipe installs coturn, writes config with controller URL/token, worker auto-registers
  4. Controller picks least-loaded worker for each new session
  5. Browsers connect directly to workers for WebRTC (no load balancer needed for media)
  6. Workers that stop heartbeating are marked unhealthy after heartbeat_timeout

TURN Server (coturn)

All WebRTC media is relayed through TURN (iceTransportPolicy: 'relay' is forced on the browser side). Direct host/srflx candidates are disabled because NAT binding timeouts cause stream drops. coturn is automatically installed and configured by the hydraneckwebrtc recipe on each worker node.

Manual setup (if not using recipe)

apt install coturn

Config (/etc/turnserver.conf):

listening-port=3478
external-ip=<server-public-ip>
realm=hydraneckwebrtc.experiencenet.com
server-name=hydraneckwebrtc.experiencenet.com
lt-cred-mech
user=hydraturn:<password>
min-port=49152
max-port=65535
no-multicast-peers
no-cli
log-file=/var/log/turnserver.log
simple-log
allowed-peer-ip=10.10.0.0-10.10.255.255

Firewall

Ports required:

Verify

# Check coturn is running
systemctl status coturn

# Check listening
ss -ulnp | grep 3478

# In browser console during a stream, look for "typ relay" ICE candidates

Troubleshooting

Worker not registering with controller

Session creation fails

Pairing fails

WireGuard down on worker

Symptoms: all sessions to WireGuard-routed bodies fail with "body unreachable". Health endpoint shows "wireguard": false and "status": "degraded".

# Check WireGuard status
wg show
ip link show type wireguard

# Start WireGuard (config must exist in /etc/wireguard/)
systemctl enable --now wg-quick@wg0

# Verify body reachable
ping -c 2 -W 3 10.10.100.6
curl -sk -u sunshine:sunshine https://10.10.100.6:47990/api/currentClient

The neckwebrtc recipe now auto-enables WireGuard on provision, but if the server was set up before this change or WG was manually stopped, re-enable it manually. The worker logs a WARNING: no WireGuard interface detected on startup if no WG interface is found.

Session process dies

Duplicate instance prevention

Orphaned processes after crash

Port conflicts

Logs

# systemd journal
journalctl -u hydraneckwebrtc -f          # worker
journalctl -u hydraneckwebrtc-controller -f  # controller

# Key log prefixes
# [controller] - controller routing decisions
# [worker] - worker session management
# [session <id>] - per-session lifecycle
# [pairing] - Sunshine pairing flow
# [heartbeat] - worker-to-controller heartbeat
# [cleanup] - expired session cleanup
# [proxy] - reverse proxy errors
# [mic] - browser mic WebRTC connections
# [rating] - experience ratings

Releasing

  1. Tag: git tag v<X.Y.Z> && git push origin v<X.Y.Z>
  2. GitHub Actions builds linux-amd64 and linux-arm64 binaries
  3. Uploads to GitHub Releases and releases.experiencenet.com
  4. Running instances auto-update on next 6-hour check cycle