diff --git a/Content.Server/Connection/ConnectionManager.cs b/Content.Server/Connection/ConnectionManager.cs index bc3ec78a32..a39c028d8e 100644 --- a/Content.Server/Connection/ConnectionManager.cs +++ b/Content.Server/Connection/ConnectionManager.cs @@ -6,6 +6,8 @@ using System.Runtime.InteropServices; using Content.Server.Administration.Managers; using Content.Server.Chat.Managers; using Content.Server.Connection.IPIntel; +using Content.Server.Connection.IPBlocking; +using Content.Shared.Connection.IPBlocking; using Content.Server.Database; using Content.Server.GameTicking; using Content.Server.Preferences.Managers; @@ -59,6 +61,7 @@ namespace Content.Server.Connection private readonly Dictionary _temporaryBypasses = []; private readonly Dictionary _temporaryConnectionAllowed = []; private IPIntel.IPIntel _ipintel = default!; + private IPBlockingSystem _ipBlockingSystem = default!; public event EventHandler? PlayerConnectingWithBan; @@ -73,6 +76,14 @@ namespace Content.Server.Connection _ipintel = new IPIntel.IPIntel(new IPIntelApi(_http, _cfg), _db, _cfg, _logManager, _chatManager, _gameTiming); + // Инициализация системы блокировки IP + _ipBlockingSystem = new IPBlockingSystem(); + IoCManager.Instance!.InjectDependencies(_ipBlockingSystem); + _ipBlockingSystem.Initialize(); + + // Регистрация в IoC для использования в других системах + IoCManager.Instance.RegisterInstance(_ipBlockingSystem, true); + IoCManager.Instance!.TryResolveType(out _sponsorsMgr); _netMgr.Connecting += NetMgrOnConnecting; _netMgr.AssignUserIdCallback = AssignUserIdCallback; @@ -124,6 +135,16 @@ namespace Content.Server.Connection { _sawmill.Error("IPIntel update failed:" + e); } + + // Периодическая очистка истекших блокировок IP + try + { + _ipBlockingSystem.Update(); + } + catch (Exception e) + { + _sawmill.Error("IPBlockingSystem update failed:" + e); + } } private async Task NetMgrOnConnecting(NetConnectingArgs e) diff --git a/Content.Server/Connection/IPBlocking/IPBlockingSystem.cs b/Content.Server/Connection/IPBlocking/IPBlockingSystem.cs new file mode 100644 index 0000000000..e9bce2d4b1 --- /dev/null +++ b/Content.Server/Connection/IPBlocking/IPBlockingSystem.cs @@ -0,0 +1,156 @@ +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Net; +using Content.Shared.CCVar; +using Robust.Shared.Configuration; +using Robust.Shared.IoC; +using Robust.Shared.Log; +using Robust.Shared.Timing; + +using Content.Shared.Connection.IPBlocking; + +namespace Content.Server.Connection.IPBlocking; + +/// +/// Система блокировки IP-адресов для защиты от перегрузки памяти +/// при получении подозрительных запросов с некорректными длинами ответов. +/// +public sealed class IPBlockingSystem : IIPBlockingSystem +{ + [Dependency] private readonly IConfigurationManager _cfg = default!; + [Dependency] private readonly ILogManager _logManager = default!; + [Dependency] private readonly IGameTiming _gameTiming = default!; + + private readonly ConcurrentDictionary _blockedIPs = new(); + private ISawmill _sawmill = default!; + + private bool _enabled; + private int _blockDurationSeconds; + private int _maxResponseLength; + + public void Initialize() + { + _sawmill = _logManager.GetSawmill("ipblocking"); + + _cfg.OnValueChanged(CCVars.GameIPBlockingEnabled, b => _enabled = b, true); + _cfg.OnValueChanged(CCVars.GameIPBlockingDuration, b => _blockDurationSeconds = b, true); + _cfg.OnValueChanged(CCVars.GameIPBlockingMaxResponseLength, b => _maxResponseLength = b, true); + } + + /// + /// Проверяет, заблокирован ли указанный IP-адрес. + /// + public bool IsBlocked(IPAddress ip) + { + if (!_enabled) + return false; + + if (!_blockedIPs.TryGetValue(ip, out var unblockTime)) + return false; + + // Проверяем, не истекла ли блокировка + if (DateTime.UtcNow >= unblockTime) + { + _blockedIPs.TryRemove(ip, out _); + return false; + } + + return true; + } + + /// + /// Блокирует IP-адрес на указанное время с указанной причиной. + /// + public void BlockIP(IPAddress ip, TimeSpan duration, string reason) + { + if (!_enabled) + return; + + var unblockTime = DateTime.UtcNow + duration; + _blockedIPs.AddOrUpdate(ip, unblockTime, (_, _) => unblockTime); + + _sawmill.Warning($"Заблокирован IP {ip} на {duration.TotalMinutes:F1} минут. Причина: {reason}"); + } + + /// + /// Блокирует IP-адрес на время, указанное в CVar, с указанной причиной. + /// + public void BlockIP(IPAddress ip, string reason) + { + var duration = TimeSpan.FromSeconds(_blockDurationSeconds); + BlockIP(ip, duration, reason); + } + + /// + /// Проверяет длину ответа и блокирует IP при обнаружении подозрительного значения. + /// + /// true, если длина подозрительная и IP был заблокирован + public bool CheckAndBlockSuspiciousLength(IPAddress ip, int length, string context) + { + if (!_enabled) + return false; + + // Проверяем на отрицательные или слишком большие значения + if (length < 0 || length > _maxResponseLength) + { + var reason = $"Подозрительная длина ответа: {length} байт (контекст: {context})"; + BlockIP(ip, reason); + return true; + } + + return false; + } + + /// + /// Получает максимально допустимую длину ответа. + /// + public int GetMaxResponseLength() + { + return _maxResponseLength; + } + + /// + /// Очищает истекшие блокировки. Должен вызываться периодически. + /// + public void Update() + { + if (!_enabled) + return; + + var now = DateTime.UtcNow; + var keysToRemove = new List(); + + foreach (var (ip, unblockTime) in _blockedIPs) + { + if (now >= unblockTime) + { + keysToRemove.Add(ip); + } + } + + foreach (var ip in keysToRemove) + { + _blockedIPs.TryRemove(ip, out _); + } + } + + /// + /// Разблокирует IP-адрес вручную. + /// + public void UnblockIP(IPAddress ip) + { + if (_blockedIPs.TryRemove(ip, out _)) + { + _sawmill.Info($"IP {ip} разблокирован вручную"); + } + } + + /// + /// Получает количество заблокированных IP-адресов. + /// + public int GetBlockedCount() + { + return _blockedIPs.Count; + } +} + diff --git a/Content.Shared/CCVar/CCVars.Game.cs b/Content.Shared/CCVar/CCVars.Game.cs index 8d7e6b94fc..ee9c1651c1 100644 --- a/Content.Shared/CCVar/CCVars.Game.cs +++ b/Content.Shared/CCVar/CCVars.Game.cs @@ -310,6 +310,24 @@ public sealed partial class CCVars public static readonly CVarDef GameIPIntelAlertAdminWarnRating = CVarDef.Create("game.ipintel_alert_admin_warn_rating", 0f, CVar.SERVERONLY); + /// + /// Включить систему блокировки IP-адресов для защиты от перегрузки памяти. + /// + public static readonly CVarDef GameIPBlockingEnabled = + CVarDef.Create("game.ipblocking_enabled", true, CVar.SERVERONLY); + + /// + /// Время блокировки IP-адреса в секундах при обнаружении подозрительного запроса. + /// + public static readonly CVarDef GameIPBlockingDuration = + CVarDef.Create("game.ipblocking_duration", 900, CVar.SERVERONLY); // 15 минут по умолчанию + + /// + /// Максимальная допустимая длина ответа в байтах. Запросы с большей длиной будут блокироваться. + /// + public static readonly CVarDef GameIPBlockingMaxResponseLength = + CVarDef.Create("game.ipblocking_max_response_length", 10485760, CVar.SERVERONLY); // 10MB по умолчанию + /// /// Make people bonk when trying to climb certain objects like tables. /// diff --git a/Content.Shared/Connection/IPBlocking/IIPBlockingSystem.cs b/Content.Shared/Connection/IPBlocking/IIPBlockingSystem.cs new file mode 100644 index 0000000000..8a8643719b --- /dev/null +++ b/Content.Shared/Connection/IPBlocking/IIPBlockingSystem.cs @@ -0,0 +1,26 @@ +using System.Net; + +namespace Content.Shared.Connection.IPBlocking; + +/// +/// Интерфейс для системы блокировки IP-адресов. +/// +public interface IIPBlockingSystem +{ + /// + /// Проверяет, заблокирован ли указанный IP-адрес. + /// + bool IsBlocked(IPAddress ip); + + /// + /// Проверяет длину ответа и блокирует IP при обнаружении подозрительного значения. + /// + /// true, если длина подозрительная и IP был заблокирован + bool CheckAndBlockSuspiciousLength(IPAddress ip, int length, string context); + + /// + /// Получает максимально допустимую длину ответа. + /// + int GetMaxResponseLength(); +} +