At 02:17 on a Tuesday last year, our NOC pager didn't fire for a single VIP. Instead, we watched 4,800 IPs inside a single /20 each take a polite 40 Mbps sip of junk — not enough to trip any per-IP threshold, but together a clean 192 Gbps carpet that saturated the customer's upstream router. I've spent a decade building volumetric defenses that assumed attackers would hammer one address, and that night taught me how quickly that assumption falls apart when they decide to paint the entire subnet.
Why Carpet Bombing Breaks Traditional Per-IP Scrubbing
Most scrubbing architectures I've operated — from early Arbor TMS deployments to modern CDN edge filters — were built around a simple idea: identify the victim IP, divert it, clean it. Your Flowspec rules, your blackhole communities, your per-destination policers all key on dst_ip == X. Carpet bombing, sometimes called a spread-spectrum or wide-area flood, inverts that. The attacker spreads 100-300 Gbps across every address in your announced prefix, often a /22 or /20, so no single IP looks anomalous enough to trigger automated mitigation. The traffic isn't huge per host, which is exactly the point.
We first saw this pattern at scale when a gaming hoster asked why their whole /22 was intermittently unreachable even though their "biggest attacked IP" graph topped out at 80 Mbps. Their upstream had DDoS Volumetric Attack Patterns: Identifying and Absorbing Bandwidth Floods style thresholds set at 1 Gbps per destination, so the mitigation never engaged. Unlike a classic DDoS DNS Amplification Defense: Stopping Reflected Attacks at CDN Scale reflection flood that concentrates on one address and one port, carpet bombing uses randomized source and destination ports, often with small UDP payloads or TCP ACKs that look like legitimate return traffic. The HTTP Working Group Specifications (https://httpwg.org/specs/) won't help you here — this is well below L7, and web.dev Articles (https://web.dev/articles/) performance tuning doesn't matter when the link is already full two hops upstream.
That night we dumped sFlow from the border and the distribution was eerily uniform. Here's the kind of quick Python check we now run on sampled flow to catch it before the customer does:
#!/usr/bin/env python3
import collections
from netaddr import IPNetwork, IPAddress
# flows = [(dst_ip, bytes), ...] sampled over 10s window
SUBNET = IPNetwork("203.0.113.0/20")
flows = get_sflow_sample(window=10)
# Count per-IP bytes inside our announced prefix
per_ip = collections.Counter()
for dst, b in flows:
if IPAddress(dst) in SUBNET:
per_ip[dst] += b
# Carpet bomb signal: high subnet coverage + low per-IP peak
subnet_coverage = len(per_ip) / SUBNET.size # e.g. 0.62 = 62% of IPs hit
max_ip_bps = max(per_ip.values()) * 8 / 10 if per_ip else 0
total_bps = sum(per_ip.values()) * 8 / 10
if subnet_coverage > 0.25 and max_ip_bps < 100e6 and total_bps > 10e9:
alert(f"CARPET BOMB? cov={subnet_coverage:.1%} max={max_ip_bps/1e6:.0f}Mbps total={total_bps/1e9:.1f}Gbps")
Detecting the Spread: Entropy Instead of Thresholds
Once you stop looking at "which IP is getting hit" and start asking "how distributed is this flood," detection gets a lot more reliable. I learned this the hard way trying to tune NetFlow alerts that kept missing these events. A per-IP byte counter will always lose to a patient attacker who can stay under 50 Mbps per destination. What doesn't hide well is entropy. When 60% of your /20 lights up within a 30-second window — and your normal baseline is maybe 5-10% of that space showing any inbound traffic at all — that's a signal you can't miss, even if each IP is barely above noise.
Our current pipeline doesn't trigger on absolute bps anymore for these prefixes. It tracks two things together: destination IP entropy within the announced subnet and the ratio of total subnet bps to the top-talker's bps. In a normal flash crowd or even a concentrated volumetric attack, that ratio is low — one or two IPs dominate. In a carpet bomb, the ratio explodes. We've tied that into our telemetry stack so it runs per-PoP, not just globally, because I've seen attacks that only hit one transit provider's path and look completely benign from another PoP's perspective. If you're only doing aggregate sampling at your scrubbing center, you'll average the anomaly away.
This is a fundamentally different problem than DDoS Application Layer Attack Techniques: Slowloris, RUDY, and HTTP Floods where you can fingerprint User-Agents or connection behavior at the edge. Here you have to make a decision before the traffic ever reaches your servers. This eBPF/XDP snippet is similar to what we push to edge routers to compute that entropy signal in real time without sampling loss:
// XDP prog: compute dst IP entropy per /20 in BPF map
SEC("xdp/carpet_detect")
int xdp_carpet_bomb(struct xdp_md *ctx) {
void *data_end = (void *)(long)ctx->data_end;
void *data = (void *)(long)ctx->data;
struct ethhdr *eth = data;
if ((void*)eth + sizeof(*eth) > data_end) return XDP_PASS;
if (eth->h_proto != bpf_htons(ETH_P_IP)) return XDP_PASS;
struct iphdr *iph = data + sizeof(*eth);
if ((void*)iph + sizeof(*iph) > data_end) return XDP_PASS;
__u32 dst = bpf_ntohl(iph->daddr);
// Check if dst in 203.0.113.0/20 (example protected prefix)
if ((dst & 0xFFF00000) != 0xCB007100) return XDP_PASS; // 203.0.113.0/20
__u32 key = dst & 0xFFF; // host bits 0-4095 within /20
__u64 *cnt = bpf_map_lookup_elem(&ip_hit_map, &key);
if (cnt) __sync_fetch_and_add(cnt, 1);
// Another map tracks total packets in window, user-space computes:
// entropy = -sum(p_i * log2(p_i)) where p_i = count_i / total
// Alert if entropy > 9.5 and total > 1M pps (empirical tuning)
return XDP_PASS;
}
Mitigation Without Nuking Your Own Prefix
Blocking a carpet bomb is where most teams accidentally finish the attacker's job for them. If you apply a /20 blackhole or even force a BGP community that diverts the whole prefix through a scrubber with limited capacity, you've just created a self-inflicted outage for all the clean IPs in that range. I watched a peer do exactly that — 2,000 enterprise customers behind one /19 all went dark because the mitigation was prefix-wide and the scrubbing cluster couldn't handle the spread-out state table. You can't treat a distributed target like a single victim IP anymore.
What actually works is prefix-aware, stateless filtering as far upstream as you can push it, combined with selective dirty-traffic shedding. We're talking BGP Flowspec v2 rules with destination prefix + port range matching, or ACLs pushed to the upstream's peering router that drop small-packet UDP floods matching the attack signature but allow normal traffic to the same destinations. Because the per-IP rate is low, you can't rely on policers either — you need signature-based drops (e.g., UDP src port 19/53/389 with specific payload lengths) or SYN/ACK rate limiting that keys on the subnet, not the host. And you must have a way to measure collateral damage in real time: we run active probes to a canary IP inside each protected prefix that should never see attack traffic, and if its loss exceeds 1%, we back off the filters.
Here's how our current options compare when you're deciding what to push upstream during an active carpet bomb. Numbers are from our last 12 months of incidents across three Tier-1 upstreams:
| Mitigation Strategy | Time to Deploy | Collateral Drop (Clean Traffic) | Max Absorbed Flood |
|---|---|---|---|
| BGP Flowspec (port/payload filter per /20) | 45-90 sec | 0.2 - 0.8% | ~250 Gbps |
| Prefix-wide RTBH / Scrubbing Diversion | 20-40 sec | 100% (full prefix blackhole window) | ~400 Gbps (cluster limit) |
| Upstream ACL (stateless UDP drop at peer) | 3-7 min (manual NOC ticket) | 0.05 - 0.3% | ~600 Gbps |
| Per-IP Policers (legacy approach) | 60-120 sec | 4 - 12% (over-policed due to low per-IP rate) | <30 Gbps effective |
Building the Real-Time Pipeline: From IPFIX to Flowspec Push
Knowing the theory doesn't help when your /24 is getting lit up at 3 AM. In production, you can't afford a 2-minute detection loop — carpet bombing will have already saturated your edge routers' small packet buffers. We've settled on a pipeline that samples IPFIX at 1:1000, streams it through Kafka, and runs a sliding-window entropy check every 5 seconds. Anything longer and you'll miss the first wave; anything shorter and you'll drown in noise from normal CDN cache revalidation. For a typical 10 Tbps edge PoP handling ~120M pps, that sampling rate still gives you ~120k flows per window — more than enough to see the distribution flatten.
When entropy crosses your threshold (we use normalized entropy >0.82 combined with a minimum of 40% of prefix IPs receiving traffic), don't reach for remote-triggered blackhole. That's exactly what the attacker wants — you'll nuke your own prefix. Instead, push a BGP Flowspec rule that matches the attack fingerprint and drops or rate-limits only that traffic. We've had good luck matching on packet length variance and UDP src port sprawl. For example, if 90% of the carpet traffic is 64-byte UDP with random ephemeral ports to your entire 256-IP range, a single Flowspec rule handles it without touching legitimate traffic to port 443. The key is propagation time — with ExaBGP or GoBGP peering to your transit and scrubbing infrastructure, you can push that rule globally in under 8 seconds.
At the box level, we lean on XDP for the actual drop to save the conntrack table. An XDP program pinned to the NIC driver can drop 14 Mpps per core before you even hit the kernel stack, which is critical when each IP is only seeing 30-50k pps — not enough to trigger per-IP policers, but collectively 7-12 Mpps across the subnet. Don't try to do this in iptables; you'll melt.
# Simplified carpet-bomb detector - runs every 5s on IPFIX aggregate
import math
from collections import Counter
def normalized_entropy(dst_ip_hits, prefix_size=256):
total = sum(dst_ip_hits.values())
entropy = -sum((c/total) * math.log2(c/total) for c in dst_ip_hits.values())
return entropy / math.log2(prefix_size) # 0.0 = single IP, 1.0 = perfect spread
# Example trigger logic
window_hits = Counter(ipfix_dst_ips_last_5s) # e.g., {'192.0.2.1': 412, ...}
coverage = len(window_hits) / 256
entropy = normalized_entropy(window_hits)
if entropy > 0.82 and coverage > 0.40 and total_pps > 5000000:
# Push Flowspec via ExaBGP - drop small UDP carpet traffic
announce = """
announce flow route carpet-drop-01 {
match {
destination 192.0.2.0/24;
protocol udp;
packet-length 60-80;
fragment dont-fragment;
}
then {
discard;
community [ 65000:666 ];
}
}
"""
push_to_exabgp(announce) # propagates in <8s to all edge routers
Advanced Evasion Patterns and Troubleshooting False Positives
Once you've handled the basic spread, attackers get clever. The nastiest variant we've seen in the last 12 months is carpet bombing mixed with pulse waves — they'll spray your /24 for 45 seconds at 800 Gbps, go silent for 90 seconds, then hit again with a different vector. Your entropy detector will flap, and if you're auto-withdrawing Flowspec on low traffic, you'll be chasing your tail. That's where you need to correlate with our DDoS Pulse Wave Detection: Identifying Intermittent Attack Bursts logic. We now hold Flowspec announcements for at least 300 seconds after entropy drops and track the inter-burst interval. If you see three high-entropy bursts with >0.75 periodicity in 10 minutes, lock the rule in and switch to scrubbing-center diversion instead of edge discard.
The other headache is hybrid reflection. It's not uncommon to see a carpet bomb where every packet is also a reflected amplification payload — especially NTP and memcached. You'll see 1,200-byte UDP packets hitting every IP in your prefix from thousands of reflectors. Per-IP thresholds look even more innocent here because the packet count is lower, but the byte volume is brutal. If you're not already filtering those vectors at the edge, read up on DDoS NTP Reflection Mitigation: Blocking Monlist-Based Amplification Attacks and DDoS Memcached Amplification Defense: Neutralizing 51,000x Amplification Factor — we've found that dropping monlist responses and memcached UDP/11211 at ingress peering kills 60-70% of the carpet's byte volume before your entropy logic even fires.
And you'll get false positives. A legitimate cache-fill or health-check sweep can briefly look like a carpet bomb, especially if your origin is warming a new PoP and touching every IP. We've learned to whitelist our own monitoring ASNs and to add a second check: carpet bombing almost always has a port entropy >0.9 (randomized source ports) and a TTL variance under 8. Legitimate sweeps don't. If your mitigation is rate-limiting instead of discarding, start with a 50k pps per-destination limit with a 5% exceed queue — that way a misfire won't blackhole real users, but an actual 10 Mpps spread still gets squashed. Monitor your Flowspec RIB — Cisco and Juniper both cap at ~3k-5k active Flowspec rules, so expire aggressively.
Frequently Asked Questions
Why doesn't my per-IP 100k pps policer stop carpet bombing?
Because the math works against you. If an attacker has 40 Gbps to spread across your entire /24 (256 IPs), that's only ~15k pps per IP with 1,400-byte packets or ~60k pps with 64-byte packets. Your per-IP policer set at 100k pps never fires, but the aggregate 10-15 Mpps and 40 Gbps still overwhelms your router's PPS budget and uplink. Carpet bombing is designed specifically to stay under those per-IP radars.
How do I calculate entropy fast enough for real-time detection?
Don't overthink it. Use a 5-second sliding window, count hits per destination IP in your prefix, and compute normalized Shannon entropy: entropy / log2(prefix_size). A value near 1.0 means perfectly even spread. In practice, >0.82 with >40% prefix coverage and >5 Mpps aggregate is a reliable trigger. We've run this in ClickHouse and even in eBPF maps at line rate — at 1:1000 IPFIX sampling, the calculation costs less than 2ms per window on a single core.
Should I just blackhole the whole /24 when under a carpet bomb?
Please don't — that's an own-goal. Remote-triggered blackholing (RTBH) for the prefix takes your entire subnet offline, which is often worse than the attack itself. Use BGP Flowspec to discard only the attack fingerprint (e.g., small UDP to the affected prefix) or divert to a scrubbing center with per-flow cleaning. If you must blackhole, do it with a /32 Flowspec discard scoped to the attack's destination port and packet length, not a /24 community.
Can sampling miss a low-and-slow carpet bomb?
Yes, if your sampling is too aggressive. At 1:4096 sampling, a 2 Mpps low-rate carpet (about 8k pps per IP) only yields ~488 samples per second — your entropy calculation gets noisy and you'll see false negatives. We don't go sparser than 1:1000 on edge routers and we complement sampling with sFlow-triggered XDP counters that give us unsampled PPS per IP for the top 10 prefixes every second. That hybrid approach catches the slow spreads without needing full NetFlow export.