blob: 8e7e38b5592e212538916db7895f420722e35a6a [file] [log] [blame]
Avi Drissman60039d42022-09-13 21:49:051// Copyright 2012 The Chromium Authors
xunjieli413a68782015-06-16 17:15:432// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
Tom Sepez8726d30e2025-01-29 02:11:085#ifdef UNSAFE_BUFFERS_BUILD
6// TODO(crbug.com/390223051): Remove C-library calls to fix the errors.
7#pragma allow_unsafe_libc_calls
8#endif
9
xunjieli413a68782015-06-16 17:15:4310// The tests in this file attempt to verify the following through simulation:
11// a) That a server experiencing overload will actually benefit from the
12// anti-DDoS throttling logic, i.e. that its traffic spike will subside
13// and be distributed over a longer period of time;
14// b) That "well-behaved" clients of a server under DDoS attack actually
15// benefit from the anti-DDoS throttling logic; and
16// c) That the approximate increase in "perceived downtime" introduced by
17// anti-DDoS throttling for various different actual downtimes is what
18// we expect it to be.
19
Hans Wennborg5a9d1552022-09-08 12:31:2720#include <stdarg.h>
avic9cec102015-12-23 00:39:2621#include <stddef.h>
22
David Bertonib2a6e2c2024-12-20 23:46:0523#include <array>
xunjieli413a68782015-06-16 17:15:4324#include <cmath>
25#include <limits>
dchengf5d241082016-04-21 03:43:1126#include <memory>
xunjieli413a68782015-06-16 17:15:4327#include <vector>
28
29#include "base/environment.h"
dchengf5d241082016-04-21 03:43:1130#include "base/memory/ptr_util.h"
Kalvin Leebc3754ae2023-10-01 22:37:3431#include "base/memory/raw_ptr.h"
xunjieli413a68782015-06-16 17:15:4332#include "base/rand_util.h"
Gabriel Charettec7108742019-08-23 03:31:4033#include "base/test/task_environment.h"
xunjieli413a68782015-06-16 17:15:4334#include "base/time/time.h"
Chris Mumfordc54b0c342018-07-23 21:29:4635#include "extensions/renderer/extension_throttle_entry.h"
Chris Mumford3f0eda92018-07-23 14:51:1736#include "extensions/renderer/extension_throttle_test_support.h"
xunjieli413a68782015-06-16 17:15:4337#include "testing/gtest/include/gtest/gtest.h"
38
xunjieli413a68782015-06-16 17:15:4339using base::TimeTicks;
40using net::BackoffEntry;
xunjieli413a68782015-06-16 17:15:4341
42namespace extensions {
43namespace {
44
45// Set this variable in your environment if you want to see verbose results
46// of the simulation tests.
47const char kShowSimulationVariableName[] = "SHOW_SIMULATION_RESULTS";
48
49// Prints output only if a given environment variable is set. We use this
50// to not print any output for human evaluation when the test is run without
51// supervision.
52void VerboseOut(const char* format, ...) {
53 static bool have_checked_environment = false;
54 static bool should_print = false;
55 if (!have_checked_environment) {
56 have_checked_environment = true;
dchengf5d241082016-04-21 03:43:1157 std::unique_ptr<base::Environment> env(base::Environment::Create());
xunjieli413a68782015-06-16 17:15:4358 if (env->HasVar(kShowSimulationVariableName))
59 should_print = true;
60 }
61
62 if (should_print) {
63 va_list arglist;
64 va_start(arglist, format);
65 vprintf(format, arglist);
66 va_end(arglist);
67 }
68}
69
70// A simple two-phase discrete time simulation. Actors are added in the order
71// they should take action at every tick of the clock. Ticks of the clock
72// are two-phase:
73// - Phase 1 advances every actor's time to a new absolute time.
74// - Phase 2 asks each actor to perform their action.
75class DiscreteTimeSimulation {
76 public:
77 class Actor {
78 public:
79 virtual ~Actor() {}
80 virtual void AdvanceTime(const TimeTicks& absolute_time) = 0;
81 virtual void PerformAction() = 0;
82 };
83
84 DiscreteTimeSimulation() {}
85
Peter Boströmc3d907172021-09-24 18:39:5286 DiscreteTimeSimulation(const DiscreteTimeSimulation&) = delete;
87 DiscreteTimeSimulation& operator=(const DiscreteTimeSimulation&) = delete;
88
xunjieli413a68782015-06-16 17:15:4389 // Adds an |actor| to the simulation. The client of the simulation maintains
90 // ownership of |actor| and must ensure its lifetime exceeds that of the
91 // simulation. Actors should be added in the order you wish for them to
92 // act at each tick of the simulation.
93 void AddActor(Actor* actor) { actors_.push_back(actor); }
94
95 // Runs the simulation for, pretending |time_between_ticks| passes from one
96 // tick to the next. The start time will be the current real time. The
97 // simulation will stop when the simulated duration is equal to or greater
98 // than |maximum_simulated_duration|.
Peter Kasting53fd6ee2021-10-05 20:40:4899 void RunSimulation(const base::TimeDelta& maximum_simulated_duration,
100 const base::TimeDelta& time_between_ticks) {
xunjieli413a68782015-06-16 17:15:43101 TimeTicks start_time = TimeTicks();
102 TimeTicks now = start_time;
103 while ((now - start_time) <= maximum_simulated_duration) {
jdoerriea1e1598b2018-10-10 09:10:37104 for (auto it = actors_.begin(); it != actors_.end(); ++it) {
xunjieli413a68782015-06-16 17:15:43105 (*it)->AdvanceTime(now);
106 }
107
jdoerriea1e1598b2018-10-10 09:10:37108 for (auto it = actors_.begin(); it != actors_.end(); ++it) {
xunjieli413a68782015-06-16 17:15:43109 (*it)->PerformAction();
110 }
111
112 now += time_between_ticks;
113 }
114 }
115
116 private:
Ali Hijazie63cbaf62023-12-20 19:29:35117 std::vector<raw_ptr<Actor, VectorExperimental>> actors_;
xunjieli413a68782015-06-16 17:15:43118};
119
120// Represents a web server in a simulation of a server under attack by
121// a lot of clients. Must be added to the simulation's list of actors
122// after all |Requester| objects.
123class Server : public DiscreteTimeSimulation::Actor {
124 public:
125 Server(int max_queries_per_tick, double request_drop_ratio)
126 : max_queries_per_tick_(max_queries_per_tick),
127 request_drop_ratio_(request_drop_ratio),
128 num_overloaded_ticks_remaining_(0),
129 num_current_tick_queries_(0),
130 num_overloaded_ticks_(0),
Chris Mumford3f0eda92018-07-23 14:51:17131 max_experienced_queries_per_tick_(0) {}
xunjieli413a68782015-06-16 17:15:43132
Peter Boströmc3d907172021-09-24 18:39:52133 Server(const Server&) = delete;
134 Server& operator=(const Server&) = delete;
135
Peter Kasting53fd6ee2021-10-05 20:40:48136 void SetDowntime(const TimeTicks& start_time,
137 const base::TimeDelta& duration) {
xunjieli413a68782015-06-16 17:15:43138 start_downtime_ = start_time;
139 end_downtime_ = start_time + duration;
140 }
141
142 void AdvanceTime(const TimeTicks& absolute_time) override {
143 now_ = absolute_time;
144 }
145
146 void PerformAction() override {
147 // We are inserted at the end of the actor's list, so all Requester
148 // instances have already done their bit.
149 if (num_current_tick_queries_ > max_experienced_queries_per_tick_)
150 max_experienced_queries_per_tick_ = num_current_tick_queries_;
151
152 if (num_current_tick_queries_ > max_queries_per_tick_) {
153 // We pretend the server fails for the next several ticks after it
154 // gets overloaded.
155 num_overloaded_ticks_remaining_ = 5;
156 ++num_overloaded_ticks_;
157 } else if (num_overloaded_ticks_remaining_ > 0) {
158 --num_overloaded_ticks_remaining_;
159 }
160
161 requests_per_tick_.push_back(num_current_tick_queries_);
162 num_current_tick_queries_ = 0;
163 }
164
165 // This is called by Requester. It returns the response code from
166 // the server.
167 int HandleRequest() {
168 ++num_current_tick_queries_;
169 if (!start_downtime_.is_null() && start_downtime_ < now_ &&
170 now_ < end_downtime_) {
171 // For the simulation measuring the increase in perceived
172 // downtime, it might be interesting to count separately the
173 // queries seen by the server (assuming a front-end reverse proxy
174 // is what actually serves up the 503s in this case) so that we could
175 // visualize the traffic spike seen by the server when it comes up,
176 // which would in many situations be ameliorated by the anti-DDoS
177 // throttling.
178 return 503;
179 }
180
181 if ((num_overloaded_ticks_remaining_ > 0 ||
182 num_current_tick_queries_ > max_queries_per_tick_) &&
183 base::RandDouble() < request_drop_ratio_) {
184 return 503;
185 }
186
187 return 200;
188 }
189
190 int num_overloaded_ticks() const { return num_overloaded_ticks_; }
191
192 int max_experienced_queries_per_tick() const {
193 return max_experienced_queries_per_tick_;
194 }
195
xunjieli413a68782015-06-16 17:15:43196 std::string VisualizeASCII(int terminal_width) {
197 // Account for | characters we place at left of graph.
198 terminal_width -= 1;
199
200 VerboseOut("Overloaded for %d of %d ticks.\n", num_overloaded_ticks_,
201 requests_per_tick_.size());
202 VerboseOut("Got maximum of %d requests in a tick.\n\n",
203 max_experienced_queries_per_tick_);
204
205 VerboseOut("Traffic graph:\n\n");
206
207 // Printing the graph like this is a bit overkill, but was very useful
208 // while developing the various simulations to see if they were testing
209 // the corner cases we want to simulate.
210
211 // Find the smallest number of whole ticks we need to group into a
212 // column that will let all ticks fit into the column width we have.
213 int num_ticks = requests_per_tick_.size();
214 double ticks_per_column_exact =
215 static_cast<double>(num_ticks) / static_cast<double>(terminal_width);
216 int ticks_per_column = std::ceil(ticks_per_column_exact);
217 DCHECK_GE(ticks_per_column * terminal_width, num_ticks);
218
219 // Sum up the column values.
220 int num_columns = num_ticks / ticks_per_column;
221 if (num_ticks % ticks_per_column)
222 ++num_columns;
223 DCHECK_LE(num_columns, terminal_width);
Maks Orlovich04a56de2024-05-21 18:36:02224 std::vector<int> columns(num_columns);
xunjieli413a68782015-06-16 17:15:43225 for (int tx = 0; tx < num_ticks; ++tx) {
226 int cx = tx / ticks_per_column;
227 if (tx % ticks_per_column == 0)
228 columns[cx] = 0;
229 columns[cx] += requests_per_tick_[tx];
230 }
231
232 // Find the lowest integer divisor that will let the column values
233 // be represented in a graph of maximum height 50.
234 int max_value = 0;
235 for (int cx = 0; cx < num_columns; ++cx)
236 max_value = std::max(max_value, columns[cx]);
237 const int kNumRows = 50;
238 double row_divisor_exact = max_value / static_cast<double>(kNumRows);
239 int row_divisor = std::ceil(row_divisor_exact);
240 DCHECK_GE(row_divisor * kNumRows, max_value);
241
242 // To show the overload line, we calculate the appropriate value.
243 int overload_value = max_queries_per_tick_ * ticks_per_column;
244
245 // When num_ticks is not a whole multiple of ticks_per_column, the last
246 // column includes fewer ticks than the others. In this case, don't
247 // print it so that we don't show an inconsistent value.
248 int num_printed_columns = num_columns;
249 if (num_ticks % ticks_per_column)
250 --num_printed_columns;
251
252 // This is a top-to-bottom traversal of rows, left-to-right per row.
253 std::string output;
254 for (int rx = 0; rx < kNumRows; ++rx) {
255 int range_min = (kNumRows - rx) * row_divisor;
256 int range_max = range_min + row_divisor;
257 if (range_min == 0)
258 range_min = -1; // Make 0 values fit in the bottom range.
259 output.append("|");
260 for (int cx = 0; cx < num_printed_columns; ++cx) {
261 char block = ' ';
262 // Show the overload line.
263 if (range_min < overload_value && overload_value <= range_max)
264 block = '-';
265
266 // Preferentially, show the graph line.
267 if (range_min < columns[cx] && columns[cx] <= range_max)
268 block = '#';
269
270 output.append(1, block);
271 }
272 output.append("\n");
273 }
274 output.append("|");
275 output.append(num_printed_columns, '=');
276
277 return output;
278 }
279
xunjieli413a68782015-06-16 17:15:43280 private:
281 TimeTicks now_;
282 TimeTicks start_downtime_; // Can be 0 to say "no downtime".
283 TimeTicks end_downtime_;
284 const int max_queries_per_tick_;
285 const double request_drop_ratio_; // Ratio of requests to 503 when failing.
286 int num_overloaded_ticks_remaining_;
287 int num_current_tick_queries_;
288 int num_overloaded_ticks_;
289 int max_experienced_queries_per_tick_;
290 std::vector<int> requests_per_tick_;
xunjieli413a68782015-06-16 17:15:43291};
292
293// Mock throttler entry used by Requester class.
294class MockExtensionThrottleEntry : public ExtensionThrottleEntry {
295 public:
Chris Mumfordc54b0c342018-07-23 21:29:46296 MockExtensionThrottleEntry()
297 : ExtensionThrottleEntry(std::string()),
xunjieli413a68782015-06-16 17:15:43298 backoff_entry_(&backoff_policy_, &fake_clock_) {}
299
Chris Mumfordc54b0c342018-07-23 21:29:46300 ~MockExtensionThrottleEntry() override {}
301
xunjieli413a68782015-06-16 17:15:43302 const BackoffEntry* GetBackoffEntry() const override {
303 return &backoff_entry_;
304 }
305
306 BackoffEntry* GetBackoffEntry() override { return &backoff_entry_; }
307
308 TimeTicks ImplGetTimeNow() const override { return fake_clock_.NowTicks(); }
309
310 void SetFakeNow(const TimeTicks& fake_time) {
311 fake_clock_.set_now(fake_time);
312 }
313
xunjieli413a68782015-06-16 17:15:43314 private:
315 mutable TestTickClock fake_clock_;
316 BackoffEntry backoff_entry_;
317};
318
319// Registry of results for a class of |Requester| objects (e.g. attackers vs.
320// regular clients).
321class RequesterResults {
322 public:
323 RequesterResults()
324 : num_attempts_(0), num_successful_(0), num_failed_(0), num_blocked_(0) {}
325
326 void AddSuccess() {
327 ++num_attempts_;
328 ++num_successful_;
329 }
330
331 void AddFailure() {
332 ++num_attempts_;
333 ++num_failed_;
334 }
335
336 void AddBlocked() {
337 ++num_attempts_;
338 ++num_blocked_;
339 }
340
341 int num_attempts() const { return num_attempts_; }
342 int num_successful() const { return num_successful_; }
343 int num_failed() const { return num_failed_; }
344 int num_blocked() const { return num_blocked_; }
345
346 double GetBlockedRatio() {
347 DCHECK(num_attempts_);
348 return static_cast<double>(num_blocked_) /
349 static_cast<double>(num_attempts_);
350 }
351
352 double GetSuccessRatio() {
353 DCHECK(num_attempts_);
354 return static_cast<double>(num_successful_) /
355 static_cast<double>(num_attempts_);
356 }
357
358 void PrintResults(const char* class_description) {
359 if (num_attempts_ == 0) {
360 VerboseOut("No data for %s\n", class_description);
361 return;
362 }
363
364 VerboseOut("Requester results for %s\n", class_description);
365 VerboseOut(" %d attempts\n", num_attempts_);
366 VerboseOut(" %d successes\n", num_successful_);
367 VerboseOut(" %d 5xx responses\n", num_failed_);
368 VerboseOut(" %d requests blocked\n", num_blocked_);
369 VerboseOut(" %.2f success ratio\n", GetSuccessRatio());
370 VerboseOut(" %.2f blocked ratio\n", GetBlockedRatio());
371 VerboseOut("\n");
372 }
373
374 private:
375 int num_attempts_;
376 int num_successful_;
377 int num_failed_;
378 int num_blocked_;
379};
380
381// Represents an Requester in a simulated DDoS situation, that periodically
382// requests a specific resource.
383class Requester : public DiscreteTimeSimulation::Actor {
384 public:
Chris Mumfordc54b0c342018-07-23 21:29:46385 Requester(std::unique_ptr<MockExtensionThrottleEntry> throttler_entry,
Peter Kasting53fd6ee2021-10-05 20:40:48386 const base::TimeDelta& time_between_requests,
xunjieli413a68782015-06-16 17:15:43387 Server* server,
388 RequesterResults* results)
Chris Mumfordc54b0c342018-07-23 21:29:46389 : throttler_entry_(std::move(throttler_entry)),
xunjieli413a68782015-06-16 17:15:43390 time_between_requests_(time_between_requests),
391 last_attempt_was_failure_(false),
392 server_(server),
393 results_(results) {
394 DCHECK(server_);
395 }
396
Peter Boströmc3d907172021-09-24 18:39:52397 Requester(const Requester&) = delete;
398 Requester& operator=(const Requester&) = delete;
399
xunjieli413a68782015-06-16 17:15:43400 void AdvanceTime(const TimeTicks& absolute_time) override {
401 if (time_of_last_success_.is_null())
402 time_of_last_success_ = absolute_time;
403
404 throttler_entry_->SetFakeNow(absolute_time);
405 }
406
407 void PerformAction() override {
Peter Kastingf18c8ca2023-10-04 16:31:51408 const auto current_jitter =
409 request_jitter_.is_zero()
410 ? base::TimeDelta()
411 : base::RandTimeDelta(-request_jitter_, request_jitter_);
Peter Kasting53fd6ee2021-10-05 20:40:48412 const base::TimeDelta effective_delay =
Peter Kastingf18c8ca2023-10-04 16:31:51413 time_between_requests_ + current_jitter;
xunjieli413a68782015-06-16 17:15:43414
415 if (throttler_entry_->ImplGetTimeNow() - time_of_last_attempt_ >
416 effective_delay) {
Matt Menke21850502019-10-09 22:34:24417 if (!throttler_entry_->ShouldRejectRequest()) {
xunjieli413a68782015-06-16 17:15:43418 int status_code = server_->HandleRequest();
419 throttler_entry_->UpdateWithResponse(status_code);
420
421 if (status_code == 200) {
422 if (results_)
423 results_->AddSuccess();
424
425 if (last_attempt_was_failure_) {
426 last_downtime_duration_ =
427 throttler_entry_->ImplGetTimeNow() - time_of_last_success_;
428 }
429
430 time_of_last_success_ = throttler_entry_->ImplGetTimeNow();
431 last_attempt_was_failure_ = false;
432 } else {
433 if (results_)
434 results_->AddFailure();
435 last_attempt_was_failure_ = true;
436 }
437 } else {
438 if (results_)
439 results_->AddBlocked();
440 last_attempt_was_failure_ = true;
441 }
442
443 time_of_last_attempt_ = throttler_entry_->ImplGetTimeNow();
444 }
445 }
446
447 // Adds a delay until the first request, equal to a uniformly distributed
448 // value between now and now + max_delay.
Peter Kasting53fd6ee2021-10-05 20:40:48449 void SetStartupJitter(const base::TimeDelta& max_delay) {
Peter Kastingf18c8ca2023-10-04 16:31:51450 time_of_last_attempt_ = TimeTicks() + base::RandTimeDeltaUpTo(max_delay) -
451 time_between_requests_;
xunjieli413a68782015-06-16 17:15:43452 }
453
Peter Kasting53fd6ee2021-10-05 20:40:48454 void SetRequestJitter(const base::TimeDelta& request_jitter) {
xunjieli413a68782015-06-16 17:15:43455 request_jitter_ = request_jitter;
456 }
457
Peter Kasting53fd6ee2021-10-05 20:40:48458 base::TimeDelta last_downtime_duration() const {
459 return last_downtime_duration_;
460 }
xunjieli413a68782015-06-16 17:15:43461
462 private:
Chris Mumfordc54b0c342018-07-23 21:29:46463 std::unique_ptr<MockExtensionThrottleEntry> throttler_entry_;
Peter Kasting53fd6ee2021-10-05 20:40:48464 const base::TimeDelta time_between_requests_;
465 base::TimeDelta request_jitter_;
xunjieli413a68782015-06-16 17:15:43466 TimeTicks time_of_last_attempt_;
467 TimeTicks time_of_last_success_;
468 bool last_attempt_was_failure_;
Peter Kasting53fd6ee2021-10-05 20:40:48469 base::TimeDelta last_downtime_duration_;
Bartek Nowierskif473c24b2024-02-20 17:51:15470 const raw_ptr<Server> server_;
471 const raw_ptr<RequesterResults> results_; // May be nullptr.
xunjieli413a68782015-06-16 17:15:43472};
473
474void SimulateAttack(Server* server,
475 RequesterResults* attacker_results,
476 RequesterResults* client_results,
477 bool enable_throttling) {
478 const size_t kNumAttackers = 50;
479 const size_t kNumClients = 50;
480 DiscreteTimeSimulation simulation;
dchengf5d241082016-04-21 03:43:11481 std::vector<std::unique_ptr<Requester>> requesters;
xunjieli413a68782015-06-16 17:15:43482 for (size_t i = 0; i < kNumAttackers; ++i) {
483 // Use a tiny time_between_requests so the attackers will ping the
484 // server at every tick of the simulation.
Chris Mumfordc54b0c342018-07-23 21:29:46485 auto throttler_entry = std::make_unique<MockExtensionThrottleEntry>();
xunjieli413a68782015-06-16 17:15:43486 if (!enable_throttling)
487 throttler_entry->DisableBackoffThrottling();
488
489 Requester* attacker =
Peter Kasting53fd6ee2021-10-05 20:40:48490 new Requester(std::move(throttler_entry), base::Milliseconds(1), server,
491 attacker_results);
492 attacker->SetStartupJitter(base::Seconds(120));
dchengf5d241082016-04-21 03:43:11493 requesters.push_back(base::WrapUnique(attacker));
xunjieli413a68782015-06-16 17:15:43494 simulation.AddActor(attacker);
495 }
496 for (size_t i = 0; i < kNumClients; ++i) {
497 // Normal clients only make requests every 2 minutes, plus/minus 1 minute.
Chris Mumfordc54b0c342018-07-23 21:29:46498 auto throttler_entry = std::make_unique<MockExtensionThrottleEntry>();
xunjieli413a68782015-06-16 17:15:43499 if (!enable_throttling)
500 throttler_entry->DisableBackoffThrottling();
501
Peter Kasting53fd6ee2021-10-05 20:40:48502 Requester* client = new Requester(std::move(throttler_entry),
503 base::Minutes(2), server, client_results);
504 client->SetStartupJitter(base::Seconds(120));
505 client->SetRequestJitter(base::Minutes(1));
dchengf5d241082016-04-21 03:43:11506 requesters.push_back(base::WrapUnique(client));
xunjieli413a68782015-06-16 17:15:43507 simulation.AddActor(client);
508 }
509 simulation.AddActor(server);
510
Peter Kasting53fd6ee2021-10-05 20:40:48511 simulation.RunSimulation(base::Minutes(6), base::Seconds(1));
xunjieli413a68782015-06-16 17:15:43512}
513
514TEST(URLRequestThrottlerSimulation, HelpsInAttack) {
Gabriel Charetteee4f1c02019-09-07 14:07:38515 base::test::SingleThreadTaskEnvironment task_environment(
516 base::test::SingleThreadTaskEnvironment::MainThreadType::IO);
xunjieli413a68782015-06-16 17:15:43517 Server unprotected_server(30, 1.0);
518 RequesterResults unprotected_attacker_results;
519 RequesterResults unprotected_client_results;
520 Server protected_server(30, 1.0);
521 RequesterResults protected_attacker_results;
522 RequesterResults protected_client_results;
523 SimulateAttack(&unprotected_server, &unprotected_attacker_results,
524 &unprotected_client_results, false);
525 SimulateAttack(&protected_server, &protected_attacker_results,
526 &protected_client_results, true);
527
528 // These assert that the DDoS protection actually benefits the
529 // server. Manual inspection of the traffic graphs will show this
530 // even more clearly.
531 EXPECT_GT(unprotected_server.num_overloaded_ticks(),
532 protected_server.num_overloaded_ticks());
533 EXPECT_GT(unprotected_server.max_experienced_queries_per_tick(),
534 protected_server.max_experienced_queries_per_tick());
535
536 // These assert that the DDoS protection actually benefits non-malicious
537 // (and non-degenerate/accidentally DDoSing) users.
538 EXPECT_LT(protected_client_results.GetBlockedRatio(),
539 protected_attacker_results.GetBlockedRatio());
540 EXPECT_GT(protected_client_results.GetSuccessRatio(),
541 unprotected_client_results.GetSuccessRatio());
542
543 // The rest is just for optional manual evaluation of the results;
544 // in particular the traffic pattern is interesting.
545
546 VerboseOut("\nUnprotected server's results:\n\n");
547 VerboseOut(unprotected_server.VisualizeASCII(132).c_str());
548 VerboseOut("\n\n");
549 VerboseOut("Protected server's results:\n\n");
550 VerboseOut(protected_server.VisualizeASCII(132).c_str());
551 VerboseOut("\n\n");
552
553 unprotected_attacker_results.PrintResults(
554 "attackers attacking unprotected server.");
555 unprotected_client_results.PrintResults(
556 "normal clients making requests to unprotected server.");
557 protected_attacker_results.PrintResults(
558 "attackers attacking protected server.");
559 protected_client_results.PrintResults(
560 "normal clients making requests to protected server.");
561}
562
563// Returns the downtime perceived by the client, as a ratio of the
564// actual downtime.
Peter Kasting53fd6ee2021-10-05 20:40:48565double SimulateDowntime(const base::TimeDelta& duration,
566 const base::TimeDelta& average_client_interval,
xunjieli413a68782015-06-16 17:15:43567 bool enable_throttling) {
Peter Kasting53fd6ee2021-10-05 20:40:48568 base::TimeDelta time_between_ticks = duration / 200;
xunjieli413a68782015-06-16 17:15:43569 TimeTicks start_downtime = TimeTicks() + (duration / 2);
570
571 // A server that never rejects requests, but will go down for maintenance.
572 Server server(std::numeric_limits<int>::max(), 1.0);
573 server.SetDowntime(start_downtime, duration);
574
Chris Mumfordc54b0c342018-07-23 21:29:46575 auto throttler_entry = std::make_unique<MockExtensionThrottleEntry>();
xunjieli413a68782015-06-16 17:15:43576 if (!enable_throttling)
577 throttler_entry->DisableBackoffThrottling();
578
Chris Mumfordc54b0c342018-07-23 21:29:46579 Requester requester(std::move(throttler_entry), average_client_interval,
Bartek Nowierski7463fdb2020-06-25 14:19:46580 &server, nullptr);
xunjieli413a68782015-06-16 17:15:43581 requester.SetStartupJitter(duration / 3);
582 requester.SetRequestJitter(average_client_interval);
583
584 DiscreteTimeSimulation simulation;
585 simulation.AddActor(&requester);
586 simulation.AddActor(&server);
587
588 simulation.RunSimulation(duration * 2, time_between_ticks);
589
590 return static_cast<double>(
591 requester.last_downtime_duration().InMilliseconds()) /
592 static_cast<double>(duration.InMilliseconds());
593}
594
595TEST(URLRequestThrottlerSimulation, PerceivedDowntimeRatio) {
Gabriel Charetteee4f1c02019-09-07 14:07:38596 base::test::SingleThreadTaskEnvironment task_environment(
597 base::test::SingleThreadTaskEnvironment::MainThreadType::IO);
xunjieli413a68782015-06-16 17:15:43598 struct Stats {
599 // Expected interval that we expect the ratio of downtime when anti-DDoS
600 // is enabled and downtime when anti-DDoS is not enabled to fall within.
601 //
602 // The expected interval depends on two things: The exponential back-off
603 // policy encoded in ExtensionThrottleEntry, and the test or set of
604 // tests that the Stats object is tracking (e.g. a test where the client
605 // retries very rapidly on a very long downtime will tend to increase the
606 // number).
607 //
608 // To determine an appropriate new interval when parameters have changed,
609 // run the test a few times (you may have to Ctrl-C out of it after a few
610 // seconds) and choose an interval that the test converges quickly and
611 // reliably to. Then set the new interval, and run the test e.g. 20 times
612 // in succession to make sure it never takes an obscenely long time to
613 // converge to this interval.
614 double expected_min_increase;
615 double expected_max_increase;
616
617 size_t num_runs;
618 double total_ratio_unprotected;
619 double total_ratio_protected;
620
621 bool DidConverge(double* increase_ratio_out) {
622 double unprotected_ratio = total_ratio_unprotected / num_runs;
623 double protected_ratio = total_ratio_protected / num_runs;
624 double increase_ratio = protected_ratio / unprotected_ratio;
625 if (increase_ratio_out)
626 *increase_ratio_out = increase_ratio;
627 return expected_min_increase <= increase_ratio &&
628 increase_ratio <= expected_max_increase;
629 }
630
631 void ReportTrialResult(double increase_ratio) {
632 VerboseOut(
633 " Perceived downtime with throttling is %.4f times without.\n",
634 increase_ratio);
635 VerboseOut(" Test result after %d trials.\n", num_runs);
636 }
637 };
638
639 Stats global_stats = {1.08, 1.15};
640
641 struct Trial {
Peter Kasting53fd6ee2021-10-05 20:40:48642 base::TimeDelta duration;
643 base::TimeDelta average_client_interval;
xunjieli413a68782015-06-16 17:15:43644 Stats stats;
645
646 void PrintTrialDescription() {
Peter Kastinge5a38ed2021-10-02 03:06:35647 const double duration_minutes = duration / base::Minutes(1);
Peter Kasting6fdae8262020-08-14 03:24:13648 const double interval_minutes =
Peter Kastinge5a38ed2021-10-02 03:06:35649 average_client_interval / base::Minutes(1);
xunjieli413a68782015-06-16 17:15:43650 VerboseOut("Trial with %.2f min downtime, avg. interval %.2f min.\n",
651 duration_minutes, interval_minutes);
652 }
653 };
654
655 // We don't set or check expected ratio intervals on individual
656 // experiments as this might make the test too fragile, but we
657 // print them out at the end for manual evaluation (we want to be
658 // able to make claims about the expected ratios depending on the
659 // type of behavior of the client and the downtime, e.g. the difference
660 // in behavior between a client making requests every few minutes vs.
661 // one that makes a request every 15 seconds).
David Bertonib2a6e2c2024-12-20 23:46:05662 auto trials = std::to_array<Trial>({
Peter Kasting53fd6ee2021-10-05 20:40:48663 {base::Seconds(10), base::Seconds(3)},
664 {base::Seconds(30), base::Seconds(7)},
665 {base::Minutes(5), base::Seconds(30)},
666 {base::Minutes(10), base::Seconds(20)},
667 {base::Minutes(20), base::Seconds(15)},
668 {base::Minutes(20), base::Seconds(50)},
669 {base::Minutes(30), base::Minutes(2)},
670 {base::Minutes(30), base::Minutes(5)},
671 {base::Minutes(40), base::Minutes(7)},
672 {base::Minutes(40), base::Minutes(2)},
673 {base::Minutes(40), base::Seconds(15)},
674 {base::Minutes(60), base::Minutes(7)},
675 {base::Minutes(60), base::Minutes(2)},
676 {base::Minutes(60), base::Seconds(15)},
677 {base::Minutes(80), base::Minutes(20)},
678 {base::Minutes(80), base::Minutes(3)},
679 {base::Minutes(80), base::Seconds(15)},
xunjieli413a68782015-06-16 17:15:43680
681 // Most brutal?
Peter Kasting53fd6ee2021-10-05 20:40:48682 {base::Minutes(45), base::Milliseconds(500)},
David Bertonib2a6e2c2024-12-20 23:46:05683 });
xunjieli413a68782015-06-16 17:15:43684
685 // If things don't converge by the time we've done 100K trials, then
686 // clearly one or more of the expected intervals are wrong.
687 while (global_stats.num_runs < 100000) {
Daniel Chengd54aee22022-02-26 09:26:32688 for (size_t i = 0; i < std::size(trials); ++i) {
xunjieli413a68782015-06-16 17:15:43689 ++global_stats.num_runs;
690 ++trials[i].stats.num_runs;
691 double ratio_unprotected = SimulateDowntime(
692 trials[i].duration, trials[i].average_client_interval, false);
693 double ratio_protected = SimulateDowntime(
694 trials[i].duration, trials[i].average_client_interval, true);
695 global_stats.total_ratio_unprotected += ratio_unprotected;
696 global_stats.total_ratio_protected += ratio_protected;
697 trials[i].stats.total_ratio_unprotected += ratio_unprotected;
698 trials[i].stats.total_ratio_protected += ratio_protected;
699 }
700
701 double increase_ratio;
702 if (global_stats.DidConverge(&increase_ratio))
703 break;
704
705 if (global_stats.num_runs > 200) {
706 VerboseOut("Test has not yet converged on expected interval.\n");
707 global_stats.ReportTrialResult(increase_ratio);
708 }
709 }
710
711 double average_increase_ratio;
712 EXPECT_TRUE(global_stats.DidConverge(&average_increase_ratio));
713
714 // Print individual trial results for optional manual evaluation.
715 double max_increase_ratio = 0.0;
Daniel Chengd54aee22022-02-26 09:26:32716 for (size_t i = 0; i < std::size(trials); ++i) {
xunjieli413a68782015-06-16 17:15:43717 double increase_ratio;
718 trials[i].stats.DidConverge(&increase_ratio);
719 max_increase_ratio = std::max(max_increase_ratio, increase_ratio);
720 trials[i].PrintTrialDescription();
721 trials[i].stats.ReportTrialResult(increase_ratio);
722 }
723
724 VerboseOut("Average increase ratio was %.4f\n", average_increase_ratio);
725 VerboseOut("Maximum increase ratio was %.4f\n", max_increase_ratio);
726}
727
728} // namespace
729} // namespace extensions