diff --git a/Content.Server/Connection/ConnectionManager.cs b/Content.Server/Connection/ConnectionManager.cs
index 1367cae82c..cd89f48d49 100644
--- a/Content.Server/Connection/ConnectionManager.cs
+++ b/Content.Server/Connection/ConnectionManager.cs
@@ -1,4 +1,5 @@
using System.Collections.Immutable;
+using System.Runtime.InteropServices;
using System.Text.Json.Nodes;
using System.Threading.Tasks;
using Content.Server.Database;
@@ -10,6 +11,7 @@ using Content.Shared.Players.PlayTimeTracking;
using Robust.Server.Player;
using Robust.Shared.Configuration;
using Robust.Shared.Network;
+using Robust.Shared.Timing;
namespace Content.Server.Connection
@@ -17,6 +19,18 @@ namespace Content.Server.Connection
public interface IConnectionManager
{
void Initialize();
+
+ ///
+ /// Temporarily allow a user to bypass regular connection requirements.
+ ///
+ ///
+ /// The specified user will be allowed to bypass regular player cap,
+ /// whitelist and panic bunker restrictions for .
+ /// Bans are not bypassed.
+ ///
+ /// The user to give a temporary bypass.
+ /// How long the bypass should last for.
+ void AddTemporaryConnectBypass(NetUserId user, TimeSpan duration);
}
///
@@ -31,15 +45,31 @@ namespace Content.Server.Connection
[Dependency] private readonly IConfigurationManager _cfg = default!;
[Dependency] private readonly ILocalizationManager _loc = default!;
[Dependency] private readonly ServerDbEntryManager _serverDbEntry = default!;
+ [Dependency] private readonly IGameTiming _gameTiming = default!;
+ [Dependency] private readonly ILogManager _logManager = default!;
+
+ private readonly Dictionary _temporaryBypasses = [];
+ private ISawmill _sawmill = default!;
public void Initialize()
{
+ _sawmill = _logManager.GetSawmill("connections");
+
_netMgr.Connecting += NetMgrOnConnecting;
_netMgr.AssignUserIdCallback = AssignUserIdCallback;
// Approval-based IP bans disabled because they don't play well with Happy Eyeballs.
// _netMgr.HandleApprovalCallback = HandleApproval;
}
+ public void AddTemporaryConnectBypass(NetUserId user, TimeSpan duration)
+ {
+ ref var time = ref CollectionsMarshal.GetValueRefOrAddDefault(_temporaryBypasses, user, out _);
+ var newTime = _gameTiming.RealTime + duration;
+ // Make sure we only update the time if we wouldn't shrink it.
+ if (newTime > time)
+ time = newTime;
+ }
+
/*
private async Task HandleApproval(NetApprovalEventArgs eventArgs)
{
@@ -109,6 +139,20 @@ namespace Content.Server.Connection
hwId = null;
}
+ var bans = await _db.GetServerBansAsync(addr, userId, hwId, includeUnbanned: false);
+ if (bans.Count > 0)
+ {
+ var firstBan = bans[0];
+ var message = firstBan.FormatBanMessage(_cfg, _loc);
+ return (ConnectionDenyReason.Ban, message, bans);
+ }
+
+ if (HasTemporaryBypass(userId))
+ {
+ _sawmill.Verbose("User {UserId} has temporary bypass, skipping further connection checks", userId);
+ return null;
+ }
+
var adminData = await _dbManager.GetAdminDataForAsync(e.UserId);
if (_cfg.GetCVar(CCVars.PanicBunkerEnabled) && adminData == null)
@@ -167,14 +211,6 @@ namespace Content.Server.Connection
return (ConnectionDenyReason.Full, Loc.GetString("soft-player-cap-full"), null);
}
- var bans = await _db.GetServerBansAsync(addr, userId, hwId, includeUnbanned: false);
- if (bans.Count > 0)
- {
- var firstBan = bans[0];
- var message = firstBan.FormatBanMessage(_cfg, _loc);
- return (ConnectionDenyReason.Ban, message, bans);
- }
-
if (_cfg.GetCVar(CCVars.WhitelistEnabled))
{
var min = _cfg.GetCVar(CCVars.WhitelistMinPlayers);
@@ -195,6 +231,11 @@ namespace Content.Server.Connection
return null;
}
+ private bool HasTemporaryBypass(NetUserId user)
+ {
+ return _temporaryBypasses.TryGetValue(user, out var time) && time > _gameTiming.RealTime;
+ }
+
private async Task AssignUserIdCallback(string name)
{
if (!_cfg.GetCVar(CCVars.GamePersistGuests))
diff --git a/Content.Server/Connection/GrantConnectBypassCommand.cs b/Content.Server/Connection/GrantConnectBypassCommand.cs
new file mode 100644
index 0000000000..e2d0d7338a
--- /dev/null
+++ b/Content.Server/Connection/GrantConnectBypassCommand.cs
@@ -0,0 +1,60 @@
+using Content.Server.Administration;
+using Content.Shared.Administration;
+using Robust.Shared.Console;
+
+namespace Content.Server.Connection;
+
+[AdminCommand(AdminFlags.Admin)]
+public sealed class GrantConnectBypassCommand : LocalizedCommands
+{
+ private static readonly TimeSpan DefaultDuration = TimeSpan.FromHours(1);
+
+ [Dependency] private readonly IPlayerLocator _playerLocator = default!;
+ [Dependency] private readonly IConnectionManager _connectionManager = default!;
+
+ public override string Command => "grant_connect_bypass";
+
+ public override async void Execute(IConsoleShell shell, string argStr, string[] args)
+ {
+ if (args.Length is not (1 or 2))
+ {
+ shell.WriteError(Loc.GetString("cmd-grant_connect_bypass-invalid-args"));
+ return;
+ }
+
+ var argPlayer = args[0];
+ var info = await _playerLocator.LookupIdByNameOrIdAsync(argPlayer);
+ if (info == null)
+ {
+ shell.WriteError(Loc.GetString("cmd-grant_connect_bypass-unknown-user", ("user", argPlayer)));
+ return;
+ }
+
+ var duration = DefaultDuration;
+ if (args.Length > 1)
+ {
+ var argDuration = args[2];
+ if (!uint.TryParse(argDuration, out var minutes))
+ {
+ shell.WriteLine(Loc.GetString("cmd-grant_connect_bypass-invalid-duration", ("duration", argDuration)));
+ return;
+ }
+
+ duration = TimeSpan.FromMinutes(minutes);
+ }
+
+ _connectionManager.AddTemporaryConnectBypass(info.UserId, duration);
+ shell.WriteLine(Loc.GetString("cmd-grant_connect_bypass-success", ("user", argPlayer)));
+ }
+
+ public override CompletionResult GetCompletion(IConsoleShell shell, string[] args)
+ {
+ if (args.Length == 1)
+ return CompletionResult.FromHint(Loc.GetString("cmd-grant_connect_bypass-arg-user"));
+
+ if (args.Length == 2)
+ return CompletionResult.FromHint(Loc.GetString("cmd-grant_connect_bypass-arg-duration"));
+
+ return CompletionResult.Empty;
+ }
+}
diff --git a/Resources/Locale/en-US/administration/commands/connection-commands.ftl b/Resources/Locale/en-US/administration/commands/connection-commands.ftl
new file mode 100644
index 0000000000..66991042d2
--- /dev/null
+++ b/Resources/Locale/en-US/administration/commands/connection-commands.ftl
@@ -0,0 +1,16 @@
+## Strings for the "grant_connect_bypass" command.
+
+cmd-grant_connect_bypass-desc = Temporarily allow a user to bypass regular connection checks.
+cmd-grant_connect_bypass-help = Usage: grant_connect_bypass [duration minutes]
+ Temporarily grants a user the ability to bypass regular connections restrictions.
+ The bypass only applies to this game server and will expire after (by default) 1 hour.
+ They will be able to join regardless of whitelist, panic bunker, or player cap.
+
+cmd-grant_connect_bypass-arg-user =
+cmd-grant_connect_bypass-arg-duration = [duration minutes]
+
+cmd-grant_connect_bypass-invalid-args = Expected 1 or 2 arguments
+cmd-grant_connect_bypass-unknown-user = Unable to find user '{$user}'
+cmd-grant_connect_bypass-invalid-duration = Invalid duration '{$duration}'
+
+cmd-grant_connect_bypass-success = Successfully added bypass for user '{$user}'