What a UDP flood looks like in tcpdump

After the Minnesota DDoS last week, I have been thinking about what these attacks actually look like at the packet level — not the descriptions in advisories, but the texture of the traffic. Last weekend I built a small lab: one host generating a UDP flood, another host receiving it, and tcpdump capturing both ends.

This is the kind of exercise every defender should do at least once. The patterns are distinctive. Recognising them in real captures takes practice; building intuition for them is the cheapest way to develop the practice.

The setup

Two Slackware boxes on a private network. Box A is the attacker, running a small Perl script that sends UDP packets to a fixed destination at a fixed rate. Box B is the target, running the honeypot I built with no service on the targeted port.

The Perl script — short enough to inline:

use IO::Socket;
my $sock = IO::Socket::INET->new(
    Proto    => 'udp',
    PeerAddr => '192.168.1.10',
    PeerPort => 8080,
) or die $!;
my $payload = 'A' x 100;
while (1) {
    $sock->send($payload);
}

This sends a 100-byte UDP packet to port 8080 on the target, in a tight loop, as fast as the kernel can push them. On my hardware, that is around 30,000 packets per second — modest compared to a real attack but enough to demonstrate the patterns.

The target is doing nothing useful. The kernel sees the packet, finds no listening socket, generates an ICMP Port Unreachable in reply, and moves on.

What tcpdump shows on the receiving side

A short capture, with tcpdump -ni eth0 udp:

14:03:01.234567 IP 192.168.1.5.41257 > 192.168.1.10.8080: UDP, length 100
14:03:01.234572 IP 192.168.1.5.41257 > 192.168.1.10.8080: UDP, length 100
14:03:01.234578 IP 192.168.1.5.41257 > 192.168.1.10.8080: UDP, length 100
14:03:01.234583 IP 192.168.1.5.41257 > 192.168.1.10.8080: UDP, length 100
14:03:01.234589 IP 192.168.1.5.41257 > 192.168.1.10.8080: UDP, length 100

The striking thing in real time is that the timestamps are extremely close together. Five-microsecond gaps. This is faster than any legitimate UDP traffic I have ever seen on my network — DNS queries, streaming, even bursts during a download — by orders of magnitude.

The second observation is that the source port is constant. The Perl script is using the same socket repeatedly, so the kernel keeps the same source port. A real attacker would probably randomise the source port to make per-flow filtering harder, but the rate per-source-port would still be characteristic.

The third observation is that the payloads are identical. 100 bytes of 'A'. A real attack might vary the payload, but again, the aggregate statistics — packet rate, packet size distribution — are the signature, more than any individual packet.

What the receiving kernel does

With tcpdump -ni eth0 'icmp' running in parallel:

14:03:01.234580 IP 192.168.1.10 > 192.168.1.5: ICMP 192.168.1.10 udp port 8080 unreachable, length 36
14:03:01.234585 IP 192.168.1.10 > 192.168.1.5: ICMP 192.168.1.10 udp port 8080 unreachable, length 36

The ICMP Port Unreachable messages stream back at the same rate. This is a thing about UDP that I had not fully appreciated until I saw it: every flooded UDP packet to a closed port produces an ICMP reply. The target is not just receiving the flood; it is also sending an equal-magnitude flood of ICMP back. From the target's perspective, the bandwidth cost is doubled.

This is a deliberate property of TCP/IP and is mostly correct — a closed UDP port should refuse politely. The cost is asymmetric and it is part of what makes UDP floods cheap for the attacker.

Linux can be configured to rate-limit outgoing ICMP responses. The relevant sysctl is net.ipv4.icmp_ratelimit. Setting it to a small value (say 100 milliseconds) drops the rate of ICMP replies significantly without breaking legitimate use cases. This is the first defence I added to the target after the experiment.

What I see on the wire to the upstream

I also captured at the boundary between the target and its upstream. From the target's outgoing direction, the flood-of-ICMP is visible. From the incoming direction, the flood-of-UDP is visible.

What is not visible at the upstream edge is anything legitimate during the test. Both directions are saturated by the test traffic, and any normal traffic is being delayed in the queues. Even at my modest 30,000 packets per second, on my modest network, the flood was sufficient to make my SSH session to the target stutter.

A real attack at, say, 300,000 packets per second from 200 sources would produce traffic that no commodity link could handle. The defence has to be not at the receiver's edge. There is no other option.

What to look for in real captures

A few patterns that I now know to recognise.

Packets per second per source. Legitimate UDP services have characteristic rates. A DNS resolver does not send hundreds of queries per second to a single destination. A streaming session has bursts but they are bounded. Sustained rates above some threshold per-source-per-destination are anomalous.

Packet-size uniformity. Legitimate UDP traffic has variable packet sizes — DNS queries are small, replies are larger, DHCP packets are different from RTP, and so on. A flood often has packets of essentially identical size.

Source distribution. A legitimate workload talks to a small number of correspondents. A distributed attack talks from many sources to one destination. The asymmetry of source diversity to destination concentration is itself diagnostic.

ICMP Port Unreachable rate. If you see high rates of outbound ICMP Port Unreachable from a host, that host is being flooded on a closed port. This is a leading indicator that the host is the target.

What a defender can do at small scale

For a small operator, the best defence at the host level is:

  1. Drop traffic to closed UDP ports at the firewall level rather than letting the kernel reply with ICMP. The ipchains rule is straightforward. This reduces the bandwidth cost of the flood by half.
  2. Rate-limit ICMP outbound on the host. sysctl -w net.ipv4.icmp_ratelimit=100 is a good starting value.
  3. Apply per-source-per-destination rate limits at the firewall level for UDP traffic. ipchains supports this via --limit. This will not stop a distributed flood but it slows down small-source attacks.
  4. Ensure the upstream provider knows how to identify flood traffic and is willing to filter it on request.

None of these stop a real distributed attack. They make smaller-scale attacks harder.

For anything beyond small scale, the defence is upstream cooperation, capacity headroom, and — increasingly — specialised mitigation services. Those services are starting to appear; they are not yet routine.

The lab exercise was worth doing. Recognising the texture of an attack on the wire is not something you can read your way to. You need to see the packets. Now I have.


Back to all writing