Add system to kick people if they connect to multiple servers at once. (#34563)
This commit is contained in:
committed by
GitHub
parent
1031d2a6c1
commit
71c9dfc9ea
@@ -1,6 +1,4 @@
|
|||||||
using System.Text.Json;
|
|
||||||
using System.Text.Json.Serialization;
|
using System.Text.Json.Serialization;
|
||||||
using Content.Server.Database;
|
|
||||||
|
|
||||||
namespace Content.Server.Administration.Managers;
|
namespace Content.Server.Administration.Managers;
|
||||||
|
|
||||||
@@ -30,36 +28,15 @@ public sealed partial class BanManager
|
|||||||
private TimeSpan _banNotificationRateLimitStart;
|
private TimeSpan _banNotificationRateLimitStart;
|
||||||
private int _banNotificationRateLimitCount;
|
private int _banNotificationRateLimitCount;
|
||||||
|
|
||||||
private void OnDatabaseNotification(DatabaseNotification notification)
|
private bool OnDatabaseNotificationEarlyFilter()
|
||||||
{
|
{
|
||||||
if (notification.Channel != BanNotificationChannel)
|
|
||||||
return;
|
|
||||||
|
|
||||||
if (notification.Payload == null)
|
|
||||||
{
|
|
||||||
_sawmill.Error("Got ban notification with null payload!");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
BanNotificationData data;
|
|
||||||
try
|
|
||||||
{
|
|
||||||
data = JsonSerializer.Deserialize<BanNotificationData>(notification.Payload)
|
|
||||||
?? throw new JsonException("Content is null");
|
|
||||||
}
|
|
||||||
catch (JsonException e)
|
|
||||||
{
|
|
||||||
_sawmill.Error($"Got invalid JSON in ban notification: {e}");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!CheckBanRateLimit())
|
if (!CheckBanRateLimit())
|
||||||
{
|
{
|
||||||
_sawmill.Verbose("Not processing ban notification due to rate limit");
|
_sawmill.Verbose("Not processing ban notification due to rate limit");
|
||||||
return;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
_taskManager.RunOnMainThread(() => ProcessBanNotification(data));
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async void ProcessBanNotification(BanNotificationData data)
|
private async void ProcessBanNotification(BanNotificationData data)
|
||||||
|
|||||||
@@ -53,7 +53,12 @@ public sealed partial class BanManager : IBanManager, IPostInjectInit
|
|||||||
{
|
{
|
||||||
_netManager.RegisterNetMessage<MsgRoleBans>();
|
_netManager.RegisterNetMessage<MsgRoleBans>();
|
||||||
|
|
||||||
_db.SubscribeToNotifications(OnDatabaseNotification);
|
_db.SubscribeToJsonNotification<BanNotificationData>(
|
||||||
|
_taskManager,
|
||||||
|
_sawmill,
|
||||||
|
BanNotificationChannel,
|
||||||
|
ProcessBanNotification,
|
||||||
|
OnDatabaseNotificationEarlyFilter);
|
||||||
|
|
||||||
_userDbData.AddOnLoadPlayer(CachePlayerData);
|
_userDbData.AddOnLoadPlayer(CachePlayerData);
|
||||||
_userDbData.AddOnPlayerDisconnect(ClearPlayerData);
|
_userDbData.AddOnPlayerDisconnect(ClearPlayerData);
|
||||||
|
|||||||
114
Content.Server/Administration/Managers/MultiServerKickManager.cs
Normal file
114
Content.Server/Administration/Managers/MultiServerKickManager.cs
Normal file
@@ -0,0 +1,114 @@
|
|||||||
|
using System.Text.Json;
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
|
using Content.Server.Database;
|
||||||
|
using Content.Shared.CCVar;
|
||||||
|
using Robust.Server.Player;
|
||||||
|
using Robust.Shared.Asynchronous;
|
||||||
|
using Robust.Shared.Configuration;
|
||||||
|
using Robust.Shared.Enums;
|
||||||
|
using Robust.Shared.Network;
|
||||||
|
using Robust.Shared.Player;
|
||||||
|
|
||||||
|
namespace Content.Server.Administration.Managers;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Handles kicking people that connect to multiple servers on the same DB at once.
|
||||||
|
/// </summary>
|
||||||
|
/// <seealso cref="CCVars.AdminAllowMultiServerPlay"/>
|
||||||
|
public sealed class MultiServerKickManager
|
||||||
|
{
|
||||||
|
public const string NotificationChannel = "multi_server_kick";
|
||||||
|
|
||||||
|
[Dependency] private readonly IPlayerManager _playerManager = null!;
|
||||||
|
[Dependency] private readonly IServerDbManager _dbManager = null!;
|
||||||
|
[Dependency] private readonly ILogManager _logManager = null!;
|
||||||
|
[Dependency] private readonly IConfigurationManager _cfg = null!;
|
||||||
|
[Dependency] private readonly IAdminManager _adminManager = null!;
|
||||||
|
[Dependency] private readonly ITaskManager _taskManager = null!;
|
||||||
|
[Dependency] private readonly IServerNetManager _netManager = null!;
|
||||||
|
[Dependency] private readonly ILocalizationManager _loc = null!;
|
||||||
|
[Dependency] private readonly ServerDbEntryManager _serverDbEntry = null!;
|
||||||
|
|
||||||
|
private ISawmill _sawmill = null!;
|
||||||
|
private bool _allowed;
|
||||||
|
|
||||||
|
public void Initialize()
|
||||||
|
{
|
||||||
|
_sawmill = _logManager.GetSawmill("multi_server_kick");
|
||||||
|
|
||||||
|
_playerManager.PlayerStatusChanged += OnPlayerStatusChanged;
|
||||||
|
_cfg.OnValueChanged(CCVars.AdminAllowMultiServerPlay, b => _allowed = b, true);
|
||||||
|
|
||||||
|
_dbManager.SubscribeToJsonNotification<NotificationData>(
|
||||||
|
_taskManager,
|
||||||
|
_sawmill,
|
||||||
|
NotificationChannel,
|
||||||
|
OnNotification,
|
||||||
|
OnNotificationEarlyFilter
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ReSharper disable once AsyncVoidMethod
|
||||||
|
private async void OnPlayerStatusChanged(object? sender, SessionStatusEventArgs e)
|
||||||
|
{
|
||||||
|
if (_allowed)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (e.NewStatus != SessionStatus.InGame)
|
||||||
|
return;
|
||||||
|
|
||||||
|
// Send notification to other servers so they can kick this player that just connected.
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await _dbManager.SendNotification(new DatabaseNotification
|
||||||
|
{
|
||||||
|
Channel = NotificationChannel,
|
||||||
|
Payload = JsonSerializer.Serialize(new NotificationData
|
||||||
|
{
|
||||||
|
PlayerId = e.Session.UserId,
|
||||||
|
ServerId = (await _serverDbEntry.ServerEntity).Id,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_sawmill.Error($"Failed to send notification for multi server kick: {ex}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool OnNotificationEarlyFilter()
|
||||||
|
{
|
||||||
|
if (_allowed)
|
||||||
|
{
|
||||||
|
_sawmill.Verbose("Received notification for player join, but multi server play is allowed on this server. Ignoring");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ReSharper disable once AsyncVoidMethod
|
||||||
|
private async void OnNotification(NotificationData notification)
|
||||||
|
{
|
||||||
|
if (!_playerManager.TryGetSessionById(new NetUserId(notification.PlayerId), out var player))
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (notification.ServerId == (await _serverDbEntry.ServerEntity).Id)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (_adminManager.IsAdmin(player, includeDeAdmin: true))
|
||||||
|
return;
|
||||||
|
|
||||||
|
_sawmill.Info($"Kicking {player} for connecting to another server. Multi-server play is not allowed.");
|
||||||
|
_netManager.DisconnectChannel(player.Channel, _loc.GetString("multi-server-kick-reason"));
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed class NotificationData
|
||||||
|
{
|
||||||
|
[JsonPropertyName("player_id")]
|
||||||
|
public Guid PlayerId { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("server_id")]
|
||||||
|
public int ServerId { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1801,6 +1801,8 @@ INSERT INTO player_round (players_id, rounds_id) VALUES ({players[player]}, {id}
|
|||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
|
public abstract Task SendNotification(DatabaseNotification notification);
|
||||||
|
|
||||||
// SQLite returns DateTime as Kind=Unspecified, Npgsql actually knows for sure it's Kind=Utc.
|
// SQLite returns DateTime as Kind=Unspecified, Npgsql actually knows for sure it's Kind=Utc.
|
||||||
// Normalize DateTimes here so they're always Utc. Thanks.
|
// Normalize DateTimes here so they're always Utc. Thanks.
|
||||||
protected abstract DateTime NormalizeDatabaseTime(DateTime time);
|
protected abstract DateTime NormalizeDatabaseTime(DateTime time);
|
||||||
|
|||||||
@@ -350,6 +350,15 @@ namespace Content.Server.Database
|
|||||||
/// <param name="notification">The notification to trigger</param>
|
/// <param name="notification">The notification to trigger</param>
|
||||||
void InjectTestNotification(DatabaseNotification notification);
|
void InjectTestNotification(DatabaseNotification notification);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Send a notification to all other servers connected to the same database.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// The local server will receive the sent notification itself again.
|
||||||
|
/// </remarks>
|
||||||
|
/// <param name="notification">The notification to send.</param>
|
||||||
|
Task SendNotification(DatabaseNotification notification);
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1045,6 +1054,12 @@ namespace Content.Server.Database
|
|||||||
HandleDatabaseNotification(notification);
|
HandleDatabaseNotification(notification);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Task SendNotification(DatabaseNotification notification)
|
||||||
|
{
|
||||||
|
DbWriteOpsMetric.Inc();
|
||||||
|
return RunDbCommand(() => _db.SendNotification(notification));
|
||||||
|
}
|
||||||
|
|
||||||
private async void HandleDatabaseNotification(DatabaseNotification notification)
|
private async void HandleDatabaseNotification(DatabaseNotification notification)
|
||||||
{
|
{
|
||||||
lock (_notificationHandlers)
|
lock (_notificationHandlers)
|
||||||
|
|||||||
76
Content.Server/Database/ServerDbManagerExt.cs
Normal file
76
Content.Server/Database/ServerDbManagerExt.cs
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
using System.Text.Json;
|
||||||
|
using Robust.Shared.Asynchronous;
|
||||||
|
|
||||||
|
namespace Content.Server.Database;
|
||||||
|
|
||||||
|
public static class ServerDbManagerExt
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Subscribe to a database notification on a specific channel, formatted as JSON.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="dbManager">The database manager to subscribe on.</param>
|
||||||
|
/// <param name="taskManager">The task manager used to run the main callback on the main thread.</param>
|
||||||
|
/// <param name="sawmill">Sawmill to log any errors to.</param>
|
||||||
|
/// <param name="channel">
|
||||||
|
/// The notification channel to listen on. Only notifications on this channel will be handled.
|
||||||
|
/// </param>
|
||||||
|
/// <param name="action">
|
||||||
|
/// The action to run on the notification data.
|
||||||
|
/// This runs on the main thread.
|
||||||
|
/// </param>
|
||||||
|
/// <param name="earlyFilter">
|
||||||
|
/// An early filter callback that runs before the JSON message is deserialized.
|
||||||
|
/// Return false to not handle the notification.
|
||||||
|
/// This does not run on the main thread.
|
||||||
|
/// </param>
|
||||||
|
/// <param name="filter">
|
||||||
|
/// A filter callback that runs after the JSON message is deserialized.
|
||||||
|
/// Return false to not handle the notification.
|
||||||
|
/// This does not run on the main thread.
|
||||||
|
/// </param>
|
||||||
|
/// <typeparam name="TData">The type of JSON data to deserialize.</typeparam>
|
||||||
|
public static void SubscribeToJsonNotification<TData>(
|
||||||
|
this IServerDbManager dbManager,
|
||||||
|
ITaskManager taskManager,
|
||||||
|
ISawmill sawmill,
|
||||||
|
string channel,
|
||||||
|
Action<TData> action,
|
||||||
|
Func<bool>? earlyFilter = null,
|
||||||
|
Func<TData, bool>? filter = null)
|
||||||
|
{
|
||||||
|
dbManager.SubscribeToNotifications(notification =>
|
||||||
|
{
|
||||||
|
if (notification.Channel != channel)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (notification.Payload == null)
|
||||||
|
{
|
||||||
|
sawmill.Error($"Got {channel} notification with null payload!");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (earlyFilter != null && !earlyFilter())
|
||||||
|
return;
|
||||||
|
|
||||||
|
TData data;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
data = JsonSerializer.Deserialize<TData>(notification.Payload)
|
||||||
|
?? throw new JsonException("Content is null");
|
||||||
|
}
|
||||||
|
catch (JsonException e)
|
||||||
|
{
|
||||||
|
sawmill.Error($"Got invalid JSON in {channel} notification: {e}");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (filter != null && !filter(data))
|
||||||
|
return;
|
||||||
|
|
||||||
|
taskManager.RunOnMainThread(() =>
|
||||||
|
{
|
||||||
|
action(data);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,6 +2,7 @@
|
|||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Content.Server.Administration.Managers;
|
using Content.Server.Administration.Managers;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Npgsql;
|
using Npgsql;
|
||||||
|
|
||||||
namespace Content.Server.Database;
|
namespace Content.Server.Database;
|
||||||
@@ -17,6 +18,7 @@ public sealed partial class ServerDbPostgres
|
|||||||
private static readonly string[] NotificationChannels =
|
private static readonly string[] NotificationChannels =
|
||||||
[
|
[
|
||||||
BanManager.BanNotificationChannel,
|
BanManager.BanNotificationChannel,
|
||||||
|
MultiServerKickManager.NotificationChannel,
|
||||||
];
|
];
|
||||||
|
|
||||||
private static readonly TimeSpan ReconnectWaitIncrease = TimeSpan.FromSeconds(10);
|
private static readonly TimeSpan ReconnectWaitIncrease = TimeSpan.FromSeconds(10);
|
||||||
@@ -111,6 +113,14 @@ public sealed partial class ServerDbPostgres
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public override async Task SendNotification(DatabaseNotification notification)
|
||||||
|
{
|
||||||
|
await using var db = await GetDbImpl();
|
||||||
|
|
||||||
|
await db.PgDbContext.Database.ExecuteSqlAsync(
|
||||||
|
$"SELECT pg_notify({notification.Channel}, {notification.Payload})");
|
||||||
|
}
|
||||||
|
|
||||||
public override void Shutdown()
|
public override void Shutdown()
|
||||||
{
|
{
|
||||||
_notificationTokenSource.Cancel();
|
_notificationTokenSource.Cancel();
|
||||||
|
|||||||
@@ -537,6 +537,12 @@ namespace Content.Server.Database
|
|||||||
return await base.AddAdminMessage(message);
|
return await base.AddAdminMessage(message);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public override Task SendNotification(DatabaseNotification notification)
|
||||||
|
{
|
||||||
|
// Notifications not implemented on SQLite.
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
protected override DateTime NormalizeDatabaseTime(DateTime time)
|
protected override DateTime NormalizeDatabaseTime(DateTime time)
|
||||||
{
|
{
|
||||||
DebugTools.Assert(time.Kind == DateTimeKind.Unspecified);
|
DebugTools.Assert(time.Kind == DateTimeKind.Unspecified);
|
||||||
|
|||||||
@@ -152,6 +152,7 @@ namespace Content.Server.Entry
|
|||||||
IoCManager.Resolve<IEntitySystemManager>().GetEntitySystem<GameTicker>().PostInitialize();
|
IoCManager.Resolve<IEntitySystemManager>().GetEntitySystem<GameTicker>().PostInitialize();
|
||||||
IoCManager.Resolve<IBanManager>().Initialize();
|
IoCManager.Resolve<IBanManager>().Initialize();
|
||||||
IoCManager.Resolve<IConnectionManager>().PostInit();
|
IoCManager.Resolve<IConnectionManager>().PostInit();
|
||||||
|
IoCManager.Resolve<MultiServerKickManager>().Initialize();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -75,6 +75,7 @@ namespace Content.Server.IoC
|
|||||||
IoCManager.Register<MappingManager>();
|
IoCManager.Register<MappingManager>();
|
||||||
IoCManager.Register<IWatchlistWebhookManager, WatchlistWebhookManager>();
|
IoCManager.Register<IWatchlistWebhookManager, WatchlistWebhookManager>();
|
||||||
IoCManager.Register<ConnectionManager>();
|
IoCManager.Register<ConnectionManager>();
|
||||||
|
IoCManager.Register<MultiServerKickManager>();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -176,4 +176,11 @@ public sealed partial class CCVars
|
|||||||
|
|
||||||
public static readonly CVarDef<bool> BanHardwareIds =
|
public static readonly CVarDef<bool> BanHardwareIds =
|
||||||
CVarDef.Create("ban.hardware_ids", true, CVar.SERVERONLY);
|
CVarDef.Create("ban.hardware_ids", true, CVar.SERVERONLY);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// If true, players are allowed to connect to multiple game servers at once.
|
||||||
|
/// If false, they will be kicked from the first when connecting to another.
|
||||||
|
/// </summary>
|
||||||
|
public static readonly CVarDef<bool> AdminAllowMultiServerPlay =
|
||||||
|
CVarDef.Create("admin.allow_multi_server_play", true, CVar.SERVERONLY);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
multi-server-kick-reason = Connected to different server in this community.
|
||||||
Reference in New Issue
Block a user