Repository navigation
Relay Operations
A relay node is a dedicated host that runs ggscale-server relay. It runs only the TURN listener. It has no database, HTTP API, or matchmaker. The API servers issue relay credentials. The relay node checks them with the shared secret.
The relay section of .env.example lists every setting. Configuration Reference lists the main ones. P2P Connectivity and TURN Relay covers the client side.
The API servers keep a registry of relay nodes. A platform admin registers and retires nodes through the admin API. A probe reads the GET /lifecycle route of each node every 10 seconds. Credentials list only nodes that are up and active. You do not restart the API servers to add, replace, or roll back a node.
On each relay node:
RELAY_PUBLIC_IP=203.0.113.10 # IPv4 address clients reach
RELAY_SHARED_SECRET=... # >= 32 bytes, same value as the API servers
RELAY_HEALTH_ADDR=:9091 # /healthz, /metrics, lifecycle routes
LIFECYCLE_TOKEN=... # >= 32 bytes, enables drain and resume
LIFECYCLE_READ_TOKEN=... # >= 32 bytes, read-only, for the probeOn the API servers:
PLATFORM_ADMIN_TOKEN=... # >= 32 bytes, enables /v1/admin/relay-nodes
RELAY_PROBE_TOKEN=... # the relay nodes' LIFECYCLE_READ_TOKENThe API servers must reach RELAY_HEALTH_ADDR, because the probe reads GET /lifecycle there. Give the relay nodes their own tokens, and give the probe only the read token. See Give relay nodes their own token. LIFECYCLE_READ_TOKEN needs v0.9.74 or later; on v0.9.73, set RELAY_PROBE_TOKEN to the relay nodes' LIFECYCLE_TOKEN.
| Port | Protocol | Setting | Default |
|---|---|---|---|
| TURN | UDP | RELAY_UDP_PORT |
3478 |
| TURN over TCP | TCP | RELAY_TCP_PORT |
off |
| TURNS (TLS) | TCP |
RELAY_TLS_PORT, with RELAY_TLS_CERT_FILE and RELAY_TLS_KEY_FILE
|
off |
| Relayed media | UDP |
RELAY_MIN_PORT to RELAY_MAX_PORT
|
OS ephemeral range |
| Health, metrics, lifecycle | TCP | RELAY_HEALTH_ADDR |
off |
Set a media port range, so that the firewall opens only that range. RELAY_MAX_ALLOCATIONS defaults to 4000. Keep it at or below the width of the range. Keep RELAY_HEALTH_ADDR on a private network, for example a tailnet. Do not put it on the public interface.
With TURNS on, the node reads the certificate and key files each minute. When their content changes, it loads the new pair, so a renewal does not need a restart. If a pair does not load, the node keeps the last good certificate, logs the error, and tries again.
GET /healthz returns 200 while the process runs, also while it drains. GET /readyz returns 200 only while the node accepts new allocations. GET /lifecycle returns the state, live allocations, public IP, and transports as JSON. Process Lifecycle documents these three routes. GET /metrics serves Prometheus metrics. When RELAY_METRICS_TOKEN is set, it needs that bearer token. RELAY_METRICS_TOKEN has no _FILE form.
| Metric | Alert on |
|---|---|
up (from Prometheus) |
up == 0: the scrape target is down. Do not use ggscale_relay_up for this. It is 1 while the process serves, and a stopped process sends no value. |
ggscale_relay_active_allocations |
Approaching RELAY_MAX_ALLOCATIONS or the media port range width. |
ggscale_relay_allocations_rejected_total |
Allocations refused by the node cap. |
ggscale_relay_auth_failures_total |
Credential probing, or a shared secret that differs from the API servers'. |
ggscale_relay_alloc_throttled_total |
One credential hitting the per-player allocation limit. |
ggscale_relay_peer_rejected_total |
Attempts to relay into private address space. |
ggscale_lifecycle_state |
A node left in draining after a deploy. |
ggscale_lifecycle_work_refused_total |
Clients still sent to a draining node. |
ggscale_relay_no_eligible_nodes_total |
On the API servers: credential requests refused because no node was eligible. |
ggscale_relay_registry_node_eligible |
On the API servers: 0 for a node that credentials do not list. Each API server reports the same registry, so aggregate with max by (node_id). |
ggscale_relay_registry_node_probe_ok |
On the API servers: 0 for a node that serves but failed its last probe. Aggregate the same way. |
A node is eligible when all of these are true:
- It is registered and not retired.
- Its last probe was less than 30 seconds ago.
- That probe got a
2xxwithrole: "relay"andstate: "active".
POST /v1/relay/credentials?region=<name> lists the TURN URLs of each eligible node in that region. The region match ignores case. The nodes come in random order. The URLs of one node keep their registered order.
If the region has no eligible node, or the client sends no region, the response lists every eligible node. Then the region field is "" if those nodes are in more than one region. If no node is eligible, the request fails with 503 and Retry-After: 10.
A node drops out after its next probe if one of these occurs:
- It stops answering.
- It answers with a status other than
2xx. - It reports a different
role. - It reports
draining.
If no probes run, a node drops out when its last good probe is 30 seconds old.
A 401 means that RELAY_PROBE_TOKEN matches neither the LIFECYCLE_READ_TOKEN nor the LIFECYCLE_TOKEN of the node. One API server in the cluster logs this one time for each node. In the admin list, last_probe_error tells why the last probe failed, for example timeout, status 503, or unauthorized.
The probe is the River job relay_node_probe. It runs on its own queue, one time across the API server cluster, so slow jobs on the default queue cannot delay it. If River does not start, each API server logs a warning. It then probes the nodes itself every 10 seconds.
A change on a node shows in credentials after about 15 seconds:
- Up to 10 seconds until the next probe.
- Up to 3 seconds for the probe timeout.
- Up to 2 seconds while each API server uses its cached list of eligible nodes.
A delay in the probe job adds to this time. To see when a change applies, watch the node in the admin list. When its eligible and last_probe_at fields show the change, wait 2 more seconds.
The routes exist only when PLATFORM_ADMIN_TOKEN is set, and they take it as a bearer token. The API reference lists them under Platform Admin.
API=https://api.example.com
ADMIN=... # PLATFORM_ADMIN_TOKEN
# Register a node. It is listed once a probe sees it active.
curl -fsS -X POST -H "Authorization: Bearer $ADMIN" -H 'Content-Type: application/json' \
"$API/v1/admin/relay-nodes" -d '{
"region": "us-east",
"turn_urls": [
"turn:relay-2.example.com:3478?transport=udp",
"turns:relay-2.example.com:5349?transport=tcp"
],
"lifecycle_url": "http://10.0.0.6:9091/lifecycle"
}'
# List nodes with their last probe and whether they are eligible.
curl -fsS -H "Authorization: Bearer $ADMIN" "$API/v1/admin/relay-nodes"
# Retire a node: never listed or probed again. The row is kept.
curl -fsS -X POST -H "Authorization: Bearer $ADMIN" "$API/v1/admin/relay-nodes/7/retire"A region contains lowercase letters, digits, and dashes. Only one registered node can use a given lifecycle_url. You can register the URL of a retired node again.
lifecycle_url can point at a loopback or private address. The server refuses these addresses:
- Link-local addresses, including
169.254.169.254. - Cloud metadata addresses.
- Multicast and unspecified addresses.
It checks at registration, and again after DNS resolution when the probe connects. The probe ignores proxy settings and does not follow redirects.
An API server runs a TURN relay inside it when all of these are set:
-
FEATURE_P2P_RELAY_ENABLED=true. -
RELAY_SHARED_SECRET. -
RELAY_PUBLIC_IP, as an IPv4 address.
The server advertises it as turn:<RELAY_PUBLIC_IP>:<RELAY_UDP_PORT>?transport=udp. Clients can reach it at another port or a hostname, for example when Docker publishes the UDP port on a different host port. Then list the URIs to advertise in RELAY_EMBEDDED_URLS.
The embedded relay has no region. A response lists it only in two cases:
- The response falls back to every eligible node. This includes the case where no registered node is eligible.
- The server cannot read the registry.
A single-host install therefore needs no registration.
Nothing probes the embedded relay, and you cannot drain it. Do not register it as a node. Its GET /lifecycle reports role: "api", so the probe marks it unhealthy. To stop listing it, unset RELAY_PUBLIC_IP on that API server.
A draining relay refuses new TURN Allocate requests. Its current allocations continue to refresh and relay data, so calls in progress continue.
NEW=http://10.0.0.6:9091 # new node's RELAY_HEALTH_ADDR
OLD=http://10.0.0.5:9091 # old node's RELAY_HEALTH_ADDR
TOKEN=... # LIFECYCLE_TOKEN- Register the new node through the admin API.
- Wait until
GET $NEW/readyzreturns200and the admin list shows the new node with"eligible": true. Then wait 2 more seconds. - Drain the old node with
POST $OLD/lifecycle/drain. Wait until the admin list shows it with"eligible": falseandlast_lifecycle_statedraining. Then wait 2 more seconds. New credentials no longer list it. - Wait for
.details.active_allocationsinGET $OLD/lifecycleto reach0. Credentials issued before the drain list the old node until they expire afterRELAY_CRED_TTL. The old node refuses new allocations from them. The next credential request of the client lists the new node. - Retire the old node with
POST /v1/admin/relay-nodes/<old id>/retire, then stop it. With nothing left to drain,SIGTERMexits at once.
curl -fsS "$NEW/readyz"
curl -fsS -X POST -H "Authorization: Bearer $TOKEN" "$OLD/lifecycle/drain"
curl -fsS -H "Authorization: Bearer $TOKEN" "$OLD/lifecycle" | jq .details.active_allocationsTo roll back before step 5, run POST $OLD/lifecycle/resume and POST $NEW/lifecycle/drain. Each change shows in credentials after a probe records it. Retire the new node when its active_allocations is 0. After step 5, register the old node again instead of resuming it.
DRAIN_TIMEOUT defaults to 0s on a relay node. For the reason and the container stop timeout, see Process Lifecycle.
GG Scale no longer reads RELAY_URLS. An API server with RELAY_URLS set refuses to start. To upgrade:
- On the API servers, set
PLATFORM_ADMIN_TOKENandRELAY_PROBE_TOKEN, and removeRELAY_URLS. Give each relay node aRELAY_HEALTH_ADDRthat the API servers can reach. Set itsLIFECYCLE_READ_TOKEN(or, on v0.9.73, itsLIFECYCLE_TOKEN) to the value ofRELAY_PROBE_TOKEN. - Deploy. Relay credential requests get
503until a probe sees a registered node. An API server with an embedded relay is the exception. - Register every relay node that was in
RELAY_URLS.
RELAY_STUN_URLS is unchanged.
The relay accepts each configured secret. The API servers sign with RELAY_SHARED_SECRET. You can therefore rotate the secret with no downtime:
- Set the new value as
RELAY_SHARED_SECRET_NEXTon every relay node and API server. - Everywhere, move the new value to
RELAY_SHARED_SECRETand the old value toRELAY_SHARED_SECRET_NEXT. - After one
RELAY_CRED_TTL, clearRELAY_SHARED_SECRET_NEXT.
Credentials issued before each step stay valid until they expire.
- Overview