# mchost Run a Minecraft server jar and expose it through a **Cloudflare Tunnel** — no port forwarding, no public IP. Two tunnel modes: | Mode | Flag | Address players see | Cloudflare account needed? | |------|------|---------------------|----------------------------| | Quick tunnel | *(default)* | a random `something-words.trycloudflare.com` name, new on every run | no | | Named tunnel | `--token ` | the domain you bought and pointed at Cloudflare | yes (token from the Zero Trust dashboard) | ## How players join Cloudflare's tunnel edge only speaks HTTP(S), not raw Minecraft TCP, so mchost runs a small **WebSocket bridge** and points the tunnel at it. Players connect through the tunnel with either: - the **Fabric client mod** (in-game: Multiplayer → *Tunnel...* → paste the invite), or - the **CLI helper** `mchost connect ` — same thing without the mod. Both accept one shareable invite string that mchost prints (and writes to `mchost-address.txt` for the server mod to show in-game): ``` ┌──────────────┐ vanilla MC ┌────────────────┐ WebSocket over TLS ┌────────┐ ws ┌────────┐ │ MC client │─────────────▶│ mod / helper │─────────────────────▶│tunnel │─────▶│ bridge │──▶127.0.0.1:25565 │127.0.0.1:25565│ │ (player runs) │ wss:// │ edge │ │(server)│ └──────────────┘ └────────────────┘ └────────┘ └────────┘ ``` They paste one line and play in their normal vanilla client — no port forwarding, no public IP. ## Build ```sh cargo build --release # binary: target/release/mchost ``` Requires: Rust to build, Java 21+ to run a modern server jar, and the `cloudflared` binary on your `PATH` (or pass `--cloudflared `). ## Quick start (with the bundled Fabric server) If you've installed the Fabric server into `server/` (see [mod/README.md](mod/README.md)): ```sh ./start.sh # builds mchost if needed, starts server/ behind a # quick tunnel with a random invite secret SECRET=hunter2 ./start.sh # stable invite secret TOKEN=eyJh... PUBLIC_HOST=mc.example.com ./start.sh # your own domain ./start.sh --no-tunnel # local only ``` The first run writes `eula=true` to `server/eula.txt` (accepting [Minecraft's EULA](https://aka.ms/MinecraftEULA) on your behalf — only run it if you're fine with that). `DRY_RUN=1 ./start.sh` prints the command instead. ## Usage ```sh # Quick tunnel: random trycloudflare.com name, no account mchost server.jar # Accept the Minecraft EULA for you and give the JVM 4G mchost -m 4G --eula server.jar # Named tunnel: served from your purchased domain (see setup below) mchost server.jar --token eyJhIjoiXXXXXXXX... # Local only mchost --no-tunnel server.jar # Extra arguments for the server jar go after -- mchost paper.jar -- -XX:+UseG1GC # Player side: share the tunnel as a local TCP port for a vanilla client mchost connect https://funny-name.trycloudflare.com --secret my-secret # ...or paste the short invite mchost prints: mchost connect 'mchost://funny-name.trycloudflare.com/mc?s=my-secret' ``` The server prints the exact invite to share with players whenever the tunnel comes up (and again when you type `tunnel`). The same invite is written to `mchost-address.txt` in the server directory, which the [Fabric server mod](mod) turns into a click-to-copy chat message in-game. If you don't name a jar, mchost picks the newest server-looking jar in the server directory. The port is read from `server.properties` (`server-port`, default 25565) or forced with `--port`. Everything the server prints shows up prefixed with `[server]`, everything cloudflared prints with `[tunnel]`. While it runs you can type into the console: - `stop` — gracefully stop the server (same as typing `stop` in the MC console) - `tunnel` — show the current tunnel address - `help` — list commands - anything else — forwarded straight to the Minecraft server console `Ctrl-C` does a graceful stop too (second `Ctrl-C` forces). A quick tunnel that drops is restarted automatically with a fresh random name (up to `--restarts`, default 5). ## Named tunnel setup (purchased domain) 1. Buy a domain and add it to Cloudflare (its DNS must be on Cloudflare). 2. In the [Zero Trust dashboard](https://one.dash.cloudflare.com) go to **Networks → Tunnels → Create a tunnel**, choose **Cloudflared**, name it. 3. Copy the token from the install command Cloudflare shows you — that's the `--token` value for mchost. 4. Still in the tunnel's configuration, add a **Public Hostname**: - Subdomain/Domain: e.g. `mc.example.com` - Service: type **HTTP**, URL `http://localhost:8081` — the WebSocket bridge mchost runs (use your `--bridge-port` if you changed it) 5. Run `mchost server.jar --token `. mchost just runs `cloudflared tunnel run --token ...`; Cloudflare routes `mc.example.com` to your server, and players run `mchost connect https://mc.example.com --secret ...` as usual. (If you run with `--no-bridge` and Cloudflare Spectrum, use service type **TCP** and `localhost:25565` instead — then players connect directly, no helper.) ## How it works (and why a bridge) Cloudflare's tunnel edge is HTTP-only: a vanilla Minecraft client cannot speak raw TCP to a `*.trycloudflare.com` name or a tunnelled custom domain. mchost handles that end to end: 1. It runs a **WebSocket bridge** on `127.0.0.1:8081` that maps each WebSocket session onto the local Minecraft TCP port (`--bridge-port` to change, `--no-bridge` to disable). 2. It points the tunnel at the bridge (`http://127.0.0.1:8081`). 3. `mchost connect ` (player side) opens a WebSocket to the tunnel and listens on `127.0.0.1:25565`, so the player's **unmodified** Minecraft client just joins `127.0.0.1`. Every local connection gets its own tunnel session, so several players behind one helper can play together (`--bind 0.0.0.0` opens it to the whole LAN). Practical notes: - **Use `--bridge-secret`** (and pass it with `--secret` on the player side). Quick-tunnel URLs are public-ish; the secret makes the bridge answer 401 to strangers and the helper fails fast with a clear message. As always, real protection is `online-mode=true` in `server.properties`. - Right after a quick tunnel starts, its random name can take a few seconds to appear in DNS. The helper retries on its own and reports progress. - The WebSocket rides an HTTP/1.1 upgrade. Generic HTTP tools that default to HTTP/2 (curl, for instance) will get a 502 from the bridge — that is the edge, not a bug; the helper speaks HTTP/1.1 and works. - If you want vanilla-direct connections with no helper, you need one of: **Cloudflare Spectrum** (paid TCP proxying) with `--no-bridge`, a **Zero Trust private network + WARP** on the players' side, or a TCP tunnel tool built for it (playit.gg, ngrok TCP, a VPS + Tailscale). ## Minecraft mods (Fabric) [`mod/`](mod) contains two optional Fabric mods for Minecraft 26.3 that make sharing fully in-game: - **mchost client** — a *Tunnel...* button in the Multiplayer screen: players paste the invite and join with a vanilla client, no CLI needed. Remembers the last invite. - **mchost server** — shows the invite **in chat, click-to-copy**: when mchost publishes it, when an operator joins, and via `/mchost`. Build with `./gradlew build` in each folder (JDK 25, see [mod/README.md](mod/README.md)). Use `--public-host mc.example.com` with `--token` so the invite uses your own domain. ## Options ``` mchost [SERVER_JAR] [options] -j, --jar Server jar to run (auto-detected when omitted) -d, --dir Server working directory (default: the jar's directory) --java Java executable (default: java) -m, --memory JVM heap, e.g. 2G or 4096M (default: 2G) -p, --port Minecraft port (default: server.properties, else 25565) --eula Write eula=true into eula.txt (accept the Minecraft EULA) --token Cloudflare Tunnel token (named tunnel on your domain) --no-tunnel Run without any tunnel --cloudflared Path to the cloudflared binary --restarts Quick-tunnel restart budget (default: 5) --bridge-port Local port of the WebSocket bridge (default: 8081) --bridge-secret Require this secret from joining players --no-bridge Disable the bridge (Spectrum/TCP-style setups only) -h, --help Show help -V, --version Show version mchost connect [options] --local-port Local TCP port for the vanilla client (default: 25565) --bind Local address to listen on (default: 127.0.0.1) --secret The server's --bridge-secret value, if it has one ```