The normal path, and the one Wireblast uses
When a packet arrives, the NIC driver hands it to the kernel, which walks it up through netfilter, routing and the socket layer before your process ever sees it. That’s a lot of work per packet. Fine for a web server, hopeless at fourteen million packets a second. XDP is a hook that runs before any of that, right in the driver. A small program inspects each packet and decides where it goes: That fork is the whole safety story. By default Wireblast installs a program that matches nothing, so every packet keeps going up the kernel path exactly as before, and your SSH session never notices. Only when you turn on a receive mode does anything get pulled aside. Transmitting skips the stack entirely. Wireblast writes finished frames into a shared memory region and tells the driver to send them: No sockets, nosendmsg, no per-packet system call. On a driver that supports it, no copy either: the NIC reads the same memory Wireblast wrote.
UMEM and the four rings
The shared memory region is called a UMEM. It’s a slab of page-locked memory carved into fixed-size frames, one buffer per packet. Wireblast allocates one UMEM per NIC queue. Application and driver hand buffers back and forth through four lock-free rings. The directions are the part worth having a picture of: Fill and completion carry empty buffers, RX and TX carry full ones. A buffer cycles round forever and is never allocated or freed while the run is going, which is why a steady run does no memory allocation at all. It’s also why AF_XDP needs locked memory. The UMEM can’t be paged out, because the NIC writes to it directly.One socket per queue
A modern NIC spreads traffic across many hardware queues, and each can be driven independently. Wireblast opens one AF_XDP socket per queue and runs one goroutine per socket, so twelve queues means twelve cores working in parallel with no shared state and no locking. Two consequences you’ll see in the output:- Rates are aggregate, never per queue.
--pps 1Mis a million packets a second in total. Measured across 1, 4 and 12 queues on the same 10G NIC: 999.34, 999.35 and 999.44 kpps. - Flows are spread deterministically. Queue q of Q takes flow q and steps by Q, so between them they cover every flow exactly once per cycle. Changing the queue count changes which queue carries a flow, never which flows exist. See flows.
Why the link drops on the first run
Here’s the one that looks broken but isn’t. Attaching a native XDP program makes the driver tear down and rebuild its queues. On a physical NIC that means dropping carrier while the link renegotiates, typically 8 to 11 seconds on a 10G Intel card. Two details make this bearable:- It polls, it doesn’t sleep. Wireblast checks carrier every 200ms and starts the moment the link is genuinely stable. The 20-second figure in the code is a ceiling, not a wait.
- The clock starts afterwards. The bounce isn’t counted against
--duration, and the rate limiter is reset so it can’t bank credit during the outage and then release it as a burst.
And why the second run is instant
The XDP program stays attached for the life of the process. Pressingr to run again reuses it:
veth don’t bounce at all, which is part of what makes the namespace lab pleasant to experiment in.