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:
+26
-1
@@ -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
@@ -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
@@ -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
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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));
|
||||||
|
|||||||
Reference in New Issue
Block a user