https://github.com/skevo18/minecraft_proxy
A lightweight, dependency-free Java wrapper that lets one Minecraft server accept players from both direct connections (your own domain/IP) and some other proxy (Minehut etc.) at the same time. It is a drop-in replacement for your server jar: it launches the real server on a backend port and fronts it with a dual proxy
https://github.com/skevo18/minecraft_proxy
dual-proxy minecraft minehut mitm-proxy proxy
Last synced: 13 days ago
JSON representation
A lightweight, dependency-free Java wrapper that lets one Minecraft server accept players from both direct connections (your own domain/IP) and some other proxy (Minehut etc.) at the same time. It is a drop-in replacement for your server jar: it launches the real server on a backend port and fronts it with a dual proxy
- Host: GitHub
- URL: https://github.com/skevo18/minecraft_proxy
- Owner: SKevo18
- Created: 2026-05-30T11:58:54.000Z (about 1 month ago)
- Default Branch: main
- Last Pushed: 2026-05-30T12:03:47.000Z (about 1 month ago)
- Last Synced: 2026-05-30T14:04:58.817Z (about 1 month ago)
- Topics: dual-proxy, minecraft, minehut, mitm-proxy, proxy
- Language: Java
- Homepage:
- Size: 23.4 KB
- Stars: 0
- Watchers: 0
- Forks: 0
- Open Issues: 0
-
Metadata Files:
- Readme: README.md
Awesome Lists containing this project
README
# Minehut Dual-Proxy Wrapper
A lightweight, dependency-free Java wrapper that lets one Minecraft server accept
players from **both** direct connections (your own domain/IP) and the Minehut
proxy at the same time. It is a drop-in replacement for your server jar: instead
of running the server directly, it launches the real server on a backend port and
fronts it with a dual proxy.
It is a minimal Java port of [mc-dual-proxy](SKevo18/mc-dual-proxy), built on plain
`java.base`-only APIs (blocking sockets on virtual threads, `HttpURLConnection`)
so it stays small, fast, and runs even on trimmed JREs that omit extra modules.
> This is the proxy running in production on the **steedsgate.quest** MC server.
## The problem
Connecting an external server to Minehut forces two incompatible choices:
1. **PROXY protocol** — Minehut prepends HAProxy PROXY protocol headers; direct
players don't. The backend can't have it both on and off.
2. **Session server** — Minehut authenticates `hasJoined` against its own MITM
session server; direct players use Mojang's. The backend can only point at one.
## The solution
The wrapper runs three things in one process:
```plaintext
players ───────────────→ [tcp proxy :25565] ──→ [real server :25566]
(direct + minehut) normalizes proxy-protocol: true
PROXY protocol session.host → :8652
│
real server hasJoined ──→ [multiauth :8652] ──→ Mojang + Minehut (first 200 wins)
```
1. **TCP proxy** (`listen`): direct connections get a generated PROXY protocol v2
header; Minehut connections have theirs forwarded verbatim. The backend always
sees a header, so it can keep `proxy-protocol: true` for everyone.
2. **Backend server** (`backend`): launched as a child process with the Mojang API
host overrides injected, and `session.host` pointed at the local multiauth port.
3. **Multiauth HTTP server** (`auth_listen`): fans each `hasJoined` out to Mojang
and Minehut concurrently and returns the first HTTP 200. The `serverId` hash is
cryptographically unique per connection, so exactly one upstream ever matches.
## Setup
1. Build: `./build.sh` → produces `proxy.jar`.
2. Put `proxy.jar`, your real server jar, and `startup.ini` in the server folder.
3. Rename your server jar to `real-server.jar` (or set `[server] jar` in `startup.ini`).
4. Configure the backend for the proxy (see below).
5. Run it:
```bash
java -jar proxy.jar --nogui
```
The wrapper caps its own heap at `[proxy] heap` (128M) automatically, so the
memory in `startup.ini` goes to the backend — no `-Xmx` needed on the launch
command (pass one anyway and it's respected as-is).
Any arguments after the jar are passed through to the real server. `--port` is
injected automatically (from `[proxy] backend`) unless you pass your own.
## Configuration (`startup.ini`)
```ini
[proxy]
heap = 128M ; cap the wrapper's OWN heap (re-exec); "off" disables
listen = 0.0.0.0:25565 ; public address players connect to
backend = 127.0.0.1:25566 ; the real server this wrapper launches
auth_listen = 127.0.0.1:8652 ; local multiauth server (localhost only)
session_servers = https://sessionserver.mojang.com,https://api.minehut.com/mitm/proxy
[api]
enabled = true ; serve GET /source (Minehut vs Mojang) on the auth server
[web]
enabled = true ; landing page + live status
name = SteedsGate
address = steedsgate.quest ; shown on the page / Copy IP button
listen = 0.0.0.0:8080
[server]
jar = real-server.jar
min_memory = 8G ; backend heap (applied as -Xms/-Xmx)
max_memory = 8G
[restart]
autorestart = true ; restart the backend when it crashes
restart_on_stop = false ; /stop stops the wrapper; true = always-up (only a signal stops it)
restart_delay = 5 ; seconds to wait before each restart
restart_max = 5 ; max restarts per window, 0 = unlimited
restart_window = 60 ; window the limit applies to
[startup_flags]
; backend JVM flags, one per line. The Mojang API host overrides go here —
; session.host must match auth_listen so the backend authenticates via multiauth.
-Dminecraft.api.auth.host=https://authserver.mojang.com/
-Dminecraft.api.account.host=https://api.mojang.com/
-Dminecraft.api.services.host=https://api.minecraftservices.com/
-Dminecraft.api.profiles.host=https://api.mojang.com/
-Dminecraft.api.session.host=http://127.0.0.1:8652
;-XX:+UseG1GC ; plus any GC/perf flags
```
Every value is optional; omitted keys fall back to the defaults above. The
wrapper's own heap is capped at `heap` (128M) via a one-time self re-exec, unless
you pass an explicit `-Xmx` on the launch command (which is respected as-is).
### Backend supervision (`[restart]`)
When `autorestart` is on (the default), the wrapper relaunches the backend after
it **crashes** (non-zero exit), waiting `restart_delay` seconds each time. A clean
`/stop` (exit 0) instead exits the wrapper, so the panel's Stop button — which
usually just sends `/stop` to the console — actually stops the server; the panel's
Restart (stop→start) works too. Set `restart_on_stop = true` to relaunch even on a
clean stop (always-up; then only a kill signal stops it).
If the backend restarts more than `restart_max` times within `restart_window`
seconds the wrapper gives up and exits — this stops a crash-looping server from
hammering the box (`restart_max = 0` for unlimited). A signal to the wrapper
itself (Ctrl-C / SIGTERM) always shuts everything down without a restart.
## Backend configuration
These are server-side settings the wrapper can't set via flags:
`config/paper-global.yml`:
```yaml
proxies:
proxy-protocol: true
```
`server.properties`:
```properties
enforce-secure-profile=false
```
The Mojang API host overrides live in `[startup_flags]` (Paper ignores them unless
**all** of `auth.host`, `account.host`, `services.host`, `profiles.host` are set).
Keep those four pointed at Mojang and point `session.host` at the local multiauth
server — i.e. it must match `auth_listen` (default `http://127.0.0.1:8652`).
## Minehut panel
1. Point your external server at your **public IP** on the `listen` port (25565).
2. DNS record type: `Port`. TCP Shield: `Not Configured`.
3. Proxy type: `Other` for standalone Paper.
## Firewall
Only the `listen` port (25565) must be open externally. The `backend` (25566) and
`auth_listen` (8652) ports only need to be reachable from localhost.
## Restart screen
While the backend is down (restarting or still booting), the proxy:
- answers **server-list pings** with a custom MOTD (`starting_motd`, with `&`
colors and `\n` for a second line — hand-pad it with spaces to taste), and
- **holds joining players** at "Connecting…" — it keeps retrying the backend for
`connect_wait` seconds and splices them straight through once it's up, instead
of kicking them.
The protocol is only parsed on this slow path; a healthy backend is spliced raw.
## Landing page + status (`[web]`)
With `[web] enabled = true`, the proxy serves a small **landing page** for the
server (hero, description, screenshot gallery) with **live status** woven in —
players online, uptime, system memory, and service health.
- The template is `web/index.html` with simple `{{placeholder}}` substitution
(`name`, `address`, `status`, `players_online/max`, `version`, `uptime`,
`mem_used/total/percent`, `*_status`). Edit it freely.
- Images and other static files live in `web/assets/` and are served from
`/assets/...`. Swap in your own.
- `GET /status.json` returns the live data; the page polls it every 5s.
- Player counts come from a status ping to the backend; memory from `/proc/meminfo`.
Deploy the whole `web/` folder next to `proxy.jar`. Set `[web] name` and
`address` for your branding.
## Join source (Minehut vs Mojang)
The multiauth server knows how each player authenticated (Minehut players hit
Minehut's session server, direct players hit Mojang's) and records it. With
`[api] enabled = true` it's exposed at:
```text
GET http://127.0.0.1:/source?username=
→ {"source":"minehut"} | {"source":"mojang"} | {"source":null}
```
A ready-made PlaceholderAPI expansion that consumes this lives in
[`papi-expansion/`](papi-expansion/) and provides `%proxy_source%`,
`%proxy_source_badge%` (`[MH]`/``), and `%proxy_source_raw%`.
## Notes on performance
The dual proxy adds a per-connection byte pump, so very long uptimes on small
boxes can raise ping. To keep it lean:
- The wrapper caps its own heap (`[proxy] heap`, default 128M) so it can't grow
toward the JVM's ~25%-of-RAM default; the backend gets the real memory.
- `TCP_NODELAY` is set on both sides to avoid Nagle latency.
- Connections and pipe directions run on virtual threads (low per-connection cost).