Production ready: hardening, limits, docs

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-09-26 20:55:39 +02:00
co-authored by Claude Opus 5.5
parent 4a948128e5
commit 7e291d06ea
6 changed files with 63 additions and 4 deletions
+3
View File
@@ -7,6 +7,9 @@
<ImplicitUsings>enable</ImplicitUsings> <ImplicitUsings>enable</ImplicitUsings>
<RootNamespace>MSCO.Server</RootNamespace> <RootNamespace>MSCO.Server</RootNamespace>
<AssemblyName>MSCO.Server</AssemblyName> <AssemblyName>MSCO.Server</AssemblyName>
<Version>2.6.0</Version>
<Company>MBM</Company>
<Product>MSCO Dedicated Server</Product>
<InvariantGlobalization>true</InvariantGlobalization> <InvariantGlobalization>true</InvariantGlobalization>
<!-- Symbol der .exe (Windows) --> <!-- Symbol der .exe (Windows) -->
<ApplicationIcon>logo.ico</ApplicationIcon> <ApplicationIcon>logo.ico</ApplicationIcon>
+1 -1
View File
@@ -65,7 +65,7 @@ if (Array.IndexOf(args, "--import-save") >= 0)
if (File.Exists(world)) File.Delete(world); if (File.Exists(world)) File.Delete(world);
} }
// Lizenzschlüssel prüfen (wie beim FiveM-Keymaster) // Lizenzschlüssel beim Keymaster prüfen
if (string.IsNullOrWhiteSpace(config.LicenseKey)) if (string.IsNullOrWhiteSpace(config.LicenseKey))
{ {
GameServer.Log("No license key set. Get one at " + MasterServer.Url + "/keys and put it into server.json."); GameServer.Log("No license key set. Get one at " + MasterServer.Url + "/keys and put it into server.json.");
+2 -1
View File
@@ -74,7 +74,8 @@ public static class Wire
/// <summary>TCP-Verbindung zu einem Spieler, gleiches Format wie in der Mod: [int Länge][byte Typ][Payload].</summary> /// <summary>TCP-Verbindung zu einem Spieler, gleiches Format wie in der Mod: [int Länge][byte Typ][Payload].</summary>
public sealed class Connection public sealed class Connection
{ {
const int MaxFrame = 32 * 1024 * 1024; // Spieler schicken nur kleine Pakete (Positionen, Aktionen); der Spielstand geht nur vom Server zum Spieler
const int MaxFrame = 1024 * 1024;
public bool IsClosed { get; private set; } public bool IsClosed { get; private set; }
public string CloseReason { get; private set; } public string CloseReason { get; private set; }
+40 -1
View File
@@ -9,7 +9,7 @@ und Windows.
## Voraussetzungen ## Voraussetzungen
- Ein **Lizenzschlüssel** von https://msco.mbm.wtf/keys (Login mit Steam, wie der FiveM-Keymaster). - Ein **Lizenzschlüssel** von https://msco.mbm.wtf/keys (Login mit Steam).
Der Server prüft ihn beim Start und meldet sich danach regelmäßig beim Master für die Serverliste. Der Server prüft ihn beim Start und meldet sich danach regelmäßig beim Master für die Serverliste.
- Ein My-Summer-Car-Spielstand als Startwelt. - Ein My-Summer-Car-Spielstand als Startwelt.
- Port **7777/TCP** im Router/Firewall freigegeben (sonst wird der Server nicht gelistet). - Port **7777/TCP** im Router/Firewall freigegeben (sonst wird der Server nicht gelistet).
@@ -122,11 +122,50 @@ Der Chat landet auch im Server-Log.
Beim Beitreten: Spielstand laden → Objekte erfassen → Weltzustand anwenden → an der letzten eigenen Position weiterspielen. Beim Beitreten: Spielstand laden → Objekte erfassen → Weltzustand anwenden → an der letzten eigenen Position weiterspielen.
## Betrieb in Produktion
**Checkliste**
- [ ] `LicenseKey` gesetzt, Server startet ohne Fehler (`docker logs msco-server`)
- [ ] Nur Port **7777/TCP** nach außen offen; der Server braucht sonst nur ausgehend HTTPS zu `msco.mbm.wtf`
- [ ] Log meldet keine Warnung „NOT reachable from the internet“ (sonst erscheint der Server nicht in der Liste)
- [ ] Steam-Server (`RequireSteam: true`): `SteamWebApiKey` eintragen, damit Anmeldetickets wirklich bei Steam geprüft werden
- [ ] Mindestens eine SteamID in `Admins`
- [ ] Backups von `data/` eingerichtet (siehe unten)
**Neustarts:** Docker startet den Server nach einem Absturz neu (`restart: on-failure`). Bei Konfigurationsfehlern
(kein/ungültiger Schlüssel, kein Spielstand) beendet er sich mit Code 1 und wird nach 3 Versuchen nicht mehr
gestartet – dann ins Log schauen. `docker compose stop` / `systemctl stop` speichern vorher den Weltzustand.
**Logs:** werden von Docker auf 3 × 10 MB begrenzt. Chat, Beitritte, Kicks/Bans und Admin-Aktionen stehen darin.
**Backups:** Alles Wichtige liegt in `data/` (`server.json`, `save/`, `world.json`). Den Server dafür nicht
anhalten – der Weltzustand wird atomar geschrieben. Beispiel (täglich per cron):
```bash
tar czf /backup/msco-$(date +%F).tar.gz -C /pfad/zu/MSCO-Dedicated-Server data
```
**Updates:** Server und Mod müssen dieselbe Version haben. Nach einem neuen Release:
```bash
git pull
docker compose up -d --build
```
Spieler mit einer anderen Mod-Version bekommen beim Verbinden eine Warnung.
**Weltzustand zurücksetzen:** Wird `world.json` mit der Zeit sehr groß oder macht Probleme beim Beitreten,
einen aktuellen Spielstand nach `data/save/` legen und `msco reset-world` ausführen.
## Sicherheit ## Sicherheit
- Die Adresse des Masters (`https://msco.mbm.wtf`) ist fest eingebaut, damit niemand die Schlüsselprüfung mit - Die Adresse des Masters (`https://msco.mbm.wtf`) ist fest eingebaut, damit niemand die Schlüsselprüfung mit
einem eigenen Master umgehen kann. Nur Debug-Builds akzeptieren `MSCO_MASTER_URL`. einem eigenen Master umgehen kann. Nur Debug-Builds akzeptieren `MSCO_MASTER_URL`.
- `data/` enthält Lizenzschlüssel und API-Keys – nicht veröffentlichen (steht in `.gitignore`). - `data/` enthält Lizenzschlüssel und API-Keys – nicht veröffentlichen (steht in `.gitignore`).
- Der Container läuft nicht als root. Spielerdaten (Nachrichten, Objektzustände) werden in der Größe begrenzt,
unbekannte oder zu große Nachrichten verworfen.
- Gebannte Spieler werden über ihre (bei Steam geprüfte) SteamID erkannt. Auf Non-Steam-Servern gibt es keine
verlässliche Identität – dort sind Bans nur über den Namen möglich.
## Lizenz ## Lizenz
+12 -1
View File
@@ -79,7 +79,11 @@ public sealed class WorldState
void ReadPose(BinaryReader r) void ReadPose(BinaryReader r)
{ {
int id = r.ReadInt32(); int id = r.ReadInt32();
if (!Objects.TryGetValue(id, out var pose)) Objects[id] = pose = new ObjectPose(); if (!Objects.TryGetValue(id, out var pose))
{
pose = new ObjectPose(); // bei voller Liste nur mitlesen, nicht speichern
if (Objects.Count < MaxObjects) Objects[id] = pose;
}
for (int k = 0; k < 3; k++) pose.P[k] = r.ReadSingle(); for (int k = 0; k < 3; k++) pose.P[k] = r.ReadSingle();
for (int k = 0; k < 4; k++) pose.R[k] = r.ReadSingle(); for (int k = 0; k < 4; k++) pose.R[k] = r.ReadSingle();
r.ReadSingle(); r.ReadSingle(); r.ReadSingle(); // Geschwindigkeit wird nicht gespeichert r.ReadSingle(); r.ReadSingle(); r.ReadSingle(); // Geschwindigkeit wird nicht gespeichert
@@ -113,13 +117,20 @@ public sealed class WorldState
var all = new byte[value.Length + 1]; var all = new byte[value.Length + 1];
all[0] = type; all[0] = type;
value.CopyTo(all, 1); value.CopyTo(all, 1);
if (Vars.Count >= MaxVars && !Vars.ContainsKey(key)) continue;
Vars[key] = Convert.ToBase64String(all); Vars[key] = Convert.ToBase64String(all);
} }
Dirty = true; Dirty = true;
} }
// Obergrenzen, damit ein fehlerhafter oder manipulierter Client world.json nicht endlos aufbläht
public const int MaxEvents = 50_000;
public const int MaxVars = 50_000;
public const int MaxObjects = 20_000;
public void AddFsmEvent(byte[] payload) public void AddFsmEvent(byte[] payload)
{ {
if (Events.Count >= MaxEvents) return;
Events.Add(Convert.ToBase64String(payload)); Events.Add(Convert.ToBase64String(payload));
Dirty = true; Dirty = true;
} }
+5
View File
@@ -21,6 +21,11 @@ services:
stdin_open: true # Server-Konsole per docker attach stdin_open: true # Server-Konsole per docker attach
tty: true tty: true
stop_grace_period: 30s stop_grace_period: 30s
logging: # Logs begrenzen, damit sie im Dauerbetrieb nicht die Platte füllen
driver: json-file
options:
max-size: "10m"
max-file: "3"
ports: ports:
- "7777:7777/tcp" - "7777:7777/tcp"
volumes: volumes: