From 6b9d14f86b54868b13d49fef806ef0624503b335 Mon Sep 17 00:00:00 2001 From: rmanach Date: Sat, 26 Sep 2026 14:52:58 +0200 Subject: [PATCH] add etrelay UDP relay + etmasters script + update README --- .gitignore | 5 + README.md | 42 ++++- etmasters.py | 161 +++++++++++++++++ relay/Cargo.lock | 258 +++++++++++++++++++++++++++ relay/Cargo.toml | 16 ++ relay/ENGINE.md | 96 ++++++++++ relay/Makefile | 70 ++++++++ relay/README.md | 77 ++++++++ relay/diagrams/architecture.png | Bin 0 -> 24817 bytes relay/diagrams/architecture.puml | 21 +++ relay/diagrams/before.png | Bin 0 -> 22367 bytes relay/diagrams/before.puml | 14 ++ relay/diagrams/heartbeat-states.png | Bin 0 -> 17418 bytes relay/diagrams/heartbeat-states.puml | 10 ++ relay/diagrams/player-session.png | Bin 0 -> 35742 bytes relay/diagrams/player-session.puml | 19 ++ relay/diagrams/registration.png | Bin 0 -> 49556 bytes relay/diagrams/registration.puml | 28 +++ relay/diagrams/tasks.png | Bin 0 -> 35579 bytes relay/diagrams/tasks.puml | 26 +++ relay/etrelay.service | 19 ++ relay/src/main.rs | 253 ++++++++++++++++++++++++++ 22 files changed, 1106 insertions(+), 9 deletions(-) create mode 100644 .gitignore create mode 100755 etmasters.py create mode 100644 relay/Cargo.lock create mode 100644 relay/Cargo.toml create mode 100644 relay/ENGINE.md create mode 100644 relay/Makefile create mode 100644 relay/README.md create mode 100644 relay/diagrams/architecture.png create mode 100644 relay/diagrams/architecture.puml create mode 100644 relay/diagrams/before.png create mode 100644 relay/diagrams/before.puml create mode 100644 relay/diagrams/heartbeat-states.png create mode 100644 relay/diagrams/heartbeat-states.puml create mode 100644 relay/diagrams/player-session.png create mode 100644 relay/diagrams/player-session.puml create mode 100644 relay/diagrams/registration.png create mode 100644 relay/diagrams/registration.puml create mode 100644 relay/diagrams/tasks.png create mode 100644 relay/diagrams/tasks.puml create mode 100644 relay/etrelay.service create mode 100644 relay/src/main.rs diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..179c49f --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +target +dist + +*.tar.gz +*.json \ No newline at end of file diff --git a/README.md b/README.md index bb7278d..bc265ba 100644 --- a/README.md +++ b/README.md @@ -12,18 +12,18 @@ One to launch a Jaymod server: ## Requirements -Wolfenstein ET is built on a 32 bits arch. So, 32 bits librairies must exist to run the game. +Wolfenstein ET is built on a 32 bits arch. So, 32 bits libraries must exist to run the game. ```bash # add the i386 architecture sudo dpkg --add-architecture i386 -# do update to fetch i386 librairies +# do update to fetch i386 libraries sudo apt update -# install the 32 bits compatible librairies -sudo apt install ia32-libs (mint) -sudo apt install lib32z1 (ubuntu) +# install the 32 bits compatible libraries +sudo apt install ia32-libs # Mint +sudo apt install lib32z1 # Ubuntu # after the installation the following lib must be present sudo apt install libsdl1.2debian:i386 @@ -65,11 +65,11 @@ cd /usr/local/games/enemy-territory Enjoy ! -**NOTE** : Some librairies may be missing, you need to install it one by one (test to launch ./et.x86 and see missing libs). +**NOTE** : Some libraries may be missing, you need to install it one by one (test to launch ./et.x86 and see missing libs). ## Create a systemd service (server creation only) -In order to launch the server properly, a systemd service is a right solution instead of the **serverctl** sh script. Todo so : +In order to launch the server properly, a systemd service is a right solution instead of the **serverctl** sh script. To do so: * put the **[launch_server.bash](./service/launch_server.bash)** wherever you want (ex: /opt/games/enemy-territory/server/linux). * put the **[wolfenstein-et.service](./service/wolfenstein-et.service)** file in **/etc/systemd/system** directory (must be **root**). @@ -87,9 +87,33 @@ sudo service wolfenstein-et start sudo service wolfenstein-et status ``` +## Public server behind a tunnel + +When the server runs at home and is only reachable through a front server (WireGuard), its own heartbeats leave through the home box: masters challenge the wrong IP and the server never shows up in the in-game browser. + +* **[relay](./relay)**: `etrelay`, a small Rust UDP relay running on the front. It proxies players and sends the master heartbeats from the front public address. Build, deploy and usage: [relay/README.md](./relay/README.md), how it works (with diagrams): [relay/ENGINE.md](./relay/ENGINE.md). +* Disable the server's own heartbeats in `server.cfg`, the relay sends them: +```cfg +set sv_master1 "" +set sv_master2 "" +set sv_master3 "" +set sv_master4 "" +set sv_master5 "" +``` + +### List the servers known by the masters + +**[etmasters.py](./etmasters.py)** (Python 3, standard library only) asks the masters (`etmaster.idsoftware.com`, `etmaster.net`) for their server list, then every server for its info: name, map, players, mod. Handy to check the server is listed. + +```bash +./etmasters.py # table sorted by players +./etmasters.py --json servers.json # JSON file instead +./etmasters.py --master etmaster.net:27950 --timeout 5 +``` + ## Settings -More useful informations : https://wiki.archlinux.org/title/Wolfenstein:_Enemy_Territory. +More useful information: https://wiki.archlinux.org/title/Wolfenstein:_Enemy_Territory. ### Set screen size in **1920*1080** * Open the player configuration file: @@ -126,7 +150,7 @@ You must install the corresponding 32 bits library: sudo apt install libasound2-plugins:i386 ``` -### Keyboard inputs does not work +### Keyboard input does not work Try moving to `Xorg` instead of `Wayland`... To do so, on Ubuntu, logout, click on the **Settings** icon and select **Xorg**. diff --git a/etmasters.py b/etmasters.py new file mode 100755 index 0000000..ac98f3c --- /dev/null +++ b/etmasters.py @@ -0,0 +1,161 @@ +#!/usr/bin/env python3 +"""List Wolfenstein: Enemy Territory servers known by the master servers. + +Asks each master for its server list (getservers), then every server for its info +(getinfo): name, map, players, mod. Prints a table, or writes JSON with --json. + + ./etmasters.py + ./etmasters.py --json servers.json + ./etmasters.py --master etmaster.net:27950 +""" +import argparse +import json +import re +import select +import socket +import sys +import time + +OOB = b"\xff\xff\xff\xff" +# 84: ET 2.60b / ET: Legacy protocol +GETSERVERS = OOB + b"getservers 84 empty full" +GETINFO = OOB + b"getinfo etmasters" +DEFAULT_MASTERS = ["etmaster.idsoftware.com:27950", "etmaster.net:27950"] +# ^1red ^7white... color codes in server names +COLOR_CODE = re.compile(r"\^.") + + +def query_master(master, timeout): + """Server addresses listed by one master, None if it doesn't answer.""" + host, port = master.rsplit(":", 1) + try: + ip = socket.gethostbyname(host) + except OSError: + return None + + sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + sock.settimeout(timeout) + sock.sendto(GETSERVERS, (ip, int(port))) + + servers, answered = set(), False + try: + while True: + data, _ = sock.recvfrom(65535) + if b"getserversResponse" not in data: + continue + answered = True + body = data.split(b"getserversResponse", 1)[1] + # \<4 bytes IP><2 bytes port>\...\EOT + for entry in body.split(b"\\")[1:]: + if len(entry) >= 6 and not entry.startswith(b"EOT"): + addr = socket.inet_ntoa(entry[:4]) + servers.add((addr, int.from_bytes(entry[4:6], "big"))) + # Every packet ends with EOT, the list spans several: read until timeout. + except socket.timeout: + pass + finally: + sock.close() + + return servers if answered else None + + +def query_infos(addresses, timeout): + """getinfo to every server at once, {address: info dict} for those answering.""" + sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + sock.setblocking(False) + for addr in addresses: + try: + sock.sendto(GETINFO, addr) + except OSError: + pass + + infos, deadline = {}, time.monotonic() + timeout + while len(infos) < len(addresses): + left = deadline - time.monotonic() + if left <= 0 or not select.select([sock], [], [], left)[0]: + break + try: + data, addr = sock.recvfrom(65535) + except OSError: + continue + if data.startswith(OOB + b"infoResponse"): + infos[addr] = parse_info(data) + + sock.close() + return infos + + +def parse_info(data): + """infoResponse\n\\key\\value\\... -> dict.""" + body = data.split(b"\n", 1)[1].decode("latin-1").strip("\\\n\x00") + parts = body.split("\\") + return dict(zip(parts[::2], parts[1::2])) + + +def server_entry(addr, masters, info): + entry = {"address": f"{addr[0]}:{addr[1]}", "masters": sorted(masters), "online": info is not None} + if info: + entry.update( + name=COLOR_CODE.sub("", info.get("hostname", "")).strip(), + map=info.get("mapname", ""), + players=int(info.get("clients", 0) or 0), + humans=int(info["humans"]) if info.get("humans", "").isdigit() else None, + max_players=int(info.get("sv_maxclients", 0) or 0), + # game: mod folder (nq, etpro, legacy...), gamename is always "et" + mod=info.get("game") or info.get("gamename", ""), + password=info.get("needpass") == "1", + ) + return entry + + +def print_table(entries, masters_status): + for master, count in masters_status.items(): + state = "no answer" if count is None else f"{count} servers" + print(f"{master:32} {state}") + print() + + online = [e for e in entries if e["online"]] + online.sort(key=lambda e: (-e["players"], e["name"].lower())) + + header = f"{'ADDRESS':21} {'PLAYERS':>7} {'MAP':16} {'MOD':10} NAME" + print(header) + print("-" * len(header)) + for e in online: + players = f"{e['players']}/{e['max_players']}" + lock = " [pw]" if e["password"] else "" + print(f"{e['address']:21} {players:>7} {e['map'][:16]:16} {e['mod'][:10]:10} {e['name'][:60]}{lock}") + + total = sum(e["players"] for e in online) + print(f"\n{len(online)} servers online ({len(entries) - len(online)} not answering), {total} players") + + +def main(): + parser = argparse.ArgumentParser(description="List ET servers known by the master servers.") + parser.add_argument("--master", action="append", metavar="HOST:PORT", + help=f"master to query, repeatable (default: {', '.join(DEFAULT_MASTERS)})") + parser.add_argument("--json", metavar="PATH", help="write servers as JSON to PATH instead of printing a table") + parser.add_argument("--timeout", type=float, default=3.0, help="seconds to wait for answers (default: 3)") + args = parser.parse_args() + + masters = args.master or DEFAULT_MASTERS + + masters_status, listed_by = {}, {} + for master in masters: + servers = query_master(master, args.timeout) + masters_status[master] = None if servers is None else len(servers) + for addr in servers or (): + listed_by.setdefault(addr, set()).add(master) + + infos = query_infos(list(listed_by), args.timeout) + entries = [server_entry(addr, ms, infos.get(addr)) for addr, ms in listed_by.items()] + + if args.json: + with open(args.json, "w") as f: + json.dump({"masters": masters_status, "servers": entries}, f, indent=2, ensure_ascii=False) + print(f"{len(entries)} servers written to {args.json}", file=sys.stderr) + else: + print_table(entries, masters_status) + + +if __name__ == "__main__": + main() diff --git a/relay/Cargo.lock b/relay/Cargo.lock new file mode 100644 index 0000000..6ae4ebd --- /dev/null +++ b/relay/Cargo.lock @@ -0,0 +1,258 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "anstream" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000" + +[[package]] +name = "anstyle-parse" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" +dependencies = [ + "windows-sys", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" +dependencies = [ + "anstyle", + "once_cell_polyfill", + "windows-sys", +] + +[[package]] +name = "anyhow" +version = "1.0.104" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470" + +[[package]] +name = "clap" +version = "4.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa8876b300ab35ba921adea3dfd70157a46249b33f95c9084ae5709785478946" +dependencies = [ + "clap_builder", + "clap_derive", +] + +[[package]] +name = "clap_builder" +version = "4.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0797fb7aeb1406c84efac526901f7ec3ead2124f946b494e72879d4b54704d" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim", +] + +[[package]] +name = "clap_derive" +version = "4.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9c751b79415d4e559e3d1fcf128e09e720eb673a06d26cf6f392d37d75b66e0" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "clap_lex" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c133bc6a41be0d194c306b5506d15e6feeea7b1d6604bd3f8310dfb2ca96486" + +[[package]] +name = "colorchoice" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570" + +[[package]] +name = "etrelay" +version = "0.1.0" +dependencies = [ + "anyhow", + "clap", + "tokio", +] + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "is_terminal_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" + +[[package]] +name = "libc" +version = "0.2.189" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2" + +[[package]] +name = "mio" +version = "1.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b18443e9c262bfe8fa82f51666e2642c53393f7e5c27b3e1aeab922cff5b9d8" +dependencies = [ + "libc", + "wasi", + "windows-sys", +] + +[[package]] +name = "once_cell_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" + +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[package]] +name = "proc-macro2" +version = "1.0.107" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "socket2" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3d1e2c7f27f8d4cb10542a02c49005dbd6e93095799d6f3be745fae9f8fedd4" +dependencies = [ + "libc", + "windows-sys", +] + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "syn" +version = "3.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8593e8e72159ed2257d083c7a454a85cbf854f37a0966d8d483aff8c8a3ebcee" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "tokio" +version = "1.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "202caea871b69668250d242070849eb495be178ed697a3e98aebce5bc81a0bed" +dependencies = [ + "libc", + "mio", + "pin-project-lite", + "socket2", + "tokio-macros", + "windows-sys", +] + +[[package]] +name = "tokio-macros" +version = "2.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78773a2a397f451582ce068015985c33193cf6dea8b74d2a639fe457b2f07b0e" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "unicode-ident" +version = "1.0.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d245f478577f809a851594d02313b640fb437e0bb33866753cff937863096954" + +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] diff --git a/relay/Cargo.toml b/relay/Cargo.toml new file mode 100644 index 0000000..9ecd35b --- /dev/null +++ b/relay/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "etrelay" +version = "0.1.0" +edition = "2024" + +[dependencies] +anyhow = "1.0.100" +clap = { version = "4.5.51", features = ["derive"] } +tokio = { version = "1", features = ["macros", "net", "rt", "sync", "time"] } + +[profile.release] +opt-level = 3 +lto = "fat" +codegen-units = 1 +panic = "abort" +strip = true diff --git a/relay/ENGINE.md b/relay/ENGINE.md new file mode 100644 index 0000000..2daea22 --- /dev/null +++ b/relay/ENGINE.md @@ -0,0 +1,96 @@ +# How etrelay works + +> Diagrams: PlantUML sources in [`diagrams/`](diagrams), PNGs regenerated with `make diagrams`. + +## The problem + +A server shows up in the in-game browser only if a **master server** lists it. Registration is a 3-step handshake over UDP: + +1. The game server sends `heartbeat EnemyTerritory-1` to the master. +2. The master does not trust it: it queries the **source IP:port of the heartbeat**. `etmaster.net` sends `getChallenge`, then `getInfo` (classic Quake 3 masters send `getinfo ` directly). +3. The game server answers (`challengeResponse`, `infoResponse`). The master lists the **source IP:port of those answers**. + +Our ET server sits at home, reachable from the Internet only through the front server and the WireGuard tunnel. Players went in through the front (DNAT, then nginx `stream`), but the ET server's own heartbeats went **out through the home box**: the master challenged the home IP, where nothing listens for it. Result: server reachable by IP, invisible in the browser. + +![Before: heartbeat leaves through the home box](diagrams/before.png) + +## The fix + +`etrelay` runs on the front and owns the public UDP port 27960. It does two things on **the same socket**: + +- **Proxy**: forwards every client (players, server browsers, masters) to the ET server, like nginx `stream`. +- **Heartbeat**: sends the heartbeats itself, so the master challenges the **front** address. That challenge is just another client packet: it goes through the proxy, the ET server answers it, the answer leaves from the front address, and the master lists the front. + +The ET server's own heartbeats are disabled (`sv_master1..5 ""`). + +Masters are independent: a server is only listed on the masters it heartbeats, and each client browses its own master (ET 2.60b: `etmaster.idsoftware.com`, ET: Legacy: `etmaster.net`). The relay heartbeats both by default; diagrams show `etmaster.net`, the id Software master works the same way. + +![After: everything goes through the front](diagrams/architecture.png) + +## Registration sequence + +![Master registration (every 5 min)](diagrams/registration.png) + +What it looks like in production (`--debug`): + +``` +heartbeat EnemyTerritory-1 -> etmaster.net:27950 (91.220.53.121:27950) +session + 91.220.53.121:27950 (2 active) +91.220.53.121:27950 -> ET getChallenge (27 B) +91.220.53.121:27950 <- ET challengeResponse (32 B) +91.220.53.121:27950 -> ET getInfo (22 B) +91.220.53.121:27950 <- ET infoResponse (303 B) +session + 138.201.36.184:45488 (3 active) +138.201.36.184:45488 -> ET getstatus (14 B) +138.201.36.184:45488 <- ET statusResponse (993 B) +``` + +The last two lines are a server tracker that got the address from the master. + +## Player sequence + +![Player: one session per client address](diagrams/player-session.png) + +The ET server sees every client as the nginx `stream` host (one source IP, one port per session): IP bans don't work, same as before the relay. + +## Internals + +Single-threaded tokio runtime, three kinds of tasks: + +![Tasks and data flow](diagrams/tasks.png) + +- **relay loop**: reads the public socket, finds the client's session and pushes the packet into its queue. No session, or an expired one: opens a new one (and purges expired entries). A full queue drops the packet, like any congested UDP hop. +- **session task**: owns one ephemeral socket connected to the ET server. Forwards the queue to the ET server and the ET server answers back to the client **through the public socket** (so they come from `:27960`). Ends after `SESSION_TIMEOUT` without a packet either way. +- **heartbeat task**: probes the ET server with `getinfo` on its own socket (up to 3 attempts, 3 s each), then heartbeats the masters through the public socket. Masters are resolved on every heartbeat (DNS may change). + +### Heartbeat states + +![Heartbeat decision (every 5 min)](diagrams/heartbeat-states.png) + +The master drops a server that stops heartbeating after a while anyway; the flatline only makes it faster. + +### Constants + +| Constant | Value | Why | +|---|---|---| +| `HEARTBEAT_INTERVAL` | 5 min | Same period as the ET server itself | +| `PROBE_ATTEMPTS` × `PROBE_TIMEOUT` | 3 × 3 s | One lost UDP packet must not flatline the server | +| `SESSION_TIMEOUT` | 2 min | Idle clients (server browsers) freed quickly; players send packets constantly | +| `SESSION_QUEUE` | 64 packets | Burst absorption per client before dropping | +| `MAX_PACKET` | 65 535 B | Any UDP datagram: `statusResponse` easily exceeds nginx's old 512 B buffer | + +### Protocol cheat sheet + +Connectionless packets start with `\xff\xff\xff\xff`, then a command: + +| Command | From → to | Meaning | +|---|---|---| +| `heartbeat EnemyTerritory-1` | server → master | "I'm alive, come check" | +| `heartbeat ETFlatline-1` | server → master | "I'm going down" | +| `getChallenge` / `challengeResponse` | master ↔ server | Master check before `getInfo` (etmaster.net) | +| `getinfo` / `getInfo` | master/browser → server | Short info request (optional challenge) | +| `infoResponse` | server → master/browser | Hostname, map, player count, challenge | +| `getstatus` / `statusResponse` | browser ↔ server | Full cvars + player list | +| `getchallenge` / `challengeResponse` / `connect` | player ↔ server | Connection handshake | + +Anything else (no prefix) is in-game traffic: forwarded as-is, only counted in `--debug` logs. diff --git a/relay/Makefile b/relay/Makefile new file mode 100644 index 0000000..342fe7c --- /dev/null +++ b/relay/Makefile @@ -0,0 +1,70 @@ +ROOT_DIR := $(dir $(realpath $(lastword $(MAKEFILE_LIST)))) + +NAME := etrelay +VERSION := $(shell grep -m1 '^version' Cargo.toml | cut -d '"' -f2) +GNU := x86_64-unknown-linux-gnu +MUSL := x86_64-unknown-linux-musl + +# Release artifacts: -v- (+ .sha256). +DIST_DIR := $(ROOT_DIR)dist +DYNAMIC_BIN := $(DIST_DIR)/$(NAME)-v$(VERSION)-$(GNU) +STATIC_BIN := $(DIST_DIR)/$(NAME)-v$(VERSION)-$(MUSL) + +# Deployment target (front server) and ET server behind the tunnel. +HOST := +UPSTREAM := +# Extra etrelay flags, e.g. make deploy ARGS=--debug +ARGS ?= + +.PHONY: show lint format check build static release deploy logs diagrams clean + +show: + @echo "$(NAME) version: $(VERSION)" + +lint: + cargo clippy --fix --allow-dirty + +format: + cargo fmt --all + +check: lint format + +# Dynamic (glibc): runs on hosts with the same or newer glibc. +build: check + cargo build --release --target $(GNU) + @$(call dist,$(GNU),$(DYNAMIC_BIN)) + +# Static (musl): runs on any x86_64 Linux. Pure Rust, no musl-gcc needed. +static: check + rustup target add $(MUSL) + cargo build --release --target $(MUSL) + @$(call dist,$(MUSL),$(STATIC_BIN)) + +release: build static + +deploy: static + scp $(STATIC_BIN) $(HOST):/tmp/$(NAME) + sed -e 's#@UPSTREAM@#$(UPSTREAM)#' -e 's#@ARGS@#$(ARGS)#' $(NAME).service | ssh $(HOST) 'cat > /etc/systemd/system/$(NAME).service' + ssh $(HOST) 'install -m 755 /tmp/$(NAME) /usr/local/bin/$(NAME) && rm /tmp/$(NAME) \ + && systemctl daemon-reload && systemctl enable $(NAME) && systemctl restart $(NAME) \ + && systemctl --no-pager status $(NAME)' + +logs: + ssh $(HOST) 'journalctl -u $(NAME) -n 50 --no-pager' + +# PlantUML sources (diagrams/*.puml) -> PNG next to them, used by ENGINE.md. +diagrams: + plantuml -tpng $(ROOT_DIR)diagrams/*.puml + @ls -1 $(ROOT_DIR)diagrams/*.png + +clean: + cargo clean + rm -rf $(DIST_DIR) + +# $(call dist,,): copy the release binary to dist/ + checksum. +define dist +mkdir -p $(DIST_DIR) +cp $(ROOT_DIR)target/$(1)/release/$(NAME) $(2) +cd $(DIST_DIR) && sha256sum $(notdir $(2)) > $(notdir $(2)).sha256 +ls -lh $(2) +endef diff --git a/relay/README.md b/relay/README.md new file mode 100644 index 0000000..29b990a --- /dev/null +++ b/relay/README.md @@ -0,0 +1,77 @@ +# etrelay + +UDP relay + master heartbeat for the Wolfenstein: Enemy Territory server, running on the front server (replaces its UDP 27960 DNAT rule / nginx `stream` block). + +``` +players / masters ──> front :27960 (etrelay) ══wg══> 10.66.0.10:27960 (nginx stream) ──> ET 10.0.1.103: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, `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]... [--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 10.66.0.10:27960: up +ET server 10.66.0.10:27960 is up +heartbeat EnemyTerritory-1 -> etmaster.net:27950 (91.220.53.121:27950) +session + 91.220.53.121:27950 (2 active) +91.220.53.121:27950 -> ET getChallenge (27 B) +91.220.53.121:27950 <- ET challengeResponse (32 B) +91.220.53.121:27950 -> ET getInfo (22 B) +91.220.53.121:27950 <- ET infoResponse (303 B) +session - :27960: 5321 packets to ET, 4876 from ET +``` + +Deploy with it: `make deploy ARGS=--debug`, back to normal with `make deploy`. + +## 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 +``` + +## Deploy + +1. Relay (static binary + systemd unit on `ionos`): `make deploy` +2. Front: remove what captured UDP 27960 before etrelay, or players bypass it: + - the DNAT rule (`PostUp`/`PostDown` in `/etc/wireguard/wg0.conf`, then `wg-quick down wg0 && wg-quick up wg0`); `iptables -t nat -S | grep 27960` must print nothing + - the nginx `stream` block (`proxy/nginx/nginx.conf` in the `pve` repo): `make -C proxy deploy-nginx` from there +3. ET server `server.cfg`: stop its own heartbeats, they leave through the home box with the wrong IP: + ``` + set sv_master1 "" + set sv_master2 "" + set sv_master3 "" + set sv_master4 "" + set sv_master5 "" + ``` + +## Check + +```bash +make logs # "ET server ... is up" +ssh ionos 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). diff --git a/relay/diagrams/architecture.png b/relay/diagrams/architecture.png new file mode 100644 index 0000000000000000000000000000000000000000..d20f5ed07025ef45cf8e398e01dc49f096c6bbe8 GIT binary patch literal 24817 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

