# etrelay UDP relay + master heartbeat for Wolfenstein: Enemy Territory servers reachable only through a tunnel: runs on the front server (public IP) and makes the servers show up in the in-game browser. Each server gets its own public port. ``` players / masters ──> front :27960 (etrelay) ══wg══> ET :27960 front :27961 (etrelay) ══wg══> ET :27960 front :8090 (control API) <══wg══ admin tools ``` A server is relayed because its admin tool (etadm) **registered** it through the control API, or because it was given on the command line (one server, no control API over it). - **Proxy**: one upstream socket per client address, packets forwarded as-is both ways, session dropped after 2 min of silence. - **Filter**: connectionless packets pass only for what clients and masters send a server (`getstatus`, `getinfo`, `getchallenge`, `connect`, PunkBuster's `PB_`). `rcon` never reaches the server from the internet: it travels in clear, and a stream of it blocks the local admin tool (2.60b runs one rcon per 500 ms, whoever sends it). `getstatus`/`getinfo` are limited per source IP (burst of 5, then 1/s): their answers are much bigger than the query, a spoofed source would make the server a DDoS reflector. At most 2048 sessions (one socket each). - **Heartbeat**: every 5 min, `getinfo` to the ET server; if it answers, `heartbeat EnemyTerritory-1` to the masters **from the server's public socket**. When it stops answering, or when the server leaves the relay, one `heartbeat ETFlatline-1`. Masters list servers by address and port, several on one address are fine. They check (`getChallenge`, `getInfo`) the address the heartbeat came from: the server's public port. Those packets go through the relay like any client, the ET server answers them, and the master lists the front public address. How it works in detail, with diagrams: [ENGINE.md](ENGINE.md). ## Usage ```bash etrelay [UPSTREAM] [--listen 0.0.0.0:27960] [--ports 27960-27969] [--master host:port]... [--control ] [--state-dir /var/lib/etrelay] [--debug] ``` - `--control `: servers register through the control API, on a port of `--ports` bound on `--listen`'s address. They are kept in `/servers.json` and relayed again after a restart. - `UPSTREAM` (an ET server's `ip:port`): relayed on `--listen` from the start, without registering. Nothing reaches it through the control API: no real addresses, no bans. It can't be registered, nor its port taken. One of the two at least. The ports of `--ports` must be open (UDP) on the front's firewall. Default masters: `etmaster.idsoftware.com:27950` (id Software, the one ET 2.60b clients query) and `etmaster.net:27950` (ET: Legacy community). Passing `--master` replaces both. `--debug` logs sessions (open/close, packet counts), connectionless packets by command name only (`getinfo`, `getstatus`, `connect`...: no arguments, `connect` carries the userinfo), probes and heartbeats. In-game packets are only counted. ``` probe :27960: up ET server :27960 is up heartbeat EnemyTerritory-1 -> etmaster.net:27950 (198.51.100.10:27950) session + 198.51.100.10:27950 as port 51234 (2 active on port 27960) 198.51.100.10:27950 -> ET getChallenge (27 B) 198.51.100.10:27950 <- ET challengeResponse (32 B) 198.51.100.10:27950 -> ET getInfo (22 B) 198.51.100.10:27950 <- ET infoResponse (303 B) session - :27960: 5321 packets to ET, 4876 from ET ``` As a service: add `--debug` to `ExecStart`, then `systemctl daemon-reload && systemctl restart etrelay`. Without `--debug`, each player joining is logged with its real address and the port the ET server sees it on (`connect 203.0.113.7:27960 as port 51234`): browser queries aren't. ## Control API `--control ` serves a small HTTP API for admin tools (etadm), on the tunnel address only (e.g. `10.66.0.1:8090`: unreachable from the internet). Binding is retried every 10 s: the tunnel may come up after etrelay. ### Registering a server An admin tool registers the ET server it runs, with two things: - **the relay's token** (`$ETRELAY_CONTROL_TOKEN`, 32 characters at least): it only lets a server register; - **a key it picks** (32 characters at least), and the proof it runs that server: it makes the server show the key's fingerprint in the serverinfo cvar `etrelay_proof` (`sets etrelay_proof ` on its console), where etrelay reads it with a `getstatus` before taking the key. The fingerprint is the first 32 hex digits of the key's SHA-256: public while registering, it doesn't give the key away. The tool removes the cvar afterwards (`set etrelay_proof ""`). Then every other call carries the key (`Authorization: Bearer `), which names the server: a tool only sees and changes its own server. etrelay keeps the key's SHA-256, never the key. | Method | URL | Bearer | | |---|---|---|---| | PUT | `/server` | token | `{"name": "...", "port": 27960, "upstream_ip": "10.66.0.20", "upstream_port": 27960, "key": "..."}`: register the ET server `upstream_ip:upstream_port` (as seen from the relay; `upstream_ip` left out: the caller's address) on the public `port`. Answers the registration (below) | | GET | `/server` | key | `{"port", "upstream", "name", "since", "alive"}`: `alive`, the ET server answered the last probe, the masters are told | | DELETE | `/server` | key | leave the relay: the port is freed, the masters get a flatline, the server's bans are dropped (204) | | GET | `/sessions` | key | `[{"client": "203.0.113.7:27960", "port": 51234, "since": }]`: open sessions | | GET | `/bans` | key | `[{"ip", "reason", "at", "until"}]` (`until` null: until lifted) | | PUT | `/bans/{ip}` | key | `{"reason": "...", "minutes": N}` (`minutes` left out: until lifted); cuts the address's sessions at once | | DELETE | `/bans/{ip}` | key | 204, 404 when not banned | `PUT /server` answers: | Status | | |---|---| | 200 | registered | | 400 | port outside `--ports`, key too short | | 401 | wrong token | | 403 | the ET server doesn't answer, or doesn't show the key's fingerprint | | 409 | another server holds the port, or the ET server is the command line's | One server per public port, one port per server. Registering a server again (same ET server, another key: an admin tool reinstalled) takes it over: who proves to run it holds it, the former key is refused from then on. On the same port its sessions and bans go on; on another port the former one is freed. Unknown keys get 401: a tool whose relay lost its state registers again. ### Sessions and bans The ET server sees every player as `:`, the port of the player's session socket; `/sessions` maps it back to the real address. A banned address's packets are dropped before reaching the server: the player times out, and can't come back. Each server has its own bans, kept in `/bans-.json` (written atomically), expired ones forgotten (`bans.json`: the command line's server). ## Build ```bash make build # dynamic (glibc) make static # static (musl), runs on any x86_64 Linux make release # both ``` Artifacts land in `dist/`, named `-v-` with a `.sha256` checksum: ``` dist/etrelay-v0.3.0-x86_64-unknown-linux-gnu # dynamic dist/etrelay-v0.3.0-x86_64-unknown-linux-musl # static ``` ## Run as a service 1. The token servers register with, on the front server (root only, systemd hands it to the service): ```bash install -d -m 700 /etc/etrelay echo "ETRELAY_CONTROL_TOKEN=$(head -c 32 /dev/urandom | od -An -tx1 | tr -d ' \n')" > /etc/etrelay/env ``` 2. Binary and unit ([etrelay.service](etrelay.service) is a template: `@UPSTREAM@` is nothing when servers register, or one ET server's `ip:port` as seen from the front; `@ARGS@` the flags): ```bash install -m 755 dist/etrelay-v-x86_64-unknown-linux-musl /usr/local/bin/etrelay sed -e 's#@UPSTREAM@##' -e 's#@ARGS@#--control :8090 --ports 27960-27969#' etrelay.service > /etc/systemd/system/etrelay.service systemctl daemon-reload && systemctl enable --now etrelay ``` 3. The UDP ports of `--ports` open on the front. Nothing else may capture them (DNAT rule, nginx `stream`...), or players bypass the relay: `iptables -t nat -S | grep 27960` must print nothing. 4. Each admin tool gets the relay's address, the token and the public port it asks for (etadm: `--relay-url`, `--relay-token-file`, `--relay-port`, or its Relay page). 5. ET server `server.cfg`: stop its own heartbeats, they leave through the home connection with the wrong IP: ``` set sv_master1 "" set sv_master2 "" set sv_master3 "" set sv_master4 "" set sv_master5 "" ``` ## Check ```bash journalctl -u etrelay -n 50 # "registered ... on port 27960", "ET server ... is up" tcpdump -ni any udp port 27950 # heartbeat out, getChallenge/getInfo in, answers out ``` The server then shows up on the master (in-game server browser).