feat(minecraft): wake worlds on demand through mc-router, at most two at once #44

Open
hutao wants to merge 0 commits from feat/mc-router into main
Owner

What

mc-router now owns the public 25565 and sends each player to a world by the
hostname they typed (minecraft./mc./bare IP → world 1, mc2. → world 2).
Worlds publish no game port, start when someone logs in, and stop ten
minutes after the last player leaves. At most two run at once.

  • modules/containers/mc-router.nix: mc-router 1.48.0 (the release where
    only logins, not server-list pings, wake a world or reset its idle timer).
  • modules/containers/minecraft-cap.nix: the cap, an ExecCondition on each
    world's unit, so it holds for the webhook, a hand-typed systemctl start
    and the backup alike. Plus minecraft-scaler, mc-router's webhook
    receiver: root because polkit is off, no capabilities, IPAddressAllow
    set to mc-router's address.
  • New pinned mc network (172.31.0.0/24). Worlds get addresses from
    172.31.0.128/25 so they can never take mc-router's .2.
  • Firewall: 25566 closed; only mc-router reaches the scaler; the mc network
    is subtracted from the broad bridge rule (the worlds run third-party mods).
  • Weekly backup restarts only the worlds that were up, and a hold file blocks
    every start while it runs.
  • tofu: mc2 SRV record and the edge 25566 rule removed.
  • Handbook: services, network, data, runbook, deploying, CI, access control.

Deployed

Already live on the VPS from this branch (2026-10-22):

  • First deploy: docker gave world 1 172.31.0.2 before mc-router started, so
    mc-router failed with Address already in use. Fixed in 56133ba
    (--ip-range), redeployed, and the mc network recreated by hand (only
    mc-router was on it, nobody online).
  • From the internet, mc., mc2. and minecraft. all answer through
    mc-router with the asleep MOTD.
  • A login probe to mc2. woke world 2; it answered after 24s on
    172.31.0.128.
  • tofu: only the SRV record is applied (targeted). The Hetzner firewall
    change (drop 25566, rename the 25565 rule) is still pending, so a full
    tofu plan shows it. nftables already drops 25566 on the box.

Checks

  • nix build .#checks.x86_64-linux.minecraft-cap -L (hand-run, needs KVM):
    7/7 subtests pass. They cover the cap's 409, a refused start not looping,
    the hold file, a three-way start race landing on two, bad input, and the
    source-address restriction.
  • nix flake check --no-build, all pre-commit hooks, and the mdbook build pass.

Known limits

  • The first join to a sleeping world usually times out; the second, a minute
    later, gets in.
  • Worlds see mc-router's address, not the player's (whitelist and bans are by
    UUID).
  • A world started by hand, or one whose waking login gave up before it
    booted, has no connection through mc-router and never gets the idle stop.
  • A scanner that sends a login, not just a ping, can wake a world.
    AUTO_SCALE_ALLOW_DENY would limit waking to known players.
## What mc-router now owns the public 25565 and sends each player to a world by the hostname they typed (`minecraft.`/`mc.`/bare IP → world 1, `mc2.` → world 2). Worlds publish no game port, start when someone logs in, and stop ten minutes after the last player leaves. **At most two run at once.** - `modules/containers/mc-router.nix`: mc-router 1.48.0 (the release where only logins, not server-list pings, wake a world or reset its idle timer). - `modules/containers/minecraft-cap.nix`: the cap, an `ExecCondition` on each world's unit, so it holds for the webhook, a hand-typed `systemctl start` and the backup alike. Plus `minecraft-scaler`, mc-router's webhook receiver: root because polkit is off, no capabilities, `IPAddressAllow` set to mc-router's address. - New pinned `mc` network (`172.31.0.0/24`). Worlds get addresses from `172.31.0.128/25` so they can never take mc-router's `.2`. - Firewall: 25566 closed; only mc-router reaches the scaler; the `mc` network is subtracted from the broad bridge rule (the worlds run third-party mods). - Weekly backup restarts only the worlds that were up, and a hold file blocks every start while it runs. - tofu: `mc2` SRV record and the edge 25566 rule removed. - Handbook: services, network, data, runbook, deploying, CI, access control. ## Deployed Already live on the VPS from this branch (2026-10-22): - First deploy: docker gave world 1 `172.31.0.2` before mc-router started, so mc-router failed with `Address already in use`. Fixed in 56133ba (`--ip-range`), redeployed, and the `mc` network recreated by hand (only mc-router was on it, nobody online). - From the internet, `mc.`, `mc2.` and `minecraft.` all answer through mc-router with the asleep MOTD. - A login probe to `mc2.` woke world 2; it answered after 24s on `172.31.0.128`. - tofu: **only the SRV record is applied** (targeted). The Hetzner firewall change (drop 25566, rename the 25565 rule) is still pending, so a full `tofu plan` shows it. nftables already drops 25566 on the box. ## Checks - `nix build .#checks.x86_64-linux.minecraft-cap -L` (hand-run, needs KVM): 7/7 subtests pass. They cover the cap's 409, a refused start not looping, the hold file, a three-way start race landing on two, bad input, and the source-address restriction. - `nix flake check --no-build`, all pre-commit hooks, and the mdbook build pass. ## Known limits - The first join to a sleeping world usually times out; the second, a minute later, gets in. - Worlds see mc-router's address, not the player's (whitelist and bans are by UUID). - A world started by hand, or one whose waking login gave up before it booted, has no connection through mc-router and never gets the idle stop. - A scanner that sends a login, not just a ping, can wake a world. `AUTO_SCALE_ALLOW_DENY` would limit waking to known players.
mc-router now owns the public 25565 and picks the world by the hostname
in the handshake, so the worlds publish no game port and sit on a pinned
`mc` network. A login to a sleeping world calls minecraft-scaler, which
starts its unit; ten minutes after the last player leaves, it stops it.
No world starts at boot.

