diff --git a/config/patches/patch-set.json b/config/patches/patch-set.json new file mode 100644 index 0000000..6de1f0e --- /dev/null +++ b/config/patches/patch-set.json @@ -0,0 +1,42 @@ +{ + "patches": [ + { + "file": "/data/config/proxy_protocol_support.json", + "file-format": "json", + "ops": [ + { + "$set": { + "path": "$.enableProxyProtocol", + "value": true, + "value-type": "bool" + } + }, + { + "$set": { + "path": "$.whitelistTCPShieldServers", + "value": true, + "value-type": "bool" + } + }, + { + "$set": { + "path": "$.proxyServerIPs", + "value": ["127.0.0.1"] + } + }, + { + "$set": { + "path": "$.directAccessIPs", + "value": [ + "127.0.0.1", + "::1", + "10.0.0.0/8", + "172.16.0.0/12", + "192.168.0.0/16" + ] + } + } + ] + } + ] +} diff --git a/dashboard/src/app/(panel)/mods/page.tsx b/dashboard/src/app/(panel)/mods/page.tsx index 4b7982b..6f0bac0 100644 --- a/dashboard/src/app/(panel)/mods/page.tsx +++ b/dashboard/src/app/(panel)/mods/page.tsx @@ -59,6 +59,13 @@ const FEATURED = [ description: "Pre-generates the world around spawn so nobody lags exploring new chunks. Run `chunky radius 2000` then `chunky start` in Console.", }, + { + slug: "proxy-protocol-support", + emoji: "🛡", + title: "Proxy Protocol Support", + description: + "Using TCPShield (or another proxy)? This restores players' real IPs so the Security page and IP bans work. Config is auto-enforced — see docs/tcpshield.md.", + }, ]; export default function ModsPage() { diff --git a/docker-compose.yml b/docker-compose.yml index edaa69f..4e59f67 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -45,6 +45,11 @@ services: # Modpack support (leave empty for lean Fabric; see docs/mods-and-modpacks.md) MODRINTH_MODPACK: ${MODRINTH_MODPACK:-} + # Enforce mod configs on every boot (proxy-protocol/TCPShield settings — + # see docs/tcpshield.md). Mods that rewrite their own config files can't + # clobber these: patches re-apply before each server start. + PATCH_DEFINITIONS: /patches/patch-set.json + TZ: ${TZ:-UTC} ports: - "${MC_PORT:-25565}:25565" @@ -52,6 +57,7 @@ services: - "${MAP_PORT:-8100}:8100" volumes: - ./data/mc:/data + - ./config/patches:/patches:ro mem_limit: ${MC_CONTAINER_MEM_LIMIT:-7g} # ── Scheduled world backups (itzg/mc-backup) ───────────────────────────── diff --git a/docs/tcpshield.md b/docs/tcpshield.md new file mode 100644 index 0000000..baf154c --- /dev/null +++ b/docs/tcpshield.md @@ -0,0 +1,84 @@ +# TCPShield (DDoS protection + real player IPs) + +[TCPShield](https://tcpshield.com) sits in front of the server: players connect to +your TCPShield domain, and your real server IP stays hidden and DDoS-protected. +The catch: without extra setup, every connection appears to come from TCPShield's +proxies, so the Security page, IP bans, and connection logs are useless. + +TCPShield's own RealIP plugin is Bukkit/Bungee/Velocity-only — it does not exist +for Fabric. The working path for this stack is **PROXY protocol v2** plus the +**Proxy Protocol Support** Fabric mod. + +## Setup + +1. **TCPShield panel** → your backend → enable **Proxy Protocol**. +2. **Mods page** → Recommended → add **Proxy Protocol Support** → Apply + (or append `proxy-protocol-support` to `MODRINTH_PROJECTS` in `.env` and + `docker compose up -d mc`). +3. Nothing else — the mod's configuration is **enforced automatically on every + boot** by `config/patches/patch-set.json` (see below). Restart once after + installing so the patch applies: + + ```bash + docker compose restart mc + ``` + +## Verifying it works + +Watch the startup log (`docker compose logs -f mc`). You must see all three: + +``` +Proxy Protocol Support is enabled! +TCPShield integration enabled! Fetching official IPs... +Successfully added 2 TCPShield IPs to the trusted proxy list. +``` + +Then join through your TCPShield domain — the console shows your **real IP**, and +`docker compose ps` shows `mc` **healthy**. + +## How the config is managed (don't hand-edit!) + +The mod rewrites `config/proxy_protocol_support.json` at every startup, which +silently clobbers hand edits. Instead, the server image applies +`config/patches/patch-set.json` from this repo **before every server start**, +enforcing: + +| Setting | Value | Why | +|---|---|---| +| `enableProxyProtocol` | `true` | decode PROXY protocol headers | +| `whitelistTCPShieldServers` | `true` | auto-fetch TCPShield's proxy IP ranges — no hardcoding, survives their infra changes | +| `proxyServerIPs` | `127.0.0.1` | (TCPShield's ranges are added automatically at runtime) | +| `directAccessIPs` | localhost (`127.0.0.1`, `::1`) + private ranges (`10/8`, `172.16/12`, `192.168/16`) | container healthcheck + LAN/panel access | + +To change a value, edit the patch file in the repo and `docker compose up -d mc`. + +**First-boot note:** on a brand-new install the mod's config file doesn't exist +yet when patches run, so the very first boot uses mod defaults; every boot after +that is patched. One `docker compose restart mc` after first install settles it. + +## Security model + +PROXY protocol headers carry no signature, so anyone who finds your raw server IP +could spoof client IPs — the mod prevents this with default-deny: + +- PROXY headers are only accepted from **TCPShield's published ranges** (auto-fetched) +- Direct, header-less connections are only accepted from **localhost + LAN** +- Everything else → `REJECTED unauthorized direct connection` in the log + +So occasional `REJECTED` lines from random public IPs are the wall working — +that's scanner bots hitting your raw IP and bouncing. A `REJECTED ... /0:0:0:0:0:0:0:1` +(IPv6 localhost) every few seconds means the container healthcheck is being +blocked — that's exactly what the `::1` entry in the patch prevents; if you see +it, the patch didn't apply (check `docker compose logs mc | head -50`). + +For belt-and-braces, also enable TCPShield's Advanced Firewall (panel → Network → +Firewall), and consider host-firewalling port 25565 to TCPShield's ranges +(`curl tcpshield.com/v4`) once everything works. + +## Gotchas + +- **Friends must connect via the TCPShield domain** — direct-IP connections from + outside your LAN are rejected by design once this is active. +- If real players start bouncing with `REJECTED`, TCPShield may have changed + their IP ranges *and* the auto-fetch failed at boot — restart the server so it + re-fetches.