Files
Felis/deploy/lobby/entrypoint.sh
T
flyemoji 694e3cb800 feat(rcon): provision per-server RCON so the console, player list and permissions work
A server created through the panel never had RCON. CreateServer built a
MinecraftServerSpec without a Rcon block at all, so the field took its zero value
and every downstream consumer read Enabled=false. Nothing failed loudly: the
operator skips the probe when RCON is off and marks the server Ready on pod
readiness alone, so the panel showed "运行中" for a server the control plane could
not talk to. Everything that rides the write channel (spec §8 写=RCON) was dead —
the online-player list returned nothing because Status.Players is only ever
sampled by the probe, and console writes answered 503 ErrConsoleUnavailable
because internal/api/console.go refuses when Enabled is false.

The whole RCON machinery already existed — builders gate the service port,
container port, preStop save-and-stop hook and the RCON_* env on Spec.Rcon,
the reconciler probes and reports, console.go dials, the NetworkPolicy opens
25575 to {api, operator}. The only thing missing was that nobody ever turned it
on or created a password. This wires the three layers that were absent.

Provisioning lives in the operator, not in felis-api. felis-api holds secrets:get
and not create, and giving it create solely to mint a password it immediately
stops caring about (console.go re-reads the Secret at command time) would widen
the API's powers for nothing. The operator already reads every Secret in the
namespace, so adding create there grants no read it did not have. It also makes
provisioning declarative: a Secret deleted by hand comes back on the next pass, a
controller reference garbage-collects it with the server so no delete path has to
remember it, and a server that predates RCON only needs spec.rcon filled in for
the password to appear. The name comes from naming.RconSecretName so felis-api,
`felis setup` and the operator cannot drift apart on it.

RCON is enabled per system service rather than by default, because enabling it on
a backend that serves no RCON listener is destructive rather than merely useless:
the operator gates readiness on the probe, so such a server never leaves Starting
and is eventually marked Failed. The login limbo is exactly that backend
(LOOHP/Limbo has no RCON) and it is the front door, so it stays off; the lobby
runs Paper and is administered through the panel like any other server, so it is
on.

Paper only reads RCON settings from server.properties, so the operator's injected
RCON_PASSWORD did nothing on its own — felis-lobby's entrypoint now writes the
three keys on every boot. Rewriting them each time makes the copy in the world
volume derived state rather than the source of truth, so an owner who edits them
through the panel's file editor cannot lock the control plane out of their own
server. Without a password it sets enable-rcon=false and warns rather than
refusing to start: unlike the forwarding secret, a missing RCON password degrades
the server rather than making it unsafe.

That password landing in server.properties is a §286 exposure (RCON 密码绝不下发
前端), since server.properties is readable through the file editor. It is redacted
on read rather than the file being denied outright the way config/paper-global.yml
is: the forwarding secret is cluster-wide material that merely happens to sit in
the volume, whereas server.properties is the single most-edited config an owner
has, and hiding one line should not cost them MOTD, difficulty and view-distance.
The write path is deliberately left alone — the boot-time rewrite restores the
real value, which is what makes redacting rather than denying safe here.

Also guards idle auto-stop on Rcon.Enabled. Status.Players is only meaningful
when the probe ran; with RCON off it keeps its zero value, which that branch would
have read as "empty" and used to stop a server full of people. AutoStopEnabled is
not currently settable through any path, so this is a latent footgun rather than a
live bug, but it is one line and the alternative is discovering it in production.

Checks: the operator provisions a missing Secret with a 32-hex-char password and a
controller reference, and does not rotate an existing one; idle auto-stop stays
inert without RCON; the editor redacts rcon.password from the world root's
server.properties while leaving the rest of the file (and a plugin's own nested
copy) intact; login has RCON off and lobby has it on with the shared secret name;
CreateServer sets the block. That last one departs from K8sCluster being
integration-tested against a live cluster: this defect was a struct literal
missing a field, it shipped, and a fake client is enough to pin a struct literal.

Existing servers are NOT migrated by this change — CreateServer only covers new
ones and ensureSystemServers is create-if-absent, so a `felis setup` re-run will
not touch an existing lobby. A deployed install additionally needs the
felis-lobby image rebuilt and re-imported for the entrypoint change, and its pods
recreated, before the RCON keys reach server.properties.
2026-07-21 00:12:37 +09:00

113 lines
5.4 KiB
Bash

