DDoS & Rate Limiting

Where a flood is actually absorbed, and what each layer charges for absorbing it. Reads the SYN-queue admission test, SYN cookies and their real cost, the amplification ceiling a stock resolver imposes on itself, the two rate limiters that ship in pinned source, and the difference between a drop in the driver and a drop once the packet already has an sk_buff.

Launch Simulator →

What you'll explore

  1. 01

    What the attacker spends, what you spend

    A useful taxonomy is not by protocol but by asymmetry: what one forged packet costs the sender versus what it costs you. A direct flood spends the attacker's bandwidth and your CPU. A reflected flood spends a third party's bandwidth and aims the answer at a forged source, so the victim never sees the attacker's address at all. The third case is the one people forget: your own host emits. A SYN whose source address is forged makes the listener build and send a SYN-ACK to that forged address through tcp_v4_send_synack() (net/ipv4/tcp_ipv4.c:1173), and a packet arriving for a port with no listener is answered by tcp_v4_send_reset() (net/ipv4/tcp_ipv4.c:739). Those replies are backscatter — traffic you generate, addressed to people who never contacted you — which is why a third party monitoring unused address space can see that you are under attack without ever seeing the attack itself.

  2. 02

    The SYN queue, and which number actually bounds it

    A half-open connection costs you a request_sock and a slot; it costs the attacker one packet with a forged source. The admission test is the part most tuning advice gets wrong. inet_csk_reqsk_queue_is_full() compares the queue length against sk->sk_max_ack_backlog (include/net/inet_connection_sock.h:283-286), which is the backlog the application passed to listen(fd, backlog) as clamped by somaxconn — not a sysctl. tcp_max_syn_backlog has exactly one reader in the whole tree (net/ipv4/tcp_input.c:7286): a heuristic that drops requests from unproven peers once the last quarter of that budget is in use, and only when syncookies are off. Its default is not even a fixed number — it is derived from the established hash table, and therefore from RAM (net/ipv4/tcp_ipv4.c:3431). So raising tcp_max_syn_backlog on a box with syncookies enabled changes nothing at all, which is a common and expensive surprise.

  3. 03

    Amplification has a ceiling, and the resolver sets it

    Amplification is a ratio: response bytes over query bytes. The numerator is not something an attacker can choose freely, because a well-configured resolver caps its own UDP answers. Stock BIND ships edns-udp-size 1232 and max-udp-size 1232 (bin/named/config.c:57 and :73), the second of which becomes view->maxudp after being clamped into [512, 4096] (bin/named/server.c:5059-5068). A client's advertised EDNS buffer is then clamped down to that value before the answer is built (lib/ns/client.c:2443-2457), and an answer that does not fit gets the TC bit set and is truncated (lib/ns/client.c:651), which forces a retry over TCP where the forged source cannot complete a handshake. The ratio is therefore bounded by configuration you can read, not by the record size an attacker picked. Worth knowing as a configuration trap: nocookie-udp-size ships as 4096 (bin/named/config.c:75) but is itself clamped down to view->maxudp (bin/named/server.c:5080-5082), so at stock settings that 4096 can never take effect.

  4. 04

    Two buckets that ship in source, and what each one stores

    Two of the four algorithms on the control exist in pinned source, and they differ in the quantity they keep. A token bucket stores remaining allowance: xt_hashlimit gives each key a credit, tops it up by elapsed jiffies up to credit_cap, and charges cost per packet (net/netfilter/xt_hashlimit.c:529 and :778-780). A leaky bucket stores accumulated excess instead: nginx computes excess = previous excess - rate * elapsed + 1000 per request (src/http/modules/ngx_http_limit_req_module.c:454), so the counter decays with time rather than being refilled, and the request is refused once it passes burst. That is the implementation difference, and it shows up as an off-by-one you can count: at the same nominal burst 5 after a full drain, hashlimit admits five and rejects the sixth, while nginx admits six and rejects the seventh — because nginx floors a negative excess at zero (src/http/modules/ngx_http_limit_req_module.c:456-458), so its first arrival spends nothing, and the burst test is a strict greater-than (:462). Burst N means N in one and N+1 in the other. Sliding window and GCRA have no pinned implementation here and nothing is asserted about them.

  5. 05

    Where in the receive path the drop happens

    XDP is often described as dropping before the packet costs anything, and that is true only of the native driver path. The kernel also ships a generic fallback for drivers without XDP support, and its signature gives the game away: do_xdp_generic() takes a struct sk_buff ** (net/core/dev.c:5262), so the sk_buff has already been allocated before the program ever runs, and netif_receive_generic_xdp() pushes the MAC header back on and may linearize or copy the packet first (net/core/dev.c:5200-5204). Generic XDP therefore costs more per dropped packet than native XDP, not less. One layer further up, tc runs from sch_handle_ingress() (net/core/dev.c:4172) where the sk_buff certainly exists, and a TC_ACT_SHOT verdict frees it with a recorded drop reason (net/core/dev.c:4214-4215). The helper and verifier surface for tc programs is net/core/filter.c (tc_cls_act_func_proto() at net/core/filter.c:8187, tc_cls_act_verifier_ops at net/core/filter.c:11074); it defines no hook of its own.

Validated against Three pinned trees, one per layer of the question, because a packets-per-second fact, a requests-per-second fact and an amplification ceiling belong to three different codebases and citing one to another's layer is a category error. Packet-rate behaviour is Linux v6.12.93: the XDP hook and its generic fallback plus the tc ingress site in net/core/dev.c, the helper and verifier surface for tc programs in net/core/filter.c, SYN-queue admission and SYN cookies under net/ipv4/, and the hashlimit token bucket under net/netfilter/. Request-rate behaviour is nginx release-1.30.5: the leaky bucket in src/http/modules/ngx_http_limit_req_module.c. The amplification ceiling is a resolver's own policy, so it comes from BIND 9.20.29: the shipped UDP response caps in bin/named/ and the truncation path in lib/ns/client.c. Anycast distribution and upstream scrubbing have no pinned implementation behind them — neither is in any pinned tree on disk — so this module states their effect as an operational assumption and attributes it to no source. Vendor mitigation architectures are likewise unpinned and nothing here is attributed to one. linux v6.12.93 @ 1d3a00d3b · nginx release-1.30.5 @ 4556c714 · BIND 9.20.29 @ c3d4465 — net/core/dev.c and friends. Every simulated behavior cites a real source function, and a server-side correlation engine computes how each layer's state cascades into the next. See the methodology →

This module is part of zerohop Pro — it unlocks alongside the full kata library, progress tracking, and weak-area analysis.