Lesson 06 / 20
Denial of Service Attacks
Measuring denial of service through capacity arithmetic: the same 4,000 req/s load producing the same result across four different source distributions, a 4,096-slot state table filling in 1.02 seconds, a per-source rate limit cutting the load to 5 on a single source while changing nothing at 4,000 sources, handshake proof ending slot exhaustion without ever lowering the request rate, and load shedding cutting legitimate traffic to 22%.
Published Updated
In this lesson
- Calculates how source distribution changes the effect of defenses such as rate limiting
- Compares the share of load each defense removes together with the share of legitimate traffic that gets through
Contents
The four lessons so far measured a claim being accepted wrongly: a corrupted record, a wrong address, a wrong source. In all of these measurements the target service stayed up and kept answering requests. But an unverified claim does not only produce a wrong reply.
A connection-opening request does not prove its source. The target does not know whom it is reserving the space it holds for the request for, and a reply to the request may never come at all. Space is limited; when it runs out, legitimate requests are also locked out. Making a service unable to answer requests is called denial of service, and spreading the same load across a large number of sources is called distributed denial of service. The three unverifiable claims counted at the network layer in the first lesson — source address, being part of a fragmented whole, that a control message came from the network — are this lesson’s input. This lesson turns them into capacity arithmetic.
Two Separate Resources Run Out
PS16: a connection-opening request does not prove its source; the target does not know whom it is reserving the space it holds for the request for. PS17: denial of service exhausts two separate resources — request capacity per second and slots in the state table — and the two are counted separately. PS18: a defense does not see the source, only the shape and rate of the request; the same criterion applies to legitimate requests too.
PS17 is the item that frees this lesson’s measurement from a single number. “The service went down” carries no decision; there are two separate accounts that carry the decision. The first is the rate account: how many times the incoming request rate exceeds capacity. The second is the slot account: since every unfinished request holds a slot for a fixed time, how many seconds the table takes to fill. The two accounts respond to different defenses, and it is ordinary for a defense to lower one while changing the other not at all.
The calculation below is a model: the capacity, slot count, and legitimate flow of an ingest endpoint in a fictional measurement network are written as data; there is no real service or real address. The code generates no requests; it only runs the defending side’s arithmetic. Putting load on a service outside your own administration — even for measurement purposes — is both illegal and unprofessional without written authorization and a defined scope.
// networks/capacity.mjs — capacity model of an ingest endpoint in a fictional regional // measurement network: there is no real service, real address, or real measurement. No // randomness. The code only runs the defending side's arithmetic; it generates no requests. // PS16: a connection-opening request does not prove its source; the target does not know // whom it is reserving the space it holds for the request for. // PS17: denial of service exhausts two separate resources: request capacity per second and // slots in the state table. The two are counted separately. export const CAPACITY = 900; // requests/s export const SLOT = 4096; // number of slots in the state table export const HOLD = 30; // time an unfinished request holds a slot (s) export const LEGIT = { sources: 60, rate: 2 }; // 60 sources, 2 requests/s per source // PS18: a defense does not see the source, only the shape and rate of the request; the // same criterion applies to legitimate requests too. export const PROFILE = [ { name: "single source", sources: 1, rate: 4000 }, { name: "twenty sources", sources: 20, rate: 200 }, { name: "five hundred sources", sources: 500, rate: 8 }, { name: "four thousand sources", sources: 4000, rate: 1 }, ]; export const RATE_LIMIT = 5; // allowed requests/s per source export const FILTER_LOAD = 0.90; // share of load weeded out by a coarse pattern export const FILTER_LEGIT = 0.15; // share of legitimate requests caught by the same pattern // Filters a given profile and the legit flow through the given defenses; result is in requests/s. export function measure(p, defenses = []) { let load = p.sources * p.rate, legit = LEGIT.sources * LEGIT.rate; if (defenses.includes("rate")) { load = p.sources * Math.min(p.rate, RATE_LIMIT); legit = LEGIT.sources * Math.min(LEGIT.rate, RATE_LIMIT); } if (defenses.includes("filter")) { load *= 1 - FILTER_LOAD; legit *= 1 - FILTER_LEGIT; } // An unfinished request holds a slot; when handshake proof is on, no slot is reserved. const halfOpen = defenses.includes("proof") ? 0 : load; if (defenses.includes("shed") && load + legit > CAPACITY) { const ratio = CAPACITY / (load + legit); load *= ratio; legit *= ratio; } const occupancy = Math.min(SLOT, halfOpen * HOLD); return { load, legit, total: load + legit, factor: (load + legit) / CAPACITY, occupancy: (occupancy / SLOT) * 100, fillSec: halfOpen * HOLD > SLOT ? SLOT / halfOpen : null, legitShare: (legit / (LEGIT.sources * LEGIT.rate)) * 100, }; }
// networks/capacity-audit.mjs — how many requests from how many sources cross the threshold, how many seconds // the state table takes to fill, which defense lowers which rate, and how much legitimate traffic drops. import { CAPACITY, SLOT, HOLD, LEGIT, PROFILE, RATE_LIMIT, FILTER_LOAD, FILTER_LEGIT, measure } from "./capacity.mjs"; const write = (g, ...s) => console.log(s.map((v, i) => (g[i] < 0 ? String(v).padEnd(-g[i]) : String(v).padStart(g[i]))).join("")); const sec = (x) => (x === null ? "never" : x.toFixed(2)); const n = (x) => Math.round(x); console.log(`capacity ${CAPACITY} req/s, slots ${SLOT}, hold ${HOLD} s, ` + `legit flow ${LEGIT.sources} sources x ${LEGIT.rate} = ${LEGIT.sources * LEGIT.rate} req/s; ` + `threshold ${n(CAPACITY - LEGIT.sources * LEGIT.rate)} req/s`); console.log(`slot threshold ${(SLOT / HOLD).toFixed(1)} req/s: if unfinished requests exceed ` + `this rate the table fills; its ratio to capacity is ${((SLOT / HOLD / CAPACITY) * 100).toFixed(1)}%. ` + `To cross the rate threshold while staying under the ${RATE_LIMIT} req/s cap, ` + `${Math.ceil((CAPACITY - LEGIT.sources * LEGIT.rate) / RATE_LIMIT)} sources suffice.`); const A = [-23, 9, 10, 9, 10, 9, 10]; console.log("\n1. same total rate, four different source distributions (no defense)"); write(A, "profile", "sources", "rate/src", "total", "capacity", "slot %", "fill sec"); for (const p of PROFILE) { const r = measure(p); write(A, p.name, p.sources, p.rate, n(r.total), r.factor.toFixed(2) + "x", r.occupancy.toFixed(0), sec(r.fillSec)); } const B = [-23, 14, 10, 10, 15]; console.log(`\n2. rate limit of ${RATE_LIMIT} req/s per source`); write(B, "profile", "load through", "capacity", "fill sec", "legit through"); for (const p of PROFILE) { const r = measure(p, ["rate"]); write(B, p.name, n(r.load), r.factor.toFixed(2) + "x", sec(r.fillSec), r.legitShare.toFixed(0) + "%"); } const C = [-26, 14, 10, 8, 10, 15]; console.log("\n3. defenses on the most distributed profile (4000 sources x 1 req/s)"); write(C, "defense", "load through", "capacity", "slot %", "fill sec", "legit through"); const hard = PROFILE[3]; for (const s of [[], ["rate"], ["proof"], ["filter"], ["shed"], ["proof", "filter"], ["proof", "filter", "shed"]]) { const r = measure(hard, s); write(C, s.join(" + ") || "none", n(r.load), r.factor.toFixed(2) + "x", r.occupancy.toFixed(0), sec(r.fillSec), r.legitShare.toFixed(0) + "%"); } const D = [-26, -53, 15]; console.log("\n4. defenses' cost to legitimate traffic"); write(D, "defense", "cost", "legit through"); const cost = [ [["rate"], `${RATE_LIMIT} req/s cap per source`], [["proof"], "1 extra round trip per connection"], [["filter"], `${FILTER_LEGIT * 100}% of legitimate requests caught by the pattern`], [["shed"], "requests above capacity are rejected proportionally"], ]; for (const [s, name] of cost) write(D, s.join(" + "), name, measure(hard, s).legitShare.toFixed(0) + "%"); console.log(`the filter pattern weeds out ${FILTER_LOAD * 100}% of the load; the same ` + `pattern also weeds out ${FILTER_LEGIT * 100}% of legitimate requests`);
capacity 900 req/s, slots 4096, hold 30 s, legit flow 60 sources x 2 = 120 req/s; threshold 780 req/s slot threshold 136.5 req/s: if unfinished requests exceed this rate the table fills; its ratio to capacity is 15.2%. To cross the rate threshold while staying under the 5 req/s cap, 156 sources suffice. 1. same total rate, four different source distributions (no defense) profile sources rate/src total capacity slot % fill sec single source 1 4000 4120 4.58x 100 1.02 twenty sources 20 200 4120 4.58x 100 1.02 five hundred sources 500 8 4120 4.58x 100 1.02 four thousand sources 4000 1 4120 4.58x 100 1.02 2. rate limit of 5 req/s per source profile load through capacity fill sec legit through single source 5 0.14x never 100% twenty sources 100 0.24x never 100% five hundred sources 2500 2.91x 1.64 100% four thousand sources 4000 4.58x 1.02 100% 3. defenses on the most distributed profile (4000 sources x 1 req/s) defense load through capacity slot % fill sec legit through none 4000 4.58x 100 1.02 100% rate 4000 4.58x 100 1.02 100% proof 4000 4.58x 0 never 100% filter 400 0.56x 100 10.24 85% shed 874 1.00x 100 1.02 22% proof + filter 400 0.56x 0 never 85% proof + filter + shed 400 0.56x 0 never 85% 4. defenses' cost to legitimate traffic defense cost legit through rate 5 req/s cap per source 100% proof 1 extra round trip per connection 100% filter 15% of legitimate requests caught by the pattern 85% shed requests above capacity are rejected proportionally 22% the filter pattern weeds out 90% of the load; the same pattern also weeds out 15% of legitimate requests
The numbers are in the measurement class; their inputs are PS16–PS18.
Four Distributions, One Result
The first table puts four profiles side by side, and all four produce the same total rate: 4,000 req/s. 4,000 from one source, 200 each from twenty sources, 8 each from five hundred sources, 1 each from four thousand sources. With no defense, all four rows are identical: 4,120 req/s, 4.58 times capacity, slots 100% full, fill time 1.02 seconds.
This equality is the lesson’s starting point. What the target sees is not the number of sources but the number of requests per second; source distribution never enters the target’s accounting. The slot account comes from the same place: if 4,000 unfinished requests arrive per second and each one holds a slot for thirty seconds, the 4,096-slot table fills in 1.02 seconds. For the target, there is no difference at all between four thousand requests from one source and one request each from four thousand sources.
The two thresholds in the header lines also explain why the four profiles give the same result. The rate threshold is 780 req/s: what remains of capacity once the legitimate flow’s share is subtracted. The slot threshold is only 136.5 req/s — 15.2% of capacity. The moment the unfinished-request rate exceeds this number, the table fills, while the rate account still has room to spare. The 5.7-times gap between the two thresholds is the quantitative counterpart of why the two accounts are counted separately: a service can lose its state table at one-sixth of its capacity, and an observation that only watches load will not see it happen.
The second threshold calculation also gives the defending side’s preparation directly. In a service with a 5 req/s cap per source, 156 sources are enough to cross the rate threshold, and none of those sources ever exceeds the cap. Lowering the cap raises the number of sources needed, but the same move also lowers the cap on legitimate endpoints.
The difference shows up once the defense is turned on. The second table applies a 5 req/s cap per source, and the four rows split apart completely. In the single-source profile, the load that gets through drops from 4,000 to 5 — a 99.9% drop. At twenty sources it drops to 100. At five hundred sources it stays at 2,500 and the table fills in 1.64 seconds. At four thousand sources nothing changes: every source is already under the cap.
The effect of a per-source rate limit is the ratio of the rate per source to the cap; once the source count reaches the cap, the limit becomes completely ineffective. The same defense’s effect against the same total load ranges from 99.9% to 0%, and the only thing that determines this is the number of sources on the attacking side.
What Lowers the Rate and What Saves the Slot
The third table runs four defenses separately on the most distributed profile and separates PS17’s two accounts from each other.
Handshake proof — requiring the other side to show it is reachable before space is reserved — lowers slot occupancy from 100% to 0%, and the table never fills. The load that gets through, though, stays at 4,000 and remains 4.58 times capacity. This defense fully solves the slot account and never touches the rate account. 100% of legitimate traffic gets through; its cost is one extra round trip per connection.
Upstream filtering does the opposite. It weeds out 90% of the load with a coarse pattern: the load that gets through drops from 4,000 to 400, and the capacity ratio drops from 4.58 times to 0.56 times. The slot account, however, is not solved; the table still fills, just in 10.24 seconds instead of 1.02. Its cost is legitimate requests caught by the same pattern: 15% of legitimate traffic is also weeded out.
When the two defenses are turned on together, both accounts are solved: load through 0.56 times, slot occupancy 0%, legit through 85%. This is the best row in the table, and it shows the two defenses add up because they protect different resources. In the third lesson, two defenses protecting the same record did not add up; here they add up because one lowers the slot and the other lowers the rate.
The Rate of Load Shedding
The fourth row measures load shedding: not accepting any request above capacity at all. The total that gets through sits exactly at capacity — 1.00 times — and this is the only “target stays up” row in the table. Its cost is in the last column: only 22% of legitimate traffic gets through.
The reason is proportioning. Load shedding cannot distinguish a request’s source; it only cuts the total, so when capacity is prorated against a 4,120-request flow, every source gets the same share. Because the legitimate flow makes up 2.9% of the total, it also gets 2.9% of capacity. A defense that cuts without distinguishing protects legitimate traffic only in proportion to its share of the total; if the share is small, the protection is small too.
The last row completes this: with handshake proof and filtering both on, load shedding changes nothing, because the total that gets through is already under capacity. Load shedding remains a last resort and only comes into play when the other two defenses are not enough. Where the defense falls short is plain in this table: none of the four defenses makes the unverified claim verifiable. The source address’s claim is still unproven; the only thing measured is how many requests and how many slots that unproven claim is enough to consume.
Summary
- Denial of service exhausts two separate resources: 900 req/s of capacity and a 4,096-slot state table; a 4,000 req/s load exceeds capacity by 4.58 times and fills the table in 1.02 seconds.
- The same total rate gives an undefended target the same result across four different source distributions; the number of sources never enters the target’s accounting.
- A 5 req/s cap per source lowers the load from 4,000 to 5 at a single source and changes nothing at four thousand sources; its effect is the ratio of the per-source rate to the cap.
- Handshake proof lowers slot occupancy from 100% to 0% and never lowers the request rate; upstream filtering lowers the rate to 0.56 times, does not save the slot, and weeds out 15% of legitimate traffic. Together the two solve both accounts and let 85% of legitimate traffic through.
- Load shedding sits the total at capacity but, because it does not distinguish, protects legitimate traffic only in proportion to its share: 22%. None of the four defenses makes the source-address claim verifiable.
Next Step
This lesson measured the quantity of an unproven claim: how many requests, how many slots, how many seconds. When load drops or the defense is enough, the service stays up and the measurement ends. But the same unproven claim can also be used without consuming any capacity and without tripping any counter. If the two ends of a connection do not recognize each other by proof, every hop between them can present itself as the other side, and the connection continues at its ordinary rate and ordinary volume. The next lesson measures this as a missing chain of evidence: how many hops a path has, at how many hops the evidence chain breaks, how many of them endpoint verification closes off, and how many hops stay open when the chain’s own builder is itself unverified.
To keep your progress and take notes, Log in
My notes
Log in to take notes.