panel — open-source game server manager (public release)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-14 19:19:43 -07:00
commit 658bda1d24
2160 changed files with 300413 additions and 0 deletions
+51
View File
@@ -0,0 +1,51 @@
# panel-native DayZ runtime image.
#
# DayZ ships a native Linux binary (SteamCMD app 223350). No Wine. The
# binary is a static-ish x86_64 ELF that pulls in the usual i386 pile
# (steamclient.so is 32-bit) + a couple of Bohemia's own shared libs
# that live under <install>/.
#
# Expected runtime layout inside the container:
# /game — SteamCMD-installed DayZ Server
# /game/DayZServer — server binary (Bohemia's launcher wrapper)
# /game/serverDZ.cfg — primary config file (symlinked to /game-saves)
# /game/battleye/ — BattlEye config + beserver.cfg (RCON port + pw)
# /game/profiles — logs, bans, RPT crash dumps (symlinked to /game-saves)
# /game-saves/ — operator data volume (configs, profiles, world DB)
FROM debian:12-slim
ENV DEBIAN_FRONTEND=noninteractive \
TZ=Etc/UTC \
LANG=en_US.UTF-8 \
LC_ALL=en_US.UTF-8
RUN dpkg --add-architecture i386 \
&& apt-get update \
&& apt-get install -y --no-install-recommends \
ca-certificates \
locales \
tini \
curl \
lib32gcc-s1 \
lib32stdc++6 \
libc6:i386 \
libcurl4 \
libxml2 \
procps \
&& sed -i -e 's/# en_US.UTF-8 UTF-8/en_US.UTF-8 UTF-8/' /etc/locale.gen \
&& locale-gen \
&& apt-get clean \
&& rm -rf /var/lib/apt/lists/*
RUN groupadd -g 1000 panel \
&& useradd -u 1000 -g 1000 -m -s /bin/bash panel \
&& mkdir -p /game /game-saves \
&& chown -R panel:panel /game /game-saves
COPY entrypoint.sh /entrypoint.sh
RUN chmod +x /entrypoint.sh
EXPOSE 2302/udp 2303/udp 2304/udp 2305/udp
ENTRYPOINT ["/usr/bin/tini", "--", "/entrypoint.sh"]
+82
View File
@@ -0,0 +1,82 @@
# DayZ — panel-native module
Panel-native DayZ dedicated server, native-Linux (no Wine). Steam app 223350.
## The Steam-login gotcha
**DayZ is paid content.** SteamCMD refuses `+login anonymous` with `No subscription`. The manifest sets `requires_steam_login: true` on the update provider, which tells the controller to gate install/update behind the Steam-login modal. Flow:
1. Create a DayZ instance in the panel. Auto-update is intentionally skipped (agent sees `requires_steam_login` on the provider and doesn't kick off the first Update).
2. Click **Update** on the instance.
3. Panel returns `{steam_login_required: true}` and the browser pops a login modal.
4. Enter your Steam username + password. If Steam Guard is enabled on your account, also enter the 5-char code from your email / Steam Mobile app.
5. Panel runs a one-shot `docker run steamcmd/steamcmd +login <user> <pass>` to verify, stores the password encrypted (AES-GCM, key HKDF-derived from the panel's CA private key), and caches the SSFN sentry file in a panel-wide volume (`panel-steamcmd-auth`) so future updates skip Steam Guard.
6. Retry Update — panel injects creds into the SteamCMD sidecar and downloads ~5 GB of DayZ server files.
Your Steam account must own the DayZ base game (app 221100). The dedicated-server app itself (223350) is a free tool; ownership of the base game is what grants download rights.
## Image + runtime
- `panel-dayz:latest` — debian:12-slim, 32-bit runtime libs (`libc6:i386`, `lib32gcc-s1`, `lib32stdc++6`) for `steamclient.so`, plus `libcurl4`/`libxml2`. No Wine.
- Binary: `/game/DayZServer` (ELF64, native).
- Config: `/game-saves/serverDZ.cfg` (symlinked to `/game/serverDZ.cfg`).
- Profiles (logs, bans, RPTs): `/game-saves/profiles/` (symlinked to `/game/profiles`).
- BattlEye config: `/game/battleye/beserver_x64.cfg` (port + password written on first boot from `BE_PASSWORD` env var).
## RCON — BattlEye protocol
DayZ uses BattlEye's proprietary UDP RCON, not Source RCON. The panel's `be_rcon` adapter handles:
- Login handshake (password auth → single-byte success/fail)
- Sequenced commands with per-seq ack tracking
- Multi-part response reassembly for long outputs (`players` on busy servers)
- Server-pushed events (chat/join/leave) ack'd automatically
- 30-second keep-alive ping (BE disconnects idle clients after ~45s)
Commands wired:
- `list``players`
- `broadcast {msg}``say -1 {msg}`
- `kick {player}``kick {player}`
- `ban {player}``ban {player}`
- `shutdown``#shutdown`
## Ports
| Port | Proto | Purpose |
|------|-------|---------|
| 2302 | UDP | Game + Steam query (same port — Bohemia convention) |
| 2303 | UDP | Additional instance offset (+1) |
| 2304 | UDP | Additional instance offset (+2) |
| 2305 | UDP | Additional instance offset (+3) |
| 2310 | UDP | BattlEye RCON (internal) |
## Environment vars
| Var | Default | Notes |
|-----|---------|-------|
| `SERVER_NAME` | `panel DayZ` | Shown in the DayZ server browser |
| `SERVER_PORT` | `2302` | Game + Steam-query UDP port |
| `SERVER_PASSWORD` | *(empty)* | Optional client join password |
| `BE_PASSWORD` | `panel_be` | BattlEye RCON admin password (edit before exposing) |
| `CPU_COUNT` | `2` | `-cpuCount=` arg to the server binary |
## Log events
- **Join:** `Player "<name>" is connected`
- **Leave:** `Player "<name>" disconnected`
- **Chat:** `Chat("<name>"): <msg>`
## Known gotchas
- **Steam credentials are a one-time ceremony per panel.** Once stored, the cached SSFN blob means subsequent updates don't re-prompt. If Steam invalidates your session (machine change, long inactivity), the modal will pop again with `{need_guard: true}` — enter a fresh code.
- **BattlEye RCON port collides by convention.** 2310 is our default; if you run multiple DayZ instances on the same host, bump via the panel's Network Ports editor.
- **Modded servers need `@<ModName>` in the launch command.** The entrypoint doesn't wire this yet — drop mod pbos into `/game/` and add `-mod=@<ModName>` via a manual launcher edit (or wait for a follow-up that exposes `EXTRA_ARGS` as an env var).
- **`steamQueryPort` must equal the game port** in `serverDZ.cfg` or DayZ refuses to register with Steam's master server. Our seeded config follows this.
- **BattlEye can kick you for "kick #0 CreateVehicle Restriction..."** type messages on vanilla — these are server-side integrity checks. Tune `verifySignatures` and the `battleye/bans.txt` / `permissions.txt` files via the Files tab.
## References
- [Bohemia Interactive: Hosting a Linux Server (DayZ)](https://community.bistudio.com/wiki/DayZ:Hosting_a_Linux_Server)
- [BattlEye RCON Protocol](https://www.battleye.com/downloads/BERConProtocol.txt)
- [Steam — DayZ Server (app 223350)](https://steamdb.info/app/223350/)
+204
View File
@@ -0,0 +1,204 @@
#!/bin/bash
# panel-native DayZ entrypoint.
#
# Contract:
# /game — SteamCMD-installed DayZ Server (app 223350). Populated
# by the panel's SteamCMD updater sidecar, which uses the
# operator-supplied Steam credentials (the module manifest
# sets requires_steam_login: true).
# /game-saves — serverDZ.cfg + profiles/ (logs, bans, RPTs) + optional
# mpmissions/ override tree.
set -euo pipefail
log() { printf '[panel-dayz] %s\n' "$*"; }
# ----- stage 1: root-only bootstrap -----
if [[ "$(id -u)" = "0" ]]; then
chown -R panel:panel /game /game-saves /home/panel 2>/dev/null || true
log "dropping privileges to panel (UID 1000)"
exec setpriv --reuid=panel --regid=panel --clear-groups \
--inh-caps=-all --bounding-set=-all \
env \
HOME=/home/panel \
SERVER_NAME="${SERVER_NAME:-panel DayZ}" \
SERVER_PORT="${SERVER_PORT:-2302}" \
SERVER_PASSWORD="${SERVER_PASSWORD:-}" \
BE_PASSWORD="${BE_PASSWORD:-panel_be}" \
CPU_COUNT="${CPU_COUNT:-2}" \
LD_LIBRARY_PATH="/game:${LD_LIBRARY_PATH:-}" \
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin \
/entrypoint.sh "$@"
fi
GAME_DIR=/game
SAVE_DIR=/game-saves
DEDI_EXE="${GAME_DIR}/DayZServer"
if [[ ! -x "${DEDI_EXE}" ]]; then
log "ERROR: ${DEDI_EXE} not found or not executable."
log " Run 'Update' on this server in the panel — the modal will"
log " prompt for Steam credentials (DayZ is paid content; the"
log " anonymous SteamCMD login won't work)."
exit 78 # EX_CONFIG
fi
# --- persist profiles into /game-saves ---
# DayZ writes RPT crash logs, ADM admin logs, and BattlEye bans to
# <install>/profiles/. Move out to the save volume + symlink back on
# first boot so SteamCMD validate can't wipe them.
mkdir -p "${SAVE_DIR}/profiles"
if [[ -d "${GAME_DIR}/profiles" && ! -L "${GAME_DIR}/profiles" ]]; then
mv "${GAME_DIR}/profiles"/* "${SAVE_DIR}/profiles/" 2>/dev/null || true
rm -rf "${GAME_DIR}/profiles"
fi
ln -sfn "${SAVE_DIR}/profiles" "${GAME_DIR}/profiles"
# --- seed serverDZ.cfg on first boot ---
CFG="${SAVE_DIR}/serverDZ.cfg"
if [[ ! -s "${CFG}" ]]; then
log "seeding serverDZ.cfg (first boot)"
cat > "${CFG}" <<EOF
hostname = "${SERVER_NAME}";
password = "${SERVER_PASSWORD}";
passwordAdmin = "";
enableWhitelist = 0;
disableVoN = 0;
vonCodecQuality = 7;
disable3rdPerson = 0;
disableCrosshair = 0;
serverTime = "SystemTime";
serverTimeAcceleration = 12;
serverNightTimeAcceleration = 1;
serverTimePersistent = 0;
guaranteedUpdates = 1;
loginQueueConcurrentPlayers = 5;
loginQueueMaxPlayers = 500;
instanceId = 1;
storageAutoFix = 1;
verifySignatures = 2;
forceSameBuild = 1;
timeStampFormat = "Short";
logAverageFps = 1;
logMemory = 1;
logPlayers = 1;
logFile = "server_console.log";
adminLogPlayerHitsOnly = 0;
adminLogPlacement = 1;
adminLogBuildActions = 1;
adminLogPlayerList = 1;
disableBanlist = 0;
BattlEye = 1;
respawnTime = 5;
steamQueryPort = ${SERVER_PORT};
maxPing = 200;
MissionTemplate = "dayzOffline.chernarusplus";
class Missions
{
class DayZ
{
template = "dayzOffline.chernarusplus";
};
};
EOF
fi
# Point /game/serverDZ.cfg at the save-volume copy so the binary's default
# relative-path lookup finds it.
ln -sf "${CFG}" "${GAME_DIR}/serverDZ.cfg"
# --- BattlEye RCON config ---
# DayZ ships a `battleye/` dir that contains a NESTED `battleye/` subdir with
# a duplicate `beserver_x64.so`. The DayZ launcher (`-BEpath=battleye`) loads
# BE from there and expects `beserver_x64.cfg` to sit alongside the nested
# lib — i.e. `battleye/battleye/beserver_x64.cfg`, NOT `battleye/beserver_x64.cfg`.
# Writing to the outer dir (which is what most tutorials show) results in BE
# initializing but never binding the RCON port, with no error in the log.
#
# We unconditionally overwrite both locations so operator env changes take
# effect on the next boot. We also purge stale `beserver_x64_active_<token>.cfg`
# files BE writes after first handshake — if the password has changed, the
# active copy will mismatch and BE refuses incoming RCON packets.
#
# Reference: AMP's dayz-experimental.kvp uses `{{$FullBaseDir}}battleye` as
# the BE path and their PreStartStages purge the active file the same way.
BE_DIR="${GAME_DIR}/battleye"
for CFGPATH in "${BE_DIR}/beserver_x64.cfg" "${BE_DIR}/battleye/beserver_x64.cfg"; do
if [[ -d "$(dirname "${CFGPATH}")" ]]; then
log "writing ${CFGPATH} (RCON port 2305, pw from BE_PASSWORD)"
cat > "${CFGPATH}" <<EOF
RConPassword ${BE_PASSWORD}
RConPort 2305
RConIP 0.0.0.0
RestrictRCon 0
RConLog 1
UseLocationsToBan 0
LogFile 0
EOF
fi
done
# Purge stale active-session files (both locations).
rm -f "${BE_DIR}/beserver_x64_active_"*.cfg 2>/dev/null || true
rm -f "${BE_DIR}/battleye/beserver_x64_active_"*.cfg 2>/dev/null || true
cd "${GAME_DIR}"
log "starting DayZ dedicated server"
log " name: ${SERVER_NAME}"
log " game port: ${SERVER_PORT}/udp"
log " rcon port: 2305/udp (internal, BattlEye)"
log " cfg: ${CFG}"
trap 'log "SIGTERM received"; kill -TERM "${DZ_PID:-0}" 2>/dev/null || true; wait "${DZ_PID:-0}" 2>/dev/null || true; exit 0' TERM INT
# --- mod list dynamic build ---
# Panel's mod manager maintains two plain-text files on the save volume:
# /game-saves/panel-mods.txt — one @ModFolder per line → -mod=
# /game-saves/panel-servermods.txt — one @ModFolder per line → -servermod=
# Blank lines and lines starting with '#' are ignored. This lets the mod
# manager edit a human-readable file and have the server pick up the new
# load order on the next start without entrypoint code changes.
build_mod_arg() {
local file="$1" prefix="$2"
[[ -s "$file" ]] || { echo ""; return; }
local joined=""
while IFS= read -r line || [[ -n "$line" ]]; do
line="${line%$'\r'}" # strip trailing CR (Windows edits)
line="${line#"${line%%[![:space:]]*}"}" # ltrim
line="${line%"${line##*[![:space:]]}"}" # rtrim
[[ -z "$line" || "$line" == \#* ]] && continue
if [[ -z "$joined" ]]; then joined="$line"
else joined="${joined};${line}"
fi
done < "$file"
if [[ -n "$joined" ]]; then
echo "${prefix}${joined}"
fi
}
MOD_ARG="$(build_mod_arg "${SAVE_DIR}/panel-mods.txt" '-mod=')"
SERVERMOD_ARG="$(build_mod_arg "${SAVE_DIR}/panel-servermods.txt" '-servermod=')"
[[ -n "${MOD_ARG}" ]] && log "loading mods: ${MOD_ARG}"
[[ -n "${SERVERMOD_ARG}" ]] && log "loading server-mods: ${SERVERMOD_ARG}"
# DayZ's documented launch command:
# ./DayZServer -config=serverDZ.cfg -port=2302 -BEpath=battleye \
# -profiles=profiles -dologs -adminlog -netlog \
# -freezecheck -doScriptLogs -cpuCount=2
# [-mod=@A;@B] [-servermod=@X]
DZ_ARGS=(
-config=serverDZ.cfg
-port="${SERVER_PORT}"
"-BEpath=${GAME_DIR}/battleye"
-profiles=profiles
-dologs
-adminlog
-netlog
-freezecheck
-doScriptLogs
"-cpuCount=${CPU_COUNT}"
)
[[ -n "${MOD_ARG}" ]] && DZ_ARGS+=("${MOD_ARG}")
[[ -n "${SERVERMOD_ARG}" ]] && DZ_ARGS+=("${SERVERMOD_ARG}")
exec stdbuf -oL -eL "${DEDI_EXE}" "${DZ_ARGS[@]}" &
DZ_PID=$!
wait "${DZ_PID}"
+144
View File
@@ -0,0 +1,144 @@
# DayZ dedicated server module.
#
# Native-Linux binary via SteamCMD app 223350 ("DayZ Server"). App is
# paid content — SteamCMD refuses `+login anonymous` with
# "No subscription". We set `requires_steam_login: true` which makes the
# controller gate updates behind the Steam-login modal; operators sign in
# once with a Steam account that owns DayZ (base game app 221100 — NOT
# the dedicated-server app itself — grants download entitlement).
#
# RCON is BattlEye's proprietary UDP protocol, not Source or WebSocket.
# The panel's new `be_rcon` adapter speaks it natively (MD5-challenge
# login, sequenced commands with ack packets, keep-alive pings).
#
# References:
# https://community.bistudio.com/wiki/DayZ:Hosting_a_Linux_Server
# https://www.battleye.com/downloads/BERConProtocol.txt
id: dayz
name: "DayZ"
version: 0.1.0
authors:
- panel contributors
supported_modes:
- docker
runtime:
docker:
# Built locally from ./Dockerfile. Rebuild with:
# docker build -t panel-dayz:latest modules/dayz
image: panel-dayz:latest
# Host networking -- container shares the host net namespace, so
# any port the game/mod binds is directly on the LAN. AMP-like
# model. Multi-instance safety comes from env-driven per-instance
# ports (panel allocator + env: mapping on each ports entry).
network_mode: host
browseable_root: /game-saves
browseable_roots:
- name: "Saves & Configs"
path: /game-saves
hint: "serverDZ.cfg + profiles/ (logs, bans, RPTs) + mpmissions/ world state"
- name: "Game Files"
path: /game
hint: "DayZServer binary + core keys/, shipped mpmissions, BattlEye/"
env:
SERVER_NAME: "panel DayZ"
SERVER_PORT: "2302"
BE_PASSWORD: "panel_be" # BattlEye RCON admin password
SERVER_PASSWORD: "" # optional client join password
CPU_COUNT: "2" # -cpuCount= for the server binary
volumes:
- { type: volume, name: "panel-$INSTANCE_ID-saves", container: "/game-saves" }
- { type: volume, name: "panel-$INSTANCE_ID-game", container: "/game" }
# Shared DayZ Workshop cache — multiple DayZ instances download and use
# the same workshop content. Each instance creates @ModName symlinks in
# its own /game/ pointing at this shared tree, so disk space is saved
# and one download serves N instances.
- { type: volume, name: "panel-dayz-workshop", container: "/game/steamapps/workshop" }
- { target: "$DATA_PATH/logs", container: "/game-saves/logs" }
# DayZ ports:
# 2302/udp — primary game + query (Bohemia's convention: game port and
# Steam query are the SAME port, unlike Source-engine games)
# 2303/udp — additional-instance offset (+1)
# 2304/udp — additional-instance offset (+2)
# 2305/udp — additional-instance offset (+3)
# 27016/udp — alternate Steam-query port (some community servers expose)
# 2310/udp — BattlEye RCON (BE_PORT + 1 by convention; configurable)
ports:
- { name: game, proto: udp, default: 2302, required: true, env: SERVER_PORT } # entrypoint reads SERVER_PORT (declared in docker env); query shares this port
- { name: reserved, proto: udp, default: 2303, required: false }
- { name: battleye, proto: udp, default: 2304, required: false } # client BE anti-cheat traffic
- { name: rcon, proto: udp, default: 2305, internal: true } # BE RCON (AMP convention)
resources:
min_ram_mb: 4096
recommended_ram_mb: 8192
# Per-instance generated RCON secret. The agent generates a random
# 24-byte token at create time (pkg/module GenerateSecrets), which
# overrides the env default for BE_PASSWORD below — so every instance
# gets a unique password. The weak env default only applies to
# containers launched outside the panel.
secrets:
- name: BE_PASSWORD
description: "DayZ BattlEye RCON password — generated per instance"
generated: true
rcon:
adapter: be_rcon
host_port: rcon
auth: password
password_secret: BE_PASSWORD
# Legacy fallback only (pre-generated-secret instances). New instances
# always get the generated BE_PASSWORD secret above.
password_literal: "panel_be"
commands:
list: "players"
broadcast: "say -1 {msg}"
kick: "kick {player}"
ban: "ban {player}"
save: "#shutdown" # DayZ has no "save" command; use shutdown as a placeholder
shutdown: "#shutdown"
state_sources:
- type: rcon
command: "players"
every: 60s
# Log-line event patterns (RE2). Patterns observed from a real DayZ
# 1.24 server log; tighten after smoke test if needed.
events:
join:
pattern: 'Player "(?P<name>[^"]+)" is connected'
kind: join
leave:
pattern: 'Player "(?P<name>[^"]+)" disconnected'
kind: leave
chat:
pattern: 'Chat\("(?P<name>[^"]+)"\): (?P<msg>.+)'
kind: chat
update_providers:
- id: stable
kind: steamcmd
app_id: "223350"
install_path: /game
# DayZ is paid content. Anonymous SteamCMD returns "No subscription".
# Operator logs in once via the panel's Steam-login modal; the cached
# SSFN blob (via the shared panel-steamcmd-auth volume) makes
# subsequent updates skip the 2FA prompt.
requires_steam_login: true
# --- Manifest-driven UI metadata (WI-07) --------------------------
# Consumed by the CONTROLLER/dashboard only (the controller loads its
# own ./modules copy at startup); the agent ignores these at runtime.
# ready_pattern is a JavaScript regex (slash-delimited when it needs
# flags) moved verbatim from the dashboard's READY_PATTERNS map.
ready_pattern: '/Mission read from bank|BattlEye Server: Initialized|Server World: Initialized|NetServer: (:\d+)?started|Creating a new world/i'
appearance:
emoji: "🧟"
grad: "linear-gradient(135deg,#2d3e2e,#6b7f56)"
art: "/game-art/dayz.jpg"
steam: "221100"