1
0

IPIntel.cs 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387
  1. using System.Buffers.Binary;
  2. using System.Net;
  3. using System.Net.Sockets;
  4. using System.Threading.Tasks;
  5. using Content.Server.Chat.Managers;
  6. using Content.Server.Database;
  7. using Content.Shared.CCVar;
  8. using Content.Shared.Players.PlayTimeTracking;
  9. using Robust.Shared.Configuration;
  10. using Robust.Shared.Network;
  11. using Robust.Shared.Timing;
  12. using Robust.Shared.Utility;
  13. namespace Content.Server.Connection.IPIntel;
  14. // Handles checking/warning if the connecting IP address is sus.
  15. public sealed class IPIntel
  16. {
  17. private readonly IIPIntelApi _api;
  18. private readonly IServerDbManager _db;
  19. private readonly IChatManager _chatManager;
  20. private readonly IGameTiming _gameTiming;
  21. private readonly ISawmill _sawmill;
  22. public IPIntel(IIPIntelApi api,
  23. IServerDbManager db,
  24. IConfigurationManager cfg,
  25. ILogManager logManager,
  26. IChatManager chatManager,
  27. IGameTiming gameTiming)
  28. {
  29. _api = api;
  30. _db = db;
  31. _chatManager = chatManager;
  32. _gameTiming = gameTiming;
  33. _sawmill = logManager.GetSawmill("ipintel");
  34. cfg.OnValueChanged(CCVars.GameIPIntelEmail, b => _contactEmail = b, true);
  35. cfg.OnValueChanged(CCVars.GameIPIntelEnabled, b => _enabled = b, true);
  36. cfg.OnValueChanged(CCVars.GameIPIntelRejectUnknown, b => _rejectUnknown = b, true);
  37. cfg.OnValueChanged(CCVars.GameIPIntelRejectBad, b => _rejectBad = b, true);
  38. cfg.OnValueChanged(CCVars.GameIPIntelRejectRateLimited, b => _rejectLimited = b, true);
  39. cfg.OnValueChanged(CCVars.GameIPIntelMaxMinute, b => _minute.Limit = b, true);
  40. cfg.OnValueChanged(CCVars.GameIPIntelMaxDay, b => _day.Limit = b, true);
  41. cfg.OnValueChanged(CCVars.GameIPIntelBackOffSeconds, b => _backoffSeconds = b, true);
  42. cfg.OnValueChanged(CCVars.GameIPIntelCleanupMins, b => _cleanupMins = b, true);
  43. cfg.OnValueChanged(CCVars.GameIPIntelBadRating, b => _rating = b, true);
  44. cfg.OnValueChanged(CCVars.GameIPIntelCacheLength, b => _cacheDays = b, true);
  45. cfg.OnValueChanged(CCVars.GameIPIntelExemptPlaytime, b => _exemptPlaytime = b, true);
  46. cfg.OnValueChanged(CCVars.GameIPIntelAlertAdminReject, b => _alertAdminReject = b, true);
  47. cfg.OnValueChanged(CCVars.GameIPIntelAlertAdminWarnRating, b => _alertAdminWarn = b, true);
  48. }
  49. internal struct Ratelimits
  50. {
  51. public bool RateLimited;
  52. public bool LimitHasBeenHandled;
  53. public int CurrentRequests;
  54. public int Limit;
  55. public TimeSpan LastRatelimited;
  56. }
  57. // Self-managed preemptive rate limits.
  58. private Ratelimits _day;
  59. private Ratelimits _minute;
  60. // Next time we need to clean the database of stale cached IPIntel results.
  61. private TimeSpan _nextClean;
  62. // Responsive backoff if we hit a Too Many Requests API error.
  63. private int _failedRequests;
  64. private TimeSpan _releasePeriod;
  65. // CCVars
  66. private string? _contactEmail;
  67. private bool _enabled;
  68. private bool _rejectUnknown;
  69. private bool _rejectBad;
  70. private bool _rejectLimited;
  71. private bool _alertAdminReject;
  72. private int _backoffSeconds;
  73. private int _cleanupMins;
  74. private TimeSpan _cacheDays;
  75. private TimeSpan _exemptPlaytime;
  76. private float _rating;
  77. private float _alertAdminWarn;
  78. public async Task<(bool IsBad, string Reason)> IsVpnOrProxy(NetConnectingArgs e)
  79. {
  80. // Check Exemption flags, let them skip if they have them.
  81. var flags = await _db.GetBanExemption(e.UserId);
  82. if ((flags & (ServerBanExemptFlags.Datacenter | ServerBanExemptFlags.BlacklistedRange)) != 0)
  83. {
  84. return (false, string.Empty);
  85. }
  86. // Check playtime, if 0 we skip this check. If player has more playtime then _exemptPlaytime is configured for then they get to skip this check.
  87. // Helps with saving your limited request limit.
  88. if (_exemptPlaytime != TimeSpan.Zero)
  89. {
  90. var overallTime = ( await _db.GetPlayTimes(e.UserId)).Find(p => p.Tracker == PlayTimeTrackingShared.TrackerOverall);
  91. if (overallTime != null && overallTime.TimeSpent >= _exemptPlaytime)
  92. {
  93. return (false, string.Empty);
  94. }
  95. }
  96. var ip = e.IP.Address;
  97. var username = e.UserName;
  98. // Is this a local ip address?
  99. if (IsAddressReservedIpv4(ip) || IsAddressReservedIpv6(ip))
  100. {
  101. _sawmill.Warning($"{e.UserName} joined using a local address. Do you need IPIntel? Or is something terribly misconfigured on your server? Trusting this connection.");
  102. return (false, string.Empty);
  103. }
  104. // Check our cache
  105. var query = await _db.GetIPIntelCache(ip);
  106. // Does it exist?
  107. if (query != null)
  108. {
  109. // Skip to score check if result is older than _cacheDays
  110. if (DateTime.UtcNow - query.Time <= _cacheDays)
  111. {
  112. var score = query.Score;
  113. return ScoreCheck(score, username);
  114. }
  115. }
  116. // Ensure our contact email is good to use.
  117. if (string.IsNullOrEmpty(_contactEmail) || !_contactEmail.Contains('@') || !_contactEmail.Contains('.'))
  118. {
  119. _sawmill.Error("IPIntel is enabled, but contact email is empty or not a valid email, treating this connection like an unknown IPIntel response.");
  120. return _rejectUnknown ? (true, Loc.GetString("generic-misconfigured")) : (false, string.Empty);
  121. }
  122. var apiResult = await QueryIPIntelRateLimited(ip);
  123. switch (apiResult.Code)
  124. {
  125. case IPIntelResultCode.Success:
  126. await Task.Run(() => _db.UpsertIPIntelCache(DateTime.UtcNow, ip, apiResult.Score));
  127. return ScoreCheck(apiResult.Score, username);
  128. case IPIntelResultCode.RateLimited:
  129. return _rejectLimited ? (true, Loc.GetString("ipintel-server-ratelimited")) : (false, string.Empty);
  130. case IPIntelResultCode.Errored:
  131. return _rejectUnknown ? (true, Loc.GetString("ipintel-unknown")) : (false, string.Empty);
  132. default:
  133. throw new ArgumentOutOfRangeException();
  134. }
  135. }
  136. public async Task<IPIntelResult> QueryIPIntelRateLimited(IPAddress ip)
  137. {
  138. IncrementAndTestRateLimit(ref _day, TimeSpan.FromDays(1), "daily");
  139. IncrementAndTestRateLimit(ref _minute, TimeSpan.FromMinutes(1), "minute");
  140. if (_minute.RateLimited || _day.RateLimited || CheckSuddenRateLimit())
  141. return new IPIntelResult(0, IPIntelResultCode.RateLimited);
  142. // Info about flag B: https://getipintel.net/free-proxy-vpn-tor-detection-api/#flagsb
  143. // TLDR: We don't care about knowing if a connection is compromised.
  144. // We just want to know if it's a vpn. This also speeds up the request by quite a bit. (A full scan can take 200ms to 5 seconds. This will take at most 120ms)
  145. using var request = await _api.GetIPScore(ip);
  146. if (request.StatusCode == HttpStatusCode.TooManyRequests)
  147. {
  148. _sawmill.Warning($"We hit the IPIntel request limit at some point. (Current limit count: Minute: {_minute.CurrentRequests} Day: {_day.CurrentRequests})");
  149. CalculateSuddenRatelimit();
  150. return new IPIntelResult(0, IPIntelResultCode.RateLimited);
  151. }
  152. var response = await request.Content.ReadAsStringAsync();
  153. var score = Parse.Float(response);
  154. if (request.StatusCode == HttpStatusCode.OK)
  155. {
  156. _failedRequests = 0;
  157. return new IPIntelResult(score, IPIntelResultCode.Success);
  158. }
  159. if (ErrorMessages.TryGetValue(response, out var errorMessage))
  160. {
  161. _sawmill.Error($"IPIntel returned error {response}: {errorMessage}");
  162. }
  163. else
  164. {
  165. // Oh boy, we don't know this error.
  166. _sawmill.Error($"IPIntel returned {response} (Status code: {request.StatusCode})... we don't know what this error code is. Please make an issue in upstream!");
  167. }
  168. return new IPIntelResult(0, IPIntelResultCode.Errored);
  169. }
  170. private bool CheckSuddenRateLimit()
  171. {
  172. return _failedRequests >= 1 && _releasePeriod > _gameTiming.RealTime;
  173. }
  174. private void CalculateSuddenRatelimit()
  175. {
  176. _failedRequests++;
  177. _releasePeriod = _gameTiming.RealTime + TimeSpan.FromSeconds(_failedRequests * _backoffSeconds);
  178. }
  179. private static readonly Dictionary<string, string> ErrorMessages = new()
  180. {
  181. ["-1"] = "Invalid/No input.",
  182. ["-2"] = "Invalid IP address.",
  183. ["-3"] = "Unroutable address / private address given to the api. Make an issue in upstream as it should have been handled.",
  184. ["-4"] = "Unable to reach IPIntel database. Perhaps it's down?",
  185. ["-5"] = "Server's IP/Contact may have been banned, go to getipintel.net and make contact to be unbanned.",
  186. ["-6"] = "You did not provide any contact information with your query or the contact information is invalid.",
  187. };
  188. private void IncrementAndTestRateLimit(ref Ratelimits ratelimits, TimeSpan expireInterval, string name)
  189. {
  190. if (ratelimits.CurrentRequests < ratelimits.Limit)
  191. {
  192. ratelimits.CurrentRequests += 1;
  193. return;
  194. }
  195. if (ShouldLiftRateLimit(in ratelimits, expireInterval))
  196. {
  197. _sawmill.Info($"IPIntel {name} rate limit lifted. We are back to normal.");
  198. ratelimits.RateLimited = false;
  199. ratelimits.CurrentRequests = 0;
  200. ratelimits.LimitHasBeenHandled = false;
  201. return;
  202. }
  203. if (ratelimits.LimitHasBeenHandled)
  204. return;
  205. _sawmill.Warning($"We just hit our last {name} IPIntel limit ({ratelimits.Limit})");
  206. ratelimits.RateLimited = true;
  207. ratelimits.LimitHasBeenHandled = true;
  208. ratelimits.LastRatelimited = _gameTiming.RealTime;
  209. }
  210. private bool ShouldLiftRateLimit(in Ratelimits ratelimits, TimeSpan liftingTime)
  211. {
  212. // Should we raise this limit now?
  213. return ratelimits.RateLimited && _gameTiming.RealTime >= ratelimits.LastRatelimited + liftingTime;
  214. }
  215. private (bool, string Empty) ScoreCheck(float score, string username)
  216. {
  217. var decisionIsReject = score > _rating;
  218. if (_alertAdminWarn != 0f && _alertAdminWarn < score && !decisionIsReject)
  219. {
  220. _chatManager.SendAdminAlert(Loc.GetString("admin-alert-ipintel-warning",
  221. ("player", username),
  222. ("percent", Math.Round(score))));
  223. }
  224. if (!decisionIsReject)
  225. return (false, string.Empty);
  226. if (_alertAdminReject)
  227. {
  228. _chatManager.SendAdminAlert(Loc.GetString("admin-alert-ipintel-blocked",
  229. ("player", username),
  230. ("percent", Math.Round(score))));
  231. }
  232. return _rejectBad ? (true, Loc.GetString("ipintel-suspicious")) : (false, string.Empty);
  233. }
  234. public async Task Update()
  235. {
  236. if (_enabled && _gameTiming.RealTime >= _nextClean)
  237. {
  238. _nextClean = _gameTiming.RealTime + TimeSpan.FromMinutes(_cleanupMins);
  239. await _db.CleanIPIntelCache(_cacheDays);
  240. }
  241. }
  242. // Stolen from Lidgren.Network (Space Wizards Edition) (NetReservedAddress.cs)
  243. // Modified with IPV6 on top
  244. private static int Ipv4(byte a, byte b, byte c, byte d)
  245. {
  246. return (a << 24) | (b << 16) | (c << 8) | d;
  247. }
  248. // From miniupnpc
  249. private static readonly (int ip, int mask)[] ReservedRangesIpv4 =
  250. [
  251. // @formatter:off
  252. (Ipv4(0, 0, 0, 0), 8 ), // RFC1122 "This host on this network"
  253. (Ipv4(10, 0, 0, 0), 8 ), // RFC1918 Private-Use
  254. (Ipv4(100, 64, 0, 0), 10), // RFC6598 Shared Address Space
  255. (Ipv4(127, 0, 0, 0), 8 ), // RFC1122 Loopback
  256. (Ipv4(169, 254, 0, 0), 16), // RFC3927 Link-Local
  257. (Ipv4(172, 16, 0, 0), 12), // RFC1918 Private-Use
  258. (Ipv4(192, 0, 0, 0), 24), // RFC6890 IETF Protocol Assignments
  259. (Ipv4(192, 0, 2, 0), 24), // RFC5737 Documentation (TEST-NET-1)
  260. (Ipv4(192, 31, 196, 0), 24), // RFC7535 AS112-v4
  261. (Ipv4(192, 52, 193, 0), 24), // RFC7450 AMT
  262. (Ipv4(192, 88, 99, 0), 24), // RFC7526 6to4 Relay Anycast
  263. (Ipv4(192, 168, 0, 0), 16), // RFC1918 Private-Use
  264. (Ipv4(192, 175, 48, 0), 24), // RFC7534 Direct Delegation AS112 Service
  265. (Ipv4(198, 18, 0, 0), 15), // RFC2544 Benchmarking
  266. (Ipv4(198, 51, 100, 0), 24), // RFC5737 Documentation (TEST-NET-2)
  267. (Ipv4(203, 0, 113, 0), 24), // RFC5737 Documentation (TEST-NET-3)
  268. (Ipv4(224, 0, 0, 0), 4 ), // RFC1112 Multicast
  269. (Ipv4(240, 0, 0, 0), 4 ), // RFC1112 Reserved for Future Use + RFC919 Limited Broadcast
  270. // @formatter:on
  271. ];
  272. private static UInt128 ToAddressBytes(string ip)
  273. {
  274. return BinaryPrimitives.ReadUInt128BigEndian(IPAddress.Parse(ip).GetAddressBytes());
  275. }
  276. private static readonly (UInt128 ip, int mask)[] ReservedRangesIpv6 =
  277. [
  278. (ToAddressBytes("::1"), 128), // "This host on this network"
  279. (ToAddressBytes("::ffff:0:0"), 96), // IPv4-mapped addresses
  280. (ToAddressBytes("::ffff:0:0:0"), 96), // IPv4-translated addresses
  281. (ToAddressBytes("64:ff9b:1::"), 48), // IPv4/IPv6 translation
  282. (ToAddressBytes("100::"), 64), // Discard prefix
  283. (ToAddressBytes("2001:20::"), 28), // ORCHIDv2
  284. (ToAddressBytes("2001:db8::"), 32), // Addresses used in documentation and example source code
  285. (ToAddressBytes("3fff::"), 20), // Addresses used in documentation and example source code
  286. (ToAddressBytes("5f00::"), 16), // IPv6 Segment Routing (SRv6)
  287. (ToAddressBytes("fc00::"), 7), // Unique local address
  288. ];
  289. internal static bool IsAddressReservedIpv4(IPAddress address)
  290. {
  291. if (address.AddressFamily != AddressFamily.InterNetwork)
  292. return false;
  293. Span<byte> ipBitsByte = stackalloc byte[4];
  294. address.TryWriteBytes(ipBitsByte, out _);
  295. var ipBits = BinaryPrimitives.ReadInt32BigEndian(ipBitsByte);
  296. foreach (var (reservedIp, maskBits) in ReservedRangesIpv4)
  297. {
  298. var mask = uint.MaxValue << (32 - maskBits);
  299. if ((ipBits & mask) == (reservedIp & mask))
  300. return true;
  301. }
  302. return false;
  303. }
  304. internal static bool IsAddressReservedIpv6(IPAddress address)
  305. {
  306. if (address.AddressFamily != AddressFamily.InterNetworkV6)
  307. return false;
  308. if (address.IsIPv4MappedToIPv6)
  309. return IsAddressReservedIpv4(address.MapToIPv4());
  310. Span<byte> ipBitsByte = stackalloc byte[16];
  311. address.TryWriteBytes(ipBitsByte, out _);
  312. var ipBits = BinaryPrimitives.ReadInt128BigEndian(ipBitsByte);
  313. foreach (var (reservedIp, maskBits) in ReservedRangesIpv6)
  314. {
  315. var mask = UInt128.MaxValue << (128 - maskBits);
  316. if (((UInt128) ipBits & mask ) == (reservedIp & mask))
  317. return true;
  318. }
  319. return false;
  320. }
  321. public readonly record struct IPIntelResult(float Score, IPIntelResultCode Code);
  322. public enum IPIntelResultCode : byte
  323. {
  324. Success = 0,
  325. RateLimited,
  326. Errored,
  327. }
  328. }