The cap is an ExecCondition on each world's unit, so it holds for every
caller, not just the webhook: it counts the other worlds that are up and
refuses at two, skipping the start rather than failing it so
Restart=always has nothing to retry. The box has 16 G, no swap, and mail
on it, which is what the cap protects.

minecraft-scaler runs as root because polkit is off, with no
capabilities and IPAddressAllow set to mc-router's address; the firewall
admits that address alone, and the mc network is subtracted from the
broad bridge rule since the worlds run third-party mods.

The weekly backup restarts only the worlds that were up, and holds every
start off while it runs so a login cannot wake a world mid-snapshot.
tests/minecraft-cap.nix boots the cap and the scaler against stand-in
worlds.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
mc-router tells the worlds apart by hostname on 25565, so mc2 needs only
its A record. The SRV record pointing at 25566 and the edge firewall's
25566 rule go, along with the SRV record's import block.

Apply right after deploying the NixOS side: between the two, mc2's SRV
record still sends players to a closed port.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
fix(minecraft): keep the worlds off mc-router's address
All checks were successful
CI / Format, lint and evaluate (pull_request) Successful in 3m43s
56133ba1b8
On the first deploy the worlds started before mc-router, docker handed
world 1 172.31.0.2, and mc-router's pinned --ip failed with "Address
already in use" on every restart. That also put world 1 on the one
address the firewall and minecraft-scaler trust.

The mc network now hands out addresses from 172.31.0.128/25 only, so
mcRouterAddress is never allocated to anything else. An existing network
is not reconciled, so this one is removed and recreated by hand once.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
feat(minecraft): only whitelisted players wake a world
All checks were successful
CI / Format, lint and evaluate (pull_request) Successful in 3m58s
CI / Format, lint and evaluate (push) Successful in 3m56s
Pages / Build the handbook (push) Successful in 41s
ae294d61fb
A scanner that sends a login, not just a ping, otherwise boots a world
for nothing; one hit the default route minutes after the first deploy.
mc-router now carries a global allowlist built from every world's
whitelist.json each time it starts, so the in-game whitelist stays the
one list and no player name or UUID lands in this public repo.

An empty allowlist lets everyone through in mc-router, so the list
always carries one UUID that matches nobody: a missing or malformed
whitelist means nobody wakes a world, not anybody.

mc-router compares the UUID the client claims; Mojang's check still
happens at the world, which is online-mode with an enforced whitelist.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
All checks were successful
CI / Format, lint and evaluate (pull_request) Successful in 3m58s
CI / Format, lint and evaluate (push) Successful in 3m56s
Pages / Build the handbook (push) Successful in 41s
This branch is already included in the target branch. There is nothing to merge.
View command line instructions

Checkout

From your project repository, check out a new branch and test the changes.
git fetch -u origin feat/mc-router:feat/mc-router
git switch feat/mc-router

Merge

Merge the changes and update on Forgejo.

Warning: The "Autodetect manual merge" setting is not enabled for this repository, you will have to mark this pull request as manually merged afterwards.

git switch main
git merge --no-ff feat/mc-router
git switch feat/mc-router
git rebase main
git switch main
git merge --ff-only feat/mc-router
git switch feat/mc-router
git rebase main
git switch main
git merge --no-ff feat/mc-router
git switch main
git merge --squash feat/mc-router
git switch main
git merge --ff-only feat/mc-router
git switch main
git merge feat/mc-router
git push origin main
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
hutao/vps!44
No description provided.