# etrelay UDP relay + master heartbeat for a Wolfenstein: Enemy Territory server reachable only through a tunnel: runs on the front server (public IP) and makes the server show up in the in-game browser. ``` players / masters ──> front :27960 (etrelay) ══wg══> ET :27960 front :8090 (control API) <══wg══ admin tool ``` - **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 public socket**. When it stops answering, one `heartbeat ETFlatline-1`. Masters check (`getChallenge`, `getInfo`) the address the heartbeat came from: the public `:27960`. 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 [--listen 0.0.0.0:27960] [--master host:port]... [--control ] [--state-dir /var/lib/etrelay] [--debug] ``` 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) 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 the admin tool (etadm), on the tunnel address only (e.g. `10.66.0.1:8090`: unreachable from the internet), with `Authorization: Bearer $ETRELAY_CONTROL_TOKEN` (32 characters at least). Binding is retried every 10 s: the tunnel may come up after etrelay. The ET server sees every player as `:`, the port of the player's session socket; the API maps it back to the real address. | Method | URL | | |---|---|---| | GET | `/sessions` | `[{"client": "203.0.113.7:27960", "port": 51234, "since": }]`: open sessions | | GET | `/bans` | `[{"ip", "reason", "at", "until"}]` (`until` null: until lifted) | | PUT | `/bans/{ip}` | `{"reason": "...", "minutes": N}` (`minutes` left out: until lifted); cuts the address's sessions at once | | DELETE | `/bans/{ip}` | 204, 404 when not banned | A banned address's packets are dropped before reaching the server: the player times out, and can't come back. Bans are kept in `/bans.json` (written atomically), expired ones forgotten. ## 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.1.0-x86_64-unknown-linux-gnu # dynamic dist/etrelay-v0.1.0-x86_64-unknown-linux-musl # static ``` ## Run as a service 1. Binary and unit on the front server ([etrelay.service](etrelay.service) is a template: replace `@UPSTREAM@` with the ET server `ip:port` as seen from the front, `@ARGS@` with extra flags or nothing): ```bash install -m 755 dist/etrelay-v-x86_64-unknown-linux-musl /usr/local/bin/etrelay sed -e 's#@UPSTREAM@##' -e 's#@ARGS@##' etrelay.service > /etc/systemd/system/etrelay.service systemctl daemon-reload && systemctl enable --now etrelay ``` 2. Nothing else on the front may capture UDP 27960 (DNAT rule, nginx `stream`...), or players bypass the relay: `iptables -t nat -S | grep 27960` must print nothing. 3. 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 # "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).