Production ready: hardening, limits, docs
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -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
@@ -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
@@ -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; }
|
||||||
|
|||||||
@@ -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
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
Reference in New Issue
Block a user