Skip to content

SKevo18/minecraft_proxy

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

6 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

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, 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:

  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:

    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)

[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:

proxies:
  proxy-protocol: true

server.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:

GET http://127.0.0.1:<auth_listen>/source?username=<name>
→ {"source":"minehut"}   |   {"source":"mojang"}   |   {"source":null}

A ready-made PlaceholderAPI expansion that consumes this lives in 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).

About

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

Topics

Resources

Stars

Watchers

Forks

Contributors