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 <noreply@anthropic.com>
This commit is contained in:
2026-10-23 20:12:10 +02:00
co-authored by Claude Opus 5.5
parent 7d2ce98f1e
commit 9e795a7802
5 changed files with 51 additions and 7 deletions
+26 -1
View File
@@ -15,6 +15,13 @@ public sealed class GameServer
/// <summary>Netzwerk-Version (= MSCO.ProtocolVersion der Mod). Mods 2.8.x passen dazu (kleine Updates).</summary> /// <summary>Netzwerk-Version (= MSCO.ProtocolVersion der Mod). Mods 2.8.x passen dazu (kleine Updates).</summary>
public const string ModVersion = "2.8"; public const string ModVersion = "2.8";
/// <summary>
/// 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).
/// </summary>
public const int SecurityLevel = 1;
/// <summary>"2.8.3" -> "2.8": kleine Mod-Updates (dritte Stelle) sind kompatibel.</summary> /// <summary>"2.8.3" -> "2.8": kleine Mod-Updates (dritte Stelle) sind kompatibel.</summary>
public static string Protocol(string version) public static string Protocol(string version)
{ {
@@ -393,7 +400,7 @@ public sealed class GameServer
heartbeat = null; heartbeat = null;
OnLicenseResult(result.Status); OnLicenseResult(result.Status);
if (result.Status == 0 || result.Status >= 500) Log("Master server not reachable - server is not listed right now."); 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) if (!offline && now >= nextHeartbeat && heartbeat == null)
{ {
@@ -403,6 +410,18 @@ public sealed class GameServer
} }
} }
bool? lastOutdated;
/// <summary>Fehlen Sicherheitsupdates (laut Keymaster)? Nur bei Änderung melden.</summary>
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).");
}
/// <summary>Erreichbarkeit laut Master nur bei Änderung ausgeben.</summary> /// <summary>Erreichbarkeit laut Master nur bei Änderung ausgeben.</summary>
void ReportReachability(MasterClient.HeartbeatResult r) 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(); var names = clients.Values.Where(x => x.State == ClientState.Joined).Select(x => x.Name).ToList();
w.Write(names.Count); w.Write(names.Count);
foreach (string n in names) w.Write(n); 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"); c.Conn.CloseAfterSend("Info query");
return; return;
@@ -694,6 +716,9 @@ public sealed class GameServer
w.Write(true); // dedizierter Server w.Write(true); // dedizierter Server
w.Write(joined.Count); w.Write(joined.Count);
foreach (var p in joined) { w.Write(p.Id); w.Write(p.Name); } 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); Broadcast(MsgType.PlayerJoined, Wire.Build(w => { w.Write(c.Id); w.Write(c.Name); }), c.Id);
+1 -1
View File
@@ -7,7 +7,7 @@
<ImplicitUsings>enable</ImplicitUsings> <ImplicitUsings>enable</ImplicitUsings>
<RootNamespace>MSCO.Server</RootNamespace> <RootNamespace>MSCO.Server</RootNamespace>
<AssemblyName>MSCO.Server</AssemblyName> <AssemblyName>MSCO.Server</AssemblyName>
<Version>2.10.5</Version> <Version>2.10.6</Version>
<Company>MBM</Company> <Company>MBM</Company>
<Product>MSCO Dedicated Server</Product> <Product>MSCO Dedicated Server</Product>
<InvariantGlobalization>true</InvariantGlobalization> <InvariantGlobalization>true</InvariantGlobalization>
+9 -5
View File
@@ -22,7 +22,8 @@ public sealed class MasterClient
public bool Unreachable { get; init; } public bool Unreachable { get; init; }
} }
record HeartbeatBody(string Key, string Name, string Description, string Address, int Port, int Players, int MaxPlayers, record HeartbeatBody(string Key, string Name, string Description, string Address, int Port, int Players, int MaxPlayers,
string Version, bool Public, bool RequireSteam, List<string> Tags, List<ModInfo> Mods); string Version, bool Public, bool RequireSteam, List<string> Tags, List<ModInfo> Mods,
string ServerVersion, int Security);
/// <summary>Mod des Servers für die Serverliste (die Dateien selbst bekommen Spieler direkt vom Server).</summary> /// <summary>Mod des Servers für die Serverliste (die Dateien selbst bekommen Spieler direkt vom Server).</summary>
public sealed record ModInfo(string Id, string Name, string Version); 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. /// 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). /// Reachable: ob der Master den Server von außen erreicht (null = Prüfung läuft noch).
/// </summary> /// </summary>
public sealed record HeartbeatResult(int Status, bool? Reachable, bool Listed, string Address, int Port); /// <summary>Outdated: dem Server fehlen Sicherheitsupdates (Stufe unter MinSecurity) - neue Spieler treten nicht bei.</summary>
record HeartbeatReply(bool? Reachable, bool Listed, string Address, int Port); 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 RemoteCommand(string Id, string Command, string By);
public sealed record RemoteResult(string Id, string Output); 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, var body = new HeartbeatBody(config.LicenseKey, config.Name, config.Description, config.PublicAddress, config.Port,
players, config.MaxPlayers, version, config.Public, config.RequireSteam, players, config.MaxPlayers, version, config.Public, config.RequireSteam,
ServerConfig.NormalizeTags(config.Tags, out _), mods ?? new List<ModInfo>()); ServerConfig.NormalizeTags(config.Tags, out _), mods ?? new List<ModInfo>(),
Updater.CurrentVersion, GameServer.SecurityLevel);
var response = await http.PostAsJsonAsync(baseUrl + "/api/servers/heartbeat", body); var response = await http.PostAsJsonAsync(baseUrl + "/api/servers/heartbeat", body);
int status = (int)response.StatusCode; int status = (int)response.StatusCode;
if (!response.IsSuccessStatusCode) return new HeartbeatResult(status, null, false, null, 0); if (!response.IsSuccessStatusCode) return new HeartbeatResult(status, null, false, null, 0);
HeartbeatReply reply = null; HeartbeatReply reply = null;
try { reply = await response.Content.ReadFromJsonAsync<HeartbeatReply>(); } catch { } try { reply = await response.Content.ReadFromJsonAsync<HeartbeatReply>(); } 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 catch
{ {
+4
View File
@@ -338,6 +338,10 @@ Reset-Punkt.
- Spieler-Eingaben werden geprüft: Namen/Chat ohne Steuerzeichen (keine gefälschten Log-Zeilen), Chat im Stil von - 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 Server-Meldungen (`* …`), Uhrzeit/Wetter (`msco.*`) und Admin-Einbau von Teilen nur von Admins, ungültige oder
absurde Geldänderungen werden verworfen. 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 ## Lizenz
+11
View File
@@ -53,6 +53,17 @@ try
Check(alice.AdminStatus == false, "Alice is not admin"); Check(alice.AdminStatus == false, "Alice is not admin");
Check(alice.SaveFiles == 5, "Alice received save files (" + alice.SaveFiles + ")"); Check(alice.SaveFiles == 5, "Alice received save files (" + alice.SaveFiles + ")");
Check(alice.GotStartGame, "Alice got StartGame"); 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"); Console.WriteLine("== Ping");
alice.Send(MsgType.Ping, BitConverter.GetBytes(12.5f)); alice.Send(MsgType.Ping, BitConverter.GetBytes(12.5f));