IPIntelTest.cs 8.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269
  1. using System;
  2. using System.Net;
  3. using System.Net.Http;
  4. using System.Threading.Tasks;
  5. using Content.Server.Chat.Managers;
  6. using Content.Server.Connection.IPIntel;
  7. using Content.Server.Database;
  8. using Content.Shared.CCVar;
  9. using Moq;
  10. using NUnit.Framework;
  11. using Robust.Shared.Configuration;
  12. using Robust.Shared.Log;
  13. using Robust.Shared.Timing;
  14. using Robust.UnitTesting;
  15. // ReSharper disable AccessToModifiedClosure
  16. namespace Content.Tests.Server.Connection;
  17. [TestFixture, TestOf(typeof(IPIntel))]
  18. [Parallelizable(ParallelScope.All)]
  19. public static class IPIntelTest
  20. {
  21. private static readonly IPAddress TestIp = IPAddress.Parse("192.0.2.1");
  22. private static void CreateIPIntel(
  23. out IPIntel ipIntel,
  24. out IConfigurationManager cfg,
  25. Func<HttpResponseMessage> apiResponse,
  26. Func<TimeSpan> realTime = null)
  27. {
  28. var dbManager = new Mock<IServerDbManager>();
  29. var gameTimingMock = new Mock<IGameTiming>();
  30. gameTimingMock.SetupGet(gt => gt.RealTime)
  31. .Returns(realTime ?? (() => TimeSpan.Zero));
  32. var logManager = new LogManager();
  33. var gameTiming = gameTimingMock.Object;
  34. cfg = MockInterfaces.MakeConfigurationManager(gameTiming, logManager, loadCvarsFromTypes: [typeof(CCVars)]);
  35. ipIntel = new IPIntel(
  36. new FakeIPIntelApi(apiResponse),
  37. dbManager.Object,
  38. cfg,
  39. logManager,
  40. new Mock<IChatManager>().Object,
  41. gameTiming
  42. );
  43. }
  44. [Test]
  45. public static async Task TestSuccess()
  46. {
  47. CreateIPIntel(
  48. out var ipIntel,
  49. out _,
  50. RespondSuccess);
  51. var result = await ipIntel.QueryIPIntelRateLimited(TestIp);
  52. Assert.Multiple(() =>
  53. {
  54. Assert.That(result.Score, Is.EqualTo(0.5f).Within(0.01f));
  55. Assert.That(result.Code, Is.EqualTo(IPIntel.IPIntelResultCode.Success));
  56. });
  57. }
  58. [Test]
  59. public static async Task KnownRateLimitMinuteTest()
  60. {
  61. var source = RespondSuccess;
  62. var time = TimeSpan.Zero;
  63. CreateIPIntel(
  64. out var ipIntel,
  65. out var cfg,
  66. () => source(),
  67. () => time);
  68. cfg.SetCVar(CCVars.GameIPIntelMaxMinute, 9);
  69. for (var i = 0; i < 9; i++)
  70. {
  71. var result = await ipIntel.QueryIPIntelRateLimited(TestIp);
  72. Assert.That(result.Code, Is.EqualTo(IPIntel.IPIntelResultCode.Success));
  73. }
  74. source = RespondTestFailed;
  75. var shouldBeRateLimited = await ipIntel.QueryIPIntelRateLimited(TestIp);
  76. Assert.That(shouldBeRateLimited.Code, Is.EqualTo(IPIntel.IPIntelResultCode.RateLimited));
  77. time += TimeSpan.FromMinutes(1.5);
  78. source = RespondSuccess;
  79. var shouldSucceed = await ipIntel.QueryIPIntelRateLimited(TestIp);
  80. Assert.That(shouldSucceed.Code, Is.EqualTo(IPIntel.IPIntelResultCode.Success));
  81. }
  82. [Test]
  83. public static async Task KnownRateLimitMinuteTimingTest()
  84. {
  85. var source = RespondSuccess;
  86. var time = TimeSpan.Zero;
  87. CreateIPIntel(
  88. out var ipIntel,
  89. out var cfg,
  90. () => source(),
  91. () => time);
  92. cfg.SetCVar(CCVars.GameIPIntelMaxMinute, 1);
  93. // First query succeeds.
  94. var result = await ipIntel.QueryIPIntelRateLimited(TestIp);
  95. Assert.That(result.Code, Is.EqualTo(IPIntel.IPIntelResultCode.Success));
  96. // Second is rate limited via known limit.
  97. source = RespondTestFailed;
  98. result = await ipIntel.QueryIPIntelRateLimited(TestIp);
  99. Assert.That(result.Code, Is.EqualTo(IPIntel.IPIntelResultCode.RateLimited));
  100. // Move 30 seconds into the future, should not be enough to unratelimit.
  101. time += TimeSpan.FromSeconds(30);
  102. var shouldBeRateLimited = await ipIntel.QueryIPIntelRateLimited(TestIp);
  103. Assert.That(shouldBeRateLimited.Code, Is.EqualTo(IPIntel.IPIntelResultCode.RateLimited));
  104. // Should be available again.
  105. source = RespondSuccess;
  106. time += TimeSpan.FromSeconds(35);
  107. var shouldSucceed = await ipIntel.QueryIPIntelRateLimited(TestIp);
  108. Assert.That(shouldSucceed.Code, Is.EqualTo(IPIntel.IPIntelResultCode.Success));
  109. }
  110. [Test]
  111. public static async Task SuddenRateLimitTest()
  112. {
  113. var time = TimeSpan.Zero;
  114. var source = RespondRateLimited;
  115. CreateIPIntel(
  116. out var ipIntel,
  117. out _,
  118. () => source(),
  119. () => time);
  120. var test = await ipIntel.QueryIPIntelRateLimited(TestIp);
  121. Assert.That(test.Code, Is.EqualTo(IPIntel.IPIntelResultCode.RateLimited));
  122. source = RespondTestFailed;
  123. test = await ipIntel.QueryIPIntelRateLimited(TestIp);
  124. Assert.That(test.Code, Is.EqualTo(IPIntel.IPIntelResultCode.RateLimited));
  125. // King crimson idk I didn't watch JoJo past part 2.
  126. time += TimeSpan.FromMinutes(2);
  127. source = RespondSuccess;
  128. test = await ipIntel.QueryIPIntelRateLimited(TestIp);
  129. Assert.That(test.Code, Is.EqualTo(IPIntel.IPIntelResultCode.Success));
  130. }
  131. [Test]
  132. public static async Task SuddenRateLimitExponentialBackoffTest()
  133. {
  134. var time = TimeSpan.Zero;
  135. var source = RespondRateLimited;
  136. CreateIPIntel(
  137. out var ipIntel,
  138. out _,
  139. () => source(),
  140. () => time);
  141. IPIntel.IPIntelResult test;
  142. for (var i = 0; i < 5; i++)
  143. {
  144. time += TimeSpan.FromHours(1);
  145. test = await ipIntel.QueryIPIntelRateLimited(TestIp);
  146. Assert.That(test.Code, Is.EqualTo(IPIntel.IPIntelResultCode.RateLimited));
  147. }
  148. // After 5 sequential failed attempts, 1 minute should not be enough to get past the exponential backoff.
  149. time += TimeSpan.FromMinutes(1);
  150. source = RespondTestFailed;
  151. test = await ipIntel.QueryIPIntelRateLimited(TestIp);
  152. Assert.That(test.Code, Is.EqualTo(IPIntel.IPIntelResultCode.RateLimited));
  153. }
  154. [Test]
  155. public static async Task ErrorTest()
  156. {
  157. CreateIPIntel(
  158. out var ipIntel,
  159. out _,
  160. RespondError);
  161. var resp = await ipIntel.QueryIPIntelRateLimited(TestIp);
  162. Assert.That(resp.Code, Is.EqualTo(IPIntel.IPIntelResultCode.Errored));
  163. }
  164. [Test]
  165. [TestCase("0.0.0.0", ExpectedResult = true)]
  166. [TestCase("0.3.5.7", ExpectedResult = true)]
  167. [TestCase("127.0.0.1", ExpectedResult = true)]
  168. [TestCase("11.0.0.0", ExpectedResult = false)]
  169. [TestCase("10.0.1.0", ExpectedResult = true)]
  170. [TestCase("192.168.5.12", ExpectedResult = true)]
  171. [TestCase("192.167.0.1", ExpectedResult = false)]
  172. // Not an IPv4!
  173. [TestCase("::1", ExpectedResult = false)]
  174. public static bool TestIsReservedIpv4(string ipAddress)
  175. {
  176. return IPIntel.IsAddressReservedIpv4(IPAddress.Parse(ipAddress));
  177. }
  178. [Test]
  179. // IPv4-mapped IPv6 should use IPv4 behavior.
  180. [TestCase("::ffff:0.0.0.0", ExpectedResult = true)]
  181. [TestCase("::ffff:0.3.5.7", ExpectedResult = true)]
  182. [TestCase("::ffff:127.0.0.1", ExpectedResult = true)]
  183. [TestCase("::ffff:11.0.0.0", ExpectedResult = false)]
  184. [TestCase("::ffff:10.0.1.0", ExpectedResult = true)]
  185. [TestCase("::ffff:192.168.5.12", ExpectedResult = true)]
  186. [TestCase("::ffff:192.167.0.1", ExpectedResult = false)]
  187. // Regular IPv6 tests.
  188. [TestCase("::1", ExpectedResult = true)]
  189. [TestCase("2001:db8::01", ExpectedResult = true)]
  190. [TestCase("2a01:4f8:252:4425::1234", ExpectedResult = false)]
  191. // Not an IPv6!
  192. [TestCase("127.0.0.1", ExpectedResult = false)]
  193. public static bool TestIsReservedIpv6(string ipAddress)
  194. {
  195. return IPIntel.IsAddressReservedIpv6(IPAddress.Parse(ipAddress));
  196. }
  197. private static HttpResponseMessage RespondSuccess()
  198. {
  199. return new HttpResponseMessage(HttpStatusCode.OK)
  200. {
  201. Content = new StringContent("0.5"),
  202. };
  203. }
  204. private static HttpResponseMessage RespondRateLimited()
  205. {
  206. return new HttpResponseMessage(HttpStatusCode.TooManyRequests);
  207. }
  208. private static HttpResponseMessage RespondTestFailed()
  209. {
  210. throw new InvalidOperationException("API should not be queried at this part of the test.");
  211. }
  212. private static HttpResponseMessage RespondError()
  213. {
  214. return new HttpResponseMessage(HttpStatusCode.BadRequest)
  215. {
  216. Content = new StringContent("-4"),
  217. };
  218. }
  219. }
  220. internal sealed class FakeIPIntelApi(Func<HttpResponseMessage> response) : IIPIntelApi
  221. {
  222. public Task<HttpResponseMessage> GetIPScore(IPAddress ip)
  223. {
  224. return Task.FromResult(response());
  225. }
  226. }