An open API service indexing awesome lists of open source software.

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

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).