ConnectionManager.cs 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372
  1. using System.Collections.Immutable;
  2. using System.Linq;
  3. using System.Threading.Tasks;
  4. using System.Runtime.InteropServices;
  5. using Content.Server.Administration.Managers;
  6. using Content.Server.Chat.Managers;
  7. using Content.Server.Connection.IPIntel;
  8. using Content.Server.Database;
  9. using Content.Server.GameTicking;
  10. using Content.Server.Preferences.Managers;
  11. using Content.Shared.CCVar;
  12. using Content.Shared.GameTicking;
  13. using Content.Shared.Players.PlayTimeTracking;
  14. using Robust.Server.Player;
  15. using Robust.Shared.Configuration;
  16. using Robust.Shared.Enums;
  17. using Robust.Shared.Network;
  18. using Robust.Shared.Prototypes;
  19. using Robust.Shared.Player;
  20. using Robust.Shared.Timing;
  21. /*
  22. * TODO: Remove baby jail code once a more mature gateway process is established. This code is only being issued as a stopgap to help with potential tiding in the immediate future.
  23. */
  24. namespace Content.Server.Connection
  25. {
  26. public interface IConnectionManager
  27. {
  28. void Initialize();
  29. void PostInit();
  30. /// <summary>
  31. /// Temporarily allow a user to bypass regular connection requirements.
  32. /// </summary>
  33. /// <remarks>
  34. /// The specified user will be allowed to bypass regular player cap,
  35. /// whitelist and panic bunker restrictions for <paramref name="duration"/>.
  36. /// Bans are not bypassed.
  37. /// </remarks>
  38. /// <param name="user">The user to give a temporary bypass.</param>
  39. /// <param name="duration">How long the bypass should last for.</param>
  40. void AddTemporaryConnectBypass(NetUserId user, TimeSpan duration);
  41. void Update();
  42. }
  43. /// <summary>
  44. /// Handles various duties like guest username assignment, bans, connection logs, etc...
  45. /// </summary>
  46. public sealed partial class ConnectionManager : IConnectionManager
  47. {
  48. [Dependency] private readonly IPlayerManager _plyMgr = default!;
  49. [Dependency] private readonly IServerNetManager _netMgr = default!;
  50. [Dependency] private readonly IServerDbManager _db = default!;
  51. [Dependency] private readonly IConfigurationManager _cfg = default!;
  52. [Dependency] private readonly ILocalizationManager _loc = default!;
  53. [Dependency] private readonly ServerDbEntryManager _serverDbEntry = default!;
  54. [Dependency] private readonly IPrototypeManager _prototypeManager = default!;
  55. [Dependency] private readonly IGameTiming _gameTiming = default!;
  56. [Dependency] private readonly ILogManager _logManager = default!;
  57. [Dependency] private readonly IChatManager _chatManager = default!;
  58. [Dependency] private readonly IHttpClientHolder _http = default!;
  59. [Dependency] private readonly IAdminManager _adminManager = default!;
  60. private ISawmill _sawmill = default!;
  61. private readonly Dictionary<NetUserId, TimeSpan> _temporaryBypasses = [];
  62. private IPIntel.IPIntel _ipintel = default!;
  63. public void PostInit()
  64. {
  65. InitializeWhitelist();
  66. }
  67. public void Initialize()
  68. {
  69. _sawmill = _logManager.GetSawmill("connections");
  70. _ipintel = new IPIntel.IPIntel(new IPIntelApi(_http, _cfg), _db, _cfg, _logManager, _chatManager, _gameTiming);
  71. _netMgr.Connecting += NetMgrOnConnecting;
  72. _netMgr.AssignUserIdCallback = AssignUserIdCallback;
  73. _plyMgr.PlayerStatusChanged += PlayerStatusChanged;
  74. // Approval-based IP bans disabled because they don't play well with Happy Eyeballs.
  75. // _netMgr.HandleApprovalCallback = HandleApproval;
  76. }
  77. public void AddTemporaryConnectBypass(NetUserId user, TimeSpan duration)
  78. {
  79. ref var time = ref CollectionsMarshal.GetValueRefOrAddDefault(_temporaryBypasses, user, out _);
  80. var newTime = _gameTiming.RealTime + duration;
  81. // Make sure we only update the time if we wouldn't shrink it.
  82. if (newTime > time)
  83. time = newTime;
  84. }
  85. public async void Update()
  86. {
  87. try
  88. {
  89. await _ipintel.Update();
  90. }
  91. catch (Exception e)
  92. {
  93. _sawmill.Error("IPIntel update failed:" + e);
  94. }
  95. }
  96. /*
  97. private async Task<NetApproval> HandleApproval(NetApprovalEventArgs eventArgs)
  98. {
  99. var ban = await _db.GetServerBanByIpAsync(eventArgs.Connection.RemoteEndPoint.Address);
  100. if (ban != null)
  101. {
  102. var expires = Loc.GetString("ban-banned-permanent");
  103. if (ban.ExpirationTime is { } expireTime)
  104. {
  105. var duration = expireTime - ban.BanTime;
  106. var utc = expireTime.ToUniversalTime();
  107. expires = Loc.GetString("ban-expires", ("duration", duration.TotalMinutes.ToString("N0")), ("time", utc.ToString("f")));
  108. }
  109. var reason = Loc.GetString("ban-banned-1") + "\n" + Loc.GetString("ban-banned-2", ("reason", this.Reason)) + "\n" + expires;;
  110. return NetApproval.Deny(reason);
  111. }
  112. return NetApproval.Allow();
  113. }
  114. */
  115. private async Task NetMgrOnConnecting(NetConnectingArgs e)
  116. {
  117. var deny = await ShouldDeny(e);
  118. var addr = e.IP.Address;
  119. var userId = e.UserId;
  120. var serverId = (await _serverDbEntry.ServerEntity).Id;
  121. var hwid = e.UserData.GetModernHwid();
  122. var trust = e.UserData.Trust;
  123. if (deny != null)
  124. {
  125. var (reason, msg, banHits) = deny.Value;
  126. var id = await _db.AddConnectionLogAsync(userId, e.UserName, addr, hwid, trust, reason, serverId);
  127. if (banHits is { Count: > 0 })
  128. await _db.AddServerBanHitsAsync(id, banHits);
  129. var properties = new Dictionary<string, object>();
  130. if (reason == ConnectionDenyReason.Full)
  131. properties["delay"] = _cfg.GetCVar(CCVars.GameServerFullReconnectDelay);
  132. e.Deny(new NetDenyReason(msg, properties));
  133. }
  134. else
  135. {
  136. await _db.AddConnectionLogAsync(userId, e.UserName, addr, hwid, trust, null, serverId);
  137. if (!ServerPreferencesManager.ShouldStorePrefs(e.AuthType))
  138. return;
  139. await _db.UpdatePlayerRecordAsync(userId, e.UserName, addr, hwid);
  140. }
  141. }
  142. private async void PlayerStatusChanged(object? sender, SessionStatusEventArgs args)
  143. {
  144. if (args.NewStatus == SessionStatus.Connected)
  145. {
  146. AdminAlertIfSharedConnection(args.Session);
  147. }
  148. }
  149. private void AdminAlertIfSharedConnection(ICommonSession newSession)
  150. {
  151. var playerThreshold = _cfg.GetCVar(CCVars.AdminAlertMinPlayersSharingConnection);
  152. if (playerThreshold < 0)
  153. return;
  154. var addr = newSession.Channel.RemoteEndPoint.Address;
  155. var otherConnectionsFromAddress = _plyMgr.Sessions.Where(session =>
  156. session.Status is SessionStatus.Connected or SessionStatus.InGame
  157. && session.Channel.RemoteEndPoint.Address.Equals(addr)
  158. && session.UserId != newSession.UserId)
  159. .ToList();
  160. var otherConnectionCount = otherConnectionsFromAddress.Count;
  161. if (otherConnectionCount + 1 < playerThreshold) // Add one for the total, not just others, using the address
  162. return;
  163. var username = newSession.Name;
  164. var otherUsernames = string.Join(", ",
  165. otherConnectionsFromAddress.Select(session => session.Name));
  166. _chatManager.SendAdminAlert(Loc.GetString("admin-alert-shared-connection",
  167. ("player", username),
  168. ("otherCount", otherConnectionCount),
  169. ("otherList", otherUsernames)));
  170. }
  171. /*
  172. * TODO: Jesus H Christ what is this utter mess of a function
  173. * TODO: Break this apart into is constituent steps.
  174. */
  175. private async Task<(ConnectionDenyReason, string, List<ServerBanDef>? bansHit)?> ShouldDeny(
  176. NetConnectingArgs e)
  177. {
  178. // Check if banned.
  179. var addr = e.IP.Address;
  180. var userId = e.UserId;
  181. ImmutableArray<byte>? hwId = e.UserData.HWId;
  182. if (hwId.Value.Length == 0 || !_cfg.GetCVar(CCVars.BanHardwareIds))
  183. {
  184. // HWId not available for user's platform, don't look it up.
  185. // Or hardware ID checks disabled.
  186. hwId = null;
  187. }
  188. var modernHwid = e.UserData.ModernHWIds;
  189. if (modernHwid.Length == 0 && e.AuthType == LoginType.LoggedIn && _cfg.GetCVar(CCVars.RequireModernHardwareId))
  190. {
  191. return (ConnectionDenyReason.NoHwid, Loc.GetString("hwid-required"), null);
  192. }
  193. var bans = await _db.GetServerBansAsync(addr, userId, hwId, modernHwid, includeUnbanned: false);
  194. if (bans.Count > 0)
  195. {
  196. var firstBan = bans[0];
  197. var message = firstBan.FormatBanMessage(_cfg, _loc);
  198. return (ConnectionDenyReason.Ban, message, bans);
  199. }
  200. if (HasTemporaryBypass(userId))
  201. {
  202. _sawmill.Verbose("User {UserId} has temporary bypass, skipping further connection checks", userId);
  203. return null;
  204. }
  205. var adminData = await _db.GetAdminDataForAsync(e.UserId);
  206. if (_cfg.GetCVar(CCVars.PanicBunkerEnabled) && adminData == null)
  207. {
  208. var showReason = _cfg.GetCVar(CCVars.PanicBunkerShowReason);
  209. var customReason = _cfg.GetCVar(CCVars.PanicBunkerCustomReason);
  210. var minMinutesAge = _cfg.GetCVar(CCVars.PanicBunkerMinAccountAge);
  211. var record = await _db.GetPlayerRecordByUserId(userId);
  212. var validAccountAge = record != null &&
  213. record.FirstSeenTime.CompareTo(DateTimeOffset.UtcNow - TimeSpan.FromMinutes(minMinutesAge)) <= 0;
  214. var bypassAllowed = _cfg.GetCVar(CCVars.BypassBunkerWhitelist) && await _db.GetWhitelistStatusAsync(userId);
  215. // Use the custom reason if it exists & they don't have the minimum account age
  216. if (customReason != string.Empty && !validAccountAge && !bypassAllowed)
  217. {
  218. return (ConnectionDenyReason.Panic, customReason, null);
  219. }
  220. if (showReason && !validAccountAge && !bypassAllowed)
  221. {
  222. return (ConnectionDenyReason.Panic,
  223. Loc.GetString("panic-bunker-account-denied-reason",
  224. ("reason", Loc.GetString("panic-bunker-account-reason-account", ("minutes", minMinutesAge)))), null);
  225. }
  226. var minOverallMinutes = _cfg.GetCVar(CCVars.PanicBunkerMinOverallMinutes);
  227. var overallTime = ( await _db.GetPlayTimes(e.UserId)).Find(p => p.Tracker == PlayTimeTrackingShared.TrackerOverall);
  228. var haveMinOverallTime = overallTime != null && overallTime.TimeSpent.TotalMinutes > minOverallMinutes;
  229. // Use the custom reason if it exists & they don't have the minimum time
  230. if (customReason != string.Empty && !haveMinOverallTime && !bypassAllowed)
  231. {
  232. return (ConnectionDenyReason.Panic, customReason, null);
  233. }
  234. if (showReason && !haveMinOverallTime && !bypassAllowed)
  235. {
  236. return (ConnectionDenyReason.Panic,
  237. Loc.GetString("panic-bunker-account-denied-reason",
  238. ("reason", Loc.GetString("panic-bunker-account-reason-overall", ("minutes", minOverallMinutes)))), null);
  239. }
  240. if (!validAccountAge || !haveMinOverallTime && !bypassAllowed)
  241. {
  242. return (ConnectionDenyReason.Panic, Loc.GetString("panic-bunker-account-denied"), null);
  243. }
  244. }
  245. var wasInGame = EntitySystem.TryGet<GameTicker>(out var ticker) &&
  246. ticker.PlayerGameStatuses.TryGetValue(userId, out var status) &&
  247. status == PlayerGameStatus.JoinedGame;
  248. var adminBypass = _cfg.GetCVar(CCVars.AdminBypassMaxPlayers) && adminData != null;
  249. var softPlayerCount = _plyMgr.PlayerCount;
  250. if (!_cfg.GetCVar(CCVars.AdminsCountForMaxPlayers))
  251. {
  252. softPlayerCount -= _adminManager.ActiveAdmins.Count();
  253. }
  254. if ((softPlayerCount >= _cfg.GetCVar(CCVars.SoftMaxPlayers) && !adminBypass) && !wasInGame)
  255. {
  256. return (ConnectionDenyReason.Full, Loc.GetString("soft-player-cap-full"), null);
  257. }
  258. // Checks for whitelist IF it's enabled AND the user isn't an admin. Admins are always allowed.
  259. if (_cfg.GetCVar(CCVars.WhitelistEnabled) && adminData is null)
  260. {
  261. if (_whitelists is null)
  262. {
  263. _sawmill.Error("Whitelist enabled but no whitelists loaded.");
  264. // Misconfigured, deny everyone.
  265. return (ConnectionDenyReason.Whitelist, Loc.GetString("generic-misconfigured"), null);
  266. }
  267. foreach (var whitelist in _whitelists)
  268. {
  269. if (!IsValid(whitelist, softPlayerCount))
  270. {
  271. // Not valid for current player count.
  272. continue;
  273. }
  274. var whitelistStatus = await IsWhitelisted(whitelist, e.UserData, _sawmill);
  275. if (!whitelistStatus.isWhitelisted)
  276. {
  277. // Not whitelisted.
  278. return (ConnectionDenyReason.Whitelist, Loc.GetString("whitelist-fail-prefix", ("msg", whitelistStatus.denyMessage!)), null);
  279. }
  280. // Whitelisted, don't check any more.
  281. break;
  282. }
  283. }
  284. // ALWAYS keep this at the end, to preserve the API limit.
  285. if (_cfg.GetCVar(CCVars.GameIPIntelEnabled) && adminData == null)
  286. {
  287. var result = await _ipintel.IsVpnOrProxy(e);
  288. if (result.IsBad)
  289. return (ConnectionDenyReason.IPChecks, result.Reason, null);
  290. }
  291. return null;
  292. }
  293. private bool HasTemporaryBypass(NetUserId user)
  294. {
  295. return _temporaryBypasses.TryGetValue(user, out var time) && time > _gameTiming.RealTime;
  296. }
  297. private async Task<NetUserId?> AssignUserIdCallback(string name)
  298. {
  299. if (!_cfg.GetCVar(CCVars.GamePersistGuests))
  300. {
  301. return null;
  302. }
  303. var userId = await _db.GetAssignedUserIdAsync(name);
  304. if (userId != null)
  305. {
  306. return userId;
  307. }
  308. var assigned = new NetUserId(Guid.NewGuid());
  309. await _db.AssignUserIdAsync(name, assigned);
  310. return assigned;
  311. }
  312. }
  313. }