From 9e795a780236583e79d555a5b417bd04a619decc Mon Sep 17 00:00:00 2001 From: MrMaho Date: Fri, 23 Oct 2026 20:12:10 +0200 Subject: [PATCH] 2.10.6: security level 1 - reported in heartbeat, server info and Welcome; log warning when the keymaster marks the server outdated Co-Authored-By: Claude Opus 5.5 --- GameServer.cs | 27 ++++++++++++++++++++++++++- MSCO.Server.csproj | 2 +- MasterClient.cs | 14 +++++++++----- README.md | 4 ++++ Tests/Program.cs | 11 +++++++++++ 5 files changed, 51 insertions(+), 7 deletions(-) diff --git a/GameServer.cs b/GameServer.cs index 30bb8c5..a92f869 100644 --- a/GameServer.cs +++ b/GameServer.cs @@ -15,6 +15,13 @@ public sealed class GameServer /// Netzwerk-Version (= MSCO.ProtocolVersion der Mod). Mods 2.8.x passen dazu (kleine Updates). public const string ModVersion = "2.8"; + /// + /// Sicherheitsstufe dieses Servers - steigt mit jedem Sicherheitsupdate (gleiche Zählung wie MSCO.SecurityLevel in + /// der Mod). Aktuelle Mods treten Servern unter ihrem Minimum nicht bei; der Keymaster markiert sie als veraltet. + /// 1 = Oktober 2026 (Paket-Prüfung, Chat/Admin-Nachrichten, sichere Mod-Downloads). + /// + public const int SecurityLevel = 1; + /// "2.8.3" -> "2.8": kleine Mod-Updates (dritte Stelle) sind kompatibel. public static string Protocol(string version) { @@ -393,7 +400,7 @@ public sealed class GameServer heartbeat = null; OnLicenseResult(result.Status); if (result.Status == 0 || result.Status >= 500) Log("Master server not reachable - server is not listed right now."); - else if (result.Status >= 200 && result.Status < 300) ReportReachability(result); + else if (result.Status >= 200 && result.Status < 300) { ReportReachability(result); ReportSecurity(result); } } if (!offline && now >= nextHeartbeat && heartbeat == null) { @@ -403,6 +410,18 @@ public sealed class GameServer } } + bool? lastOutdated; + + /// Fehlen Sicherheitsupdates (laut Keymaster)? Nur bei Änderung melden. + void ReportSecurity(MasterClient.HeartbeatResult r) + { + if (r.Outdated == lastOutdated) return; + lastOutdated = r.Outdated; + if (r.Outdated) + Log($"!!! This server is OUTDATED: security level {SecurityLevel}, required {r.MinSecurity}. Players with a current MSCO " + + "cannot join until you update (web panel -> Update, or git pull + rebuild)."); + } + /// Erreichbarkeit laut Master nur bei Änderung ausgeben. void ReportReachability(MasterClient.HeartbeatResult r) { @@ -448,6 +467,9 @@ public sealed class GameServer var names = clients.Values.Where(x => x.State == ClientState.Joined).Select(x => x.Name).ToList(); w.Write(names.Count); foreach (string n in names) w.Write(n); + // Erweiterung 2: Programmversion und Sicherheitsstufe (Mods prüfen sie vor dem Beitreten) + w.Write(Updater.CurrentVersion); + w.Write(SecurityLevel); })); c.Conn.CloseAfterSend("Info query"); return; @@ -694,6 +716,9 @@ public sealed class GameServer w.Write(true); // dedizierter Server w.Write(joined.Count); foreach (var p in joined) { w.Write(p.Id); w.Write(p.Name); } + // Sicherheitsstufe und Version (neuere Mods verlassen Server unter ihrem Minimum, ältere lesen nicht weiter) + w.Write(SecurityLevel); + w.Write(Updater.CurrentVersion); })); Broadcast(MsgType.PlayerJoined, Wire.Build(w => { w.Write(c.Id); w.Write(c.Name); }), c.Id); diff --git a/MSCO.Server.csproj b/MSCO.Server.csproj index 3bafd2f..821a822 100644 --- a/MSCO.Server.csproj +++ b/MSCO.Server.csproj @@ -7,7 +7,7 @@ enable MSCO.Server MSCO.Server - 2.10.5 + 2.10.6 MBM MSCO Dedicated Server true diff --git a/MasterClient.cs b/MasterClient.cs index cefa376..51acc4f 100644 --- a/MasterClient.cs +++ b/MasterClient.cs @@ -22,7 +22,8 @@ public sealed class MasterClient public bool Unreachable { get; init; } } record HeartbeatBody(string Key, string Name, string Description, string Address, int Port, int Players, int MaxPlayers, - string Version, bool Public, bool RequireSteam, List Tags, List Mods); + string Version, bool Public, bool RequireSteam, List Tags, List Mods, + string ServerVersion, int Security); /// Mod des Servers für die Serverliste (die Dateien selbst bekommen Spieler direkt vom Server). public sealed record ModInfo(string Id, string Name, string Version); @@ -56,8 +57,9 @@ public sealed class MasterClient /// Antwort auf den Heartbeat. Status: HTTP-Code, 0 = Master nicht erreichbar, 401 = Schlüssel ungültig. /// Reachable: ob der Master den Server von außen erreicht (null = Prüfung läuft noch). /// - public sealed record HeartbeatResult(int Status, bool? Reachable, bool Listed, string Address, int Port); - record HeartbeatReply(bool? Reachable, bool Listed, string Address, int Port); + /// Outdated: dem Server fehlen Sicherheitsupdates (Stufe unter MinSecurity) - neue Spieler treten nicht bei. + public sealed record HeartbeatResult(int Status, bool? Reachable, bool Listed, string Address, int Port, bool Outdated = false, int MinSecurity = 0); + record HeartbeatReply(bool? Reachable, bool Listed, string Address, int Port, bool Outdated, int MinSecurity); public sealed record RemoteCommand(string Id, string Command, string By); public sealed record RemoteResult(string Id, string Output); @@ -95,14 +97,16 @@ public sealed class MasterClient { var body = new HeartbeatBody(config.LicenseKey, config.Name, config.Description, config.PublicAddress, config.Port, players, config.MaxPlayers, version, config.Public, config.RequireSteam, - ServerConfig.NormalizeTags(config.Tags, out _), mods ?? new List()); + ServerConfig.NormalizeTags(config.Tags, out _), mods ?? new List(), + Updater.CurrentVersion, GameServer.SecurityLevel); var response = await http.PostAsJsonAsync(baseUrl + "/api/servers/heartbeat", body); int status = (int)response.StatusCode; if (!response.IsSuccessStatusCode) return new HeartbeatResult(status, null, false, null, 0); HeartbeatReply reply = null; try { reply = await response.Content.ReadFromJsonAsync(); } catch { } - return new HeartbeatResult(status, reply?.Reachable, reply?.Listed ?? false, reply?.Address, reply?.Port ?? 0); + return new HeartbeatResult(status, reply?.Reachable, reply?.Listed ?? false, reply?.Address, reply?.Port ?? 0, + reply?.Outdated ?? false, reply?.MinSecurity ?? 0); } catch { diff --git a/README.md b/README.md index ae7145b..43f9370 100644 --- a/README.md +++ b/README.md @@ -338,6 +338,10 @@ Reset-Punkt. - Spieler-Eingaben werden geprüft: Namen/Chat ohne Steuerzeichen (keine gefälschten Log-Zeilen), Chat im Stil von Server-Meldungen (`* …`), Uhrzeit/Wetter (`msco.*`) und Admin-Einbau von Teilen nur von Admins, ungültige oder absurde Geldänderungen werden verworfen. +- **Sicherheitsstufe:** Jede Server-Version hat eine Sicherheitsstufe (`GameServer.SecurityLevel`, steigt mit jedem + Sicherheitsupdate). Aktuelle MSCO-Mods treten Servern unter ihrem Minimum nicht bei, und die Serverliste zeigt sie + als **VERALTET**. Fehlt deinem Server ein Sicherheitsupdate, steht das im Log/Panel – dann über das Panel + (*Update*) oder `git pull` + Neubau aktualisieren. Das Minimum können MSCO-Admins im Keymaster anheben. ## Lizenz diff --git a/Tests/Program.cs b/Tests/Program.cs index bac3b27..8744024 100644 --- a/Tests/Program.cs +++ b/Tests/Program.cs @@ -53,6 +53,17 @@ try Check(alice.AdminStatus == false, "Alice is not admin"); Check(alice.SaveFiles == 5, "Alice received save files (" + alice.SaveFiles + ")"); Check(alice.GotStartGame, "Alice got StartGame"); + { + // Welcome endet mit Sicherheitsstufe + Version (aktuelle Mods verlassen veraltete Server) + using var welcome = new BinaryReader(new MemoryStream(alice.Welcome)); + welcome.ReadInt32(); welcome.ReadBoolean(); + int others = welcome.ReadInt32(); + for (int i = 0; i < others; i++) { welcome.ReadInt32(); welcome.ReadString(); } + int level = welcome.ReadInt32(); + string serverVersion = welcome.ReadString(); + Check(level == GameServer.SecurityLevel && level >= 1 && serverVersion == Updater.CurrentVersion, + $"Welcome reports security level {level} and server version {serverVersion}"); + } Console.WriteLine("== Ping"); alice.Send(MsgType.Ping, BitConverter.GetBytes(12.5f));