?0l;pgLc^;&?e_pTEkQ2V6rgk%6)dW>c+3#y>M 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 literal 0 HcmV?d00001 diff --git a/relay/diagrams/architecture.puml b/relay/diagrams/architecture.puml new file mode 100644 index 0000000..73ec89e --- /dev/null +++ b/relay/diagrams/architecture.puml @@ -0,0 +1,21 @@ +@startuml +skinparam shadowing false +title After: everything goes through the front + +actor Player as player +node "Master\netmaster.net:27950" as master + +node "Front (74.208.250.147)" { + component "etrelay\n0.0.0.0:27960" as relay +} + +node "Home" { + component "nginx stream\n10.66.0.10:27960" as nginx + component "ET server\n10.0.1.103:27960" as et +} + +player <--> relay : game traffic +master <--> relay : heartbeat / getChallenge / getInfo +relay <--> nginx : WireGuard (wg0) +nginx <--> et : LAN +@enduml diff --git a/relay/diagrams/before.png b/relay/diagrams/before.png new file mode 100644 index 0000000000000000000000000000000000000000..63b652e89bb64ce826bb891477ba813493239dc5 GIT binary patch literal 22367 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 Tj{yz17jfi}&B4O`XQKZJeWw;6 literal 0 HcmV?d00001 diff --git a/relay/diagrams/before.puml b/relay/diagrams/before.puml new file mode 100644 index 0000000..22b73f0 --- /dev/null +++ b/relay/diagrams/before.puml @@ -0,0 +1,14 @@ +@startuml +skinparam shadowing false +title Before: heartbeat leaves through the home box + +node "Master\netmaster.net:27950" as master +node "Home box\n86.x.x.x" as box +node "Front\n74.208.250.147" as front +node "ET server\n10.0.1.103:27960" as et + +et --> box : heartbeat +box --> master : heartbeat (source = home IP) +master --> box : getChallenge\n(to home IP: dropped) +front ..> et : players (DNAT + wg + nginx stream) +@enduml diff --git a/relay/diagrams/heartbeat-states.png b/relay/diagrams/heartbeat-states.png new file mode 100644 index 0000000000000000000000000000000000000000..fb1decda9f38c8e38258de11ad174228703f9dd4 GIT binary patch literal 17418 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 Y;UG`MNi6Q*|M-BCkx&q?5Hs}uU)2 Down +Down --> Up : probe answers\n/ heartbeat EnemyTerritory-1 +Up --> Up : probe answers\n/ heartbeat EnemyTerritory-1 +Up --> Down : no answer\n/ heartbeat ETFlatline-1 (once) +Down --> Down : no answer\n/ nothing sent +@enduml diff --git a/relay/diagrams/player-session.png b/relay/diagrams/player-session.png new file mode 100644 index 0000000000000000000000000000000000000000..f07aaf9f0af32135c3b1fd322613a5ce68a286bf GIT binary patch literal 35742 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 F{{?%%ZSMd8 literal 0 HcmV?d00001 diff --git a/relay/diagrams/player-session.puml b/relay/diagrams/player-session.puml new file mode 100644 index 0000000..742e4b0 --- /dev/null +++ b/relay/diagrams/player-session.puml @@ -0,0 +1,19 @@ +@startuml +skinparam shadowing false +title Player: one session per client address + +actor "Player\n1.2.3.4:5555" as player +participant "etrelay\nfront :27960" as relay +participant "session task\n(ephemeral socket)" as session +participant "ET server" as et + +player -> relay : getchallenge / connect / game packets +relay -> session : first packet: open session +session -> et : forward (source = ephemeral port) +et --> session : reply +session --> player : reply sent from front:27960 +...packets both ways, deadline pushed back on each packet... +...2 min without any packet... +session -> session : close (socket dropped) +note over relay : next packet from 1.2.3.4:5555\nopens a new session +@enduml diff --git a/relay/diagrams/registration.png b/relay/diagrams/registration.png new file mode 100644 index 0000000000000000000000000000000000000000..aecf273b4a903f0175f7c0d5ee528d7fae9ba84b GIT binary patch literal 49556 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

}_rv|j^UUVA*V-%gv-ev6zoK_QcISTAU{Tj{ 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 et : getinfo etrelay +et --> relay : infoResponse +note right of relay : up: send heartbeat\nno answer x3: skip (or flatline once) + +== Heartbeat (public socket) == +relay -> master : heartbeat EnemyTerritory-1\nsource = front:27960 + +== Master checks: handled like any client == +master -> relay : getChallenge\nto front:27960 +relay -> relay : new session for master:27950 +relay -> et : getChallenge +et --> relay : challengeResponse +relay --> master : challengeResponse\nsource = front:27960 +master -> relay : getInfo +relay -> et : getInfo +et --> relay : infoResponse (hostname, map, players) +relay --> master : infoResponse\nsource = front:27960 +note left of master : lists 74.208.250.147:27960 +@enduml diff --git a/relay/diagrams/tasks.png b/relay/diagrams/tasks.png new file mode 100644 index 0000000000000000000000000000000000000000..1fc16ec2fc1be53f0c3273ea3c87d6df02582108 GIT binary patch literal 35579 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 literal 0 HcmV?d00001 diff --git a/relay/diagrams/tasks.puml b/relay/diagrams/tasks.puml new file mode 100644 index 0000000..a7cea9e --- /dev/null +++ b/relay/diagrams/tasks.puml @@ -0,0 +1,26 @@ +@startuml +skinparam shadowing false +title Tasks and data flow + +rectangle "relay loop (main task)" as loop { + card "public socket\nrecv_from" as recv + card "sessions:\nHashMap" as map +} + +rectangle "session task (one per client)" as st { + card "mpsc Receiver\n(queue: 64)" as rx + card "upstream socket\n(connected to ET)" as up +} + +rectangle "heartbeat task" as hb { + card "interval 5 min" as tick + card "probe socket" as probe +} + +recv --> map : lookup client +map --> rx : try_send(packet)\n(full: drop) +rx --> up : send +up --> recv : recv, then public.send_to(client) +tick --> probe : getinfo +tick --> recv : heartbeat via public socket +@enduml diff --git a/relay/etrelay.service b/relay/etrelay.service new file mode 100644 index 0000000..b2ad1a8 --- /dev/null +++ b/relay/etrelay.service @@ -0,0 +1,19 @@ +[Unit] +Description=Wolfenstein: Enemy Territory UDP relay + master heartbeat +After=network-online.target +Wants=network-online.target + +[Service] +ExecStart=/usr/local/bin/etrelay @UPSTREAM@ @ARGS@ +Restart=always +RestartSec=2 + +# Port 27960 > 1024: no privilege needed. +DynamicUser=yes +NoNewPrivileges=yes +ProtectSystem=strict +ProtectHome=yes +PrivateTmp=yes + +[Install] +WantedBy=multi-user.target diff --git a/relay/src/main.rs b/relay/src/main.rs new file mode 100644 index 0000000..919e140 --- /dev/null +++ b/relay/src/main.rs @@ -0,0 +1,253 @@ +//! UDP relay for a Wolfenstein: Enemy Territory server reachable only through a tunnel. +//! +//! - Proxies every client (players, server browsers, masters) to the ET server, one +//! upstream socket per client address, like nginx `stream`. +//! - Sends master heartbeats from the public socket, so masters challenge (`getinfo`) the +//! public address: the challenge goes through the relay like any client packet, the ET +//! server answers it, and the master lists the public address. +use anyhow::{Context, Result}; +use clap::Parser; +use std::collections::HashMap; +use std::net::SocketAddr; +use std::sync::Arc; +use std::sync::atomic::{AtomicBool, Ordering}; +use std::time::Duration; +use tokio::net::{UdpSocket, lookup_host}; +use tokio::sync::mpsc::{self, error::TrySendError}; +use tokio::time::{Instant, interval, sleep_until, timeout}; + +/// Connectionless packet prefix (Quake 3 / ET protocol). +const OOB: &[u8] = b"\xff\xff\xff\xff"; +/// Same heartbeat period as the ET server itself. +const HEARTBEAT_INTERVAL: Duration = Duration::from_secs(300); +const HEARTBEAT_ALIVE: &str = "EnemyTerritory-1"; +const HEARTBEAT_DEAD: &str = "ETFlatline-1"; +const PROBE_TIMEOUT: Duration = Duration::from_secs(3); +const PROBE_ATTEMPTS: usize = 3; +const SESSION_TIMEOUT: Duration = Duration::from_secs(120); +const SESSION_QUEUE: usize = 64; +const MAX_PACKET: usize = 65_535; +/// Longest connectionless command name shown in debug logs. +const MAX_COMMAND: usize = 32; + +static DEBUG: AtomicBool = AtomicBool::new(false); + +/// println! only with --debug. +macro_rules! debug { + ($($arg:tt)*) => { + if DEBUG.load(Ordering::Relaxed) { + println!($($arg)*); + } + }; +} + +/// UDP relay + master heartbeat for a Wolfenstein: Enemy Territory server. +#[derive(Parser)] +#[command(version, name = "etrelay")] +struct Cli { + /// ET server address, e.g. 10.66.0.10:27960 + upstream: SocketAddr, + + /// Public address to listen on + #[arg(long, default_value = "0.0.0.0:27960")] + listen: SocketAddr, + + /// Master server host:port (repeatable) + #[arg( + long = "master", + default_values = ["etmaster.idsoftware.com:27950", "etmaster.net:27950"] + )] + masters: Vec, + + /// Log sessions, connectionless packets (getinfo, getstatus, connect...), probes and heartbeats + #[arg(long)] + debug: bool, +} + +#[tokio::main(flavor = "current_thread")] +async fn main() -> Result<()> { + let cli = Cli::parse(); + DEBUG.store(cli.debug, Ordering::Relaxed); + + let public = UdpSocket::bind(cli.listen) + .await + .with_context(|| format!("bind {}", cli.listen))?; + let public = Arc::new(public); + + println!( + "etrelay: {} -> {}, masters: {}", + cli.listen, + cli.upstream, + cli.masters.join(", ") + ); + + tokio::spawn(heartbeat(public.clone(), cli.upstream, cli.masters)); + relay(public, cli.upstream).await +} + +/// Dispatches client packets to their session, opening one on first packet. +async fn relay(public: Arc, upstream: SocketAddr) -> Result<()> { + let mut sessions: HashMap>> = HashMap::new(); + let mut buf = vec![0u8; MAX_PACKET]; + + loop { + let (n, client) = public.recv_from(&mut buf).await.context("recv")?; + let mut packet = buf[..n].to_vec(); + + if let Some(tx) = sessions.get(&client) { + match tx.try_send(packet) { + // Full: drop it, like any congested UDP hop. + Ok(()) | Err(TrySendError::Full(_)) => continue, + // Session expired: open a new one below. + Err(TrySendError::Closed(p)) => packet = p, + } + } + + sessions.retain(|_, tx| !tx.is_closed()); + + match open_session(public.clone(), upstream, client).await { + Ok(tx) => { + let _ = tx.try_send(packet); + sessions.insert(client, tx); + debug!("session + {client} ({} active)", sessions.len()); + } + Err(e) => eprintln!("session {client}: {e:#}"), + } + } +} + +/// Forwards packets between one client and the ET server until SESSION_TIMEOUT of silence. +async fn open_session( + public: Arc, + upstream: SocketAddr, + client: SocketAddr, +) -> Result>> { + let sock = connect(upstream).await?; + let (tx, mut rx) = mpsc::channel::>(SESSION_QUEUE); + + tokio::spawn(async move { + let mut buf = vec![0u8; MAX_PACKET]; + let mut deadline = Instant::now() + SESSION_TIMEOUT; + let (mut sent, mut received) = (0u64, 0u64); + + loop { + tokio::select! { + Some(packet) = rx.recv() => { + if let Some(command) = command(&packet) { + debug!("{client} -> ET {command} ({} B)", packet.len()); + } + if let Err(e) = sock.send(&packet).await { + debug!("{client} -> ET send failed: {e}"); + } + sent += 1; + deadline = Instant::now() + SESSION_TIMEOUT; + } + res = sock.recv(&mut buf) => match res { + Ok(n) => { + if let Some(command) = command(&buf[..n]) { + debug!("{client} <- ET {command} ({n} B)"); + } + let _ = public.send_to(&buf[..n], client).await; + received += 1; + deadline = Instant::now() + SESSION_TIMEOUT; + } + // ICMP unreachable while the ET server is down: keep waiting. + Err(e) => debug!("{client} <- ET recv failed: {e}"), + }, + _ = sleep_until(deadline) => break, + } + } + + debug!("session - {client}: {sent} packets to ET, {received} from ET"); + }); + + Ok(tx) +} + +/// Heartbeats masters while the ET server answers, sends one flatline when it stops. +async fn heartbeat(public: Arc, upstream: SocketAddr, masters: Vec) { + let mut ticker = interval(HEARTBEAT_INTERVAL); + let mut alive = false; + + loop { + ticker.tick().await; + + let up = probe(upstream).await; + debug!("probe {upstream}: {}", if up { "up" } else { "no answer" }); + if up != alive { + println!("ET server {upstream} is {}", if up { "up" } else { "down" }); + } + + let message = match (up, alive) { + (true, _) => HEARTBEAT_ALIVE, + (false, true) => HEARTBEAT_DEAD, + (false, false) => continue, + }; + alive = up; + + let packet = [OOB, format!("heartbeat {message}\n").as_bytes()].concat(); + for master in &masters { + match send_to_host(&public, &packet, master).await { + Ok(addr) => debug!("heartbeat {message} -> {master} ({addr})"), + Err(e) => eprintln!("heartbeat {master}: {e:#}"), + } + } + } +} + +/// True if the ET server answers `getinfo`. +async fn probe(upstream: SocketAddr) -> bool { + for _ in 0..PROBE_ATTEMPTS { + let attempt = async { + let sock = connect(upstream).await?; + sock.send(&[OOB, b"getinfo etrelay"].concat()).await?; + + let mut buf = vec![0u8; MAX_PACKET]; + let n = sock.recv(&mut buf).await?; + Ok::<_, anyhow::Error>(buf[..n].starts_with(&[OOB, b"infoResponse"].concat())) + }; + + if let Ok(Ok(true)) = timeout(PROBE_TIMEOUT, attempt).await { + return true; + } + } + false +} + +/// Resolves `host` (DNS may change between heartbeats) and sends from `sock`. +async fn send_to_host(sock: &UdpSocket, packet: &[u8], host: &str) -> Result { + let ipv4 = sock.local_addr()?.is_ipv4(); + let addr = lookup_host(host) + .await? + .find(|a| a.is_ipv4() == ipv4) + .context("no address in the listen socket family")?; + + sock.send_to(packet, addr).await?; + Ok(addr) +} + +/// Command name of a connectionless packet (`getinfo`, `infoResponse`, `connect`...), +/// None for in-game packets. Arguments are left out: `connect` carries the userinfo. +fn command(packet: &[u8]) -> Option { + let body = packet.strip_prefix(OOB)?; + let end = body + .iter() + .position(|b| b.is_ascii_whitespace() || *b == b'\\') + .unwrap_or(body.len()) + .min(MAX_COMMAND); + + Some(String::from_utf8_lossy(&body[..end]).into_owned()) +} + +/// Ephemeral socket connected to `upstream`. +async fn connect(upstream: SocketAddr) -> Result { + let any: SocketAddr = if upstream.is_ipv4() { + "0.0.0.0:0".parse()? + } else { + "[::]:0".parse()? + }; + + let sock = UdpSocket::bind(any).await?; + sock.connect(upstream).await?; + Ok(sock) +}