#!/bin/sh
# Felis lobby (Paper) entrypoint.
#
# The lobby sits BEHIND the login gate: a player only reaches it once the limbo has
# authenticated them and Velocity transferred them onward. For that transfer to arrive
# with a real identity, Paper has to be told to verify the proxy's signed handshake —
# otherwise it derives an offline UUID from the username and every /menu action would be
# attributed to whoever typed the name. So, exactly as in deploy/limbo/entrypoint.sh:
# NO SECRET, NO START. Refusing to boot is the safe failure; a lobby that came up in
# offline mode would look healthy while trusting forged identities.
#
# Two files carry the settings:
#
# config/paper-global.yml proxies.velocity.{enabled,online-mode,secret} — enable modern
# forwarding and share the proxy's HMAC key. online-mode mirrors
# the proxy's own online-mode (true: Velocity did the Mojang
# auth), which is what makes the forwarded UUID trustworthy.
#
# server.properties online-mode=false — the PROXY authenticated the player, so the
# backend must not try to reach Mojang itself (Paper refuses to
# start with velocity forwarding on and online-mode=true). This
# is not a downgrade: the trust comes from the signed handshake.
# server-port is pinned to the operator's GamePort (25565), the
# single const the Service, probes and NetworkPolicy all key off.
set -eu
PORT="${FELIS_GAME_PORT:-25565}"
SECRET="${FELIS_FORWARDING_SECRET:-}"
RUNTIME_DIR="/paper"
DATA_DIR="/data"
PROPS="server.properties"
if [ -z "$SECRET" ]; then
echo "felis-lobby: FATAL — FELIS_FORWARDING_SECRET is empty." >&2
echo " Without Velocity modern forwarding Paper cannot verify who a joining player is," >&2
echo " and would trust an offline UUID derived from the username alone." >&2
echo " Provision the secret with deploy/bootstrap.sh, then re-run 'sudo felis setup'." >&2
exit 1
fi
# Keep worlds, generated config, and plugin data on the operator-mounted PVC while
# refreshing executable artifacts from the immutable image on every boot.
mkdir -p "$DATA_DIR/plugins"
cp -f "$RUNTIME_DIR/paper.jar" "$DATA_DIR/paper.jar"
cp -f "$RUNTIME_DIR/plugins/felis-paper.jar" "$DATA_DIR/plugins/felis-paper.jar"
printf 'eula=true\n' > "$DATA_DIR/eula.txt"
cd "$DATA_DIR"
# set_prop KEY VALUE — replace the key's line in server.properties, or append it if absent.
set_prop() {
if [ -f "$PROPS" ] && grep -q "^$1=" "$PROPS"; then
sed -i "s|^$1=.*|$1=$2|" "$PROPS"
else
printf '%s=%s\n' "$1" "$2" >> "$PROPS"
fi
}
set_prop server-port "$PORT"
set_prop online-mode false
# RCON is the control plane's write channel (spec §8 写=RCON): the operator probes it
# for readiness and the player tally, and felis-api runs console/permission commands over
# it. Paper only reads these three keys from server.properties, so the operator's injected
# RCON_PASSWORD has to be written here to take effect — env alone does nothing.
#
# Rewritten on EVERY boot from the Secret, deliberately. That makes the value in the world
# volume derived state rather than the source of truth: an owner who edits (or clobbers)
# these lines through the panel's file editor cannot lock the control plane out of their
# own server, because the next restart restores the real password. The editor is also kept
# from reading the password back out — see internal/fileedit/exec.go (spec §286: RCON
# 密码绝不下发前端).
#
# No password, no RCON: an empty enable-rcon=true would let anything that reaches the port
# in unauthenticated. Unlike the forwarding secret this is not fatal — a server without the
# write channel still serves players — so it warns and starts rather than refusing.
if [ -n "${RCON_PASSWORD:-}" ]; then
set_prop enable-rcon true
set_prop rcon.port "${RCON_PORT:-25575}"
set_prop rcon.password "$RCON_PASSWORD"
echo "felis-lobby: rcon enabled on port ${RCON_PORT:-25575}"
else
set_prop enable-rcon false
echo "felis-lobby: WARNING — RCON_PASSWORD is empty, so the console, the online-player" >&2
echo " list and permission changes will be unavailable for this server. The operator" >&2
echo " injects it from the <server>-rcon Secret when spec.rcon.enabled is true." >&2
fi
# ponytail: rewritten whole, not merged. Paper loads this file and fills every key it does
# not find with the default, then writes the full tree back — so a proxies-only file is a
# complete, stable input, and the lobby's other globals are simply always the defaults.
# That is true of a system server Felis owns end to end; if admins are ever allowed to tune
# the lobby's globals, this has to become a real YAML merge (yq) instead.
mkdir -p config
cat > config/paper-global.yml <<YAML
# Written by felis-lobby's entrypoint on every boot. Do not hand-edit: the forwarding
# secret is injected from the felis-forwarding-secret Secret and must match the proxy.
proxies:
velocity:
enabled: true
online-mode: true
secret: "${SECRET}"
YAML
echo "felis-lobby: server-port=${PORT}, velocity modern forwarding on (UUIDs are Mojang-verified)"
JAVA_MEMORY_ARG=""
if [ -n "${JAVA_MEMORY:-}" ]; then
JAVA_MEMORY_ARG="-Xmx${JAVA_MEMORY}"
fi
set -f
# JAVA_FLAGS is emitted by the operator as a whitespace-separated JVM argument list.
# shellcheck disable=SC2086
exec java $JAVA_MEMORY_ARG ${JAVA_FLAGS:-} -jar paper.jar --nogui "$@"