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══> <wg-peer>:27960 (nginx stream) ──> ET <et-server>:27960
- Proxy: one upstream socket per client address, packets forwarded as-is both ways, session dropped after 2 min of silence.
- Heartbeat: every 5 min,
getinfoto the ET server; if it answers,heartbeat EnemyTerritory-1to the masters from the public socket. When it stops answering, oneheartbeat 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.
Usage
etrelay <UPSTREAM> [--listen 0.0.0.0:27960] [--master host:port]... [--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 <wg-peer>:27960: up
ET server <wg-peer>:27960 is up
heartbeat EnemyTerritory-1 -> etmaster.net:27950 (198.51.100.10:27950)
session + 198.51.100.10:27950 (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 - <player>:27960: 5321 packets to ET, 4876 from ET
As a service: add --debug to ExecStart, then systemctl daemon-reload && systemctl restart etrelay.
Build
make build # dynamic (glibc)
make static # static (musl), runs on any x86_64 Linux
make release # both
Artifacts land in dist/, named <name>-v<version>-<target triple> 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
- Binary and unit on the front server (etrelay.service is a template: replace
@UPSTREAM@with the ET serverip:portas seen from the front,@ARGS@with extra flags or nothing):install -m 755 dist/etrelay-v<version>-x86_64-unknown-linux-musl /usr/local/bin/etrelay sed -e 's#@UPSTREAM@#<ip:port>#' -e 's#@ARGS@##' etrelay.service > /etc/systemd/system/etrelay.service systemctl daemon-reload && systemctl enable --now etrelay - Nothing else on the front may capture UDP 27960 (DNAT rule, nginx
stream...), or players bypass the relay:iptables -t nat -S | grep 27960must print nothing. - 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
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).