mchost / README.md

raw file 9280 bytes
# 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 <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 <invite>` — 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://<tunnel-url>  │  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 <path>`).

## 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 <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 <url>` (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 <PATH>           Server jar to run (auto-detected when omitted)
-d, --dir <PATH>           Server working directory (default: the jar's directory)
    --java <PATH>          Java executable (default: java)
-m, --memory <SIZE>        JVM heap, e.g. 2G or 4096M (default: 2G)
-p, --port <PORT>          Minecraft port (default: server.properties, else 25565)
    --eula                 Write eula=true into eula.txt (accept the Minecraft EULA)
    --token <TOKEN>        Cloudflare Tunnel token (named tunnel on your domain)
    --no-tunnel            Run without any tunnel
    --cloudflared <PATH>   Path to the cloudflared binary
    --restarts <N>         Quick-tunnel restart budget (default: 5)
    --bridge-port <PORT>   Local port of the WebSocket bridge (default: 8081)
    --bridge-secret <S>    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 <URL> [options]
    --local-port <PORT>    Local TCP port for the vanilla client (default: 25565)
    --bind <ADDR>          Local address to listen on (default: 127.0.0.1)
    --secret <SECRET>      The server's --bridge-secret value, if it has one
```