> ## Documentation Index
> Fetch the complete documentation index at: https://wireblast.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

# How it works

> Where AF_XDP sits, why it goes fast, and why the link drops for a few seconds the first time.

You don't need any of this to use Wireblast. But three of its behaviours look like bugs until you know what's underneath, so here's the short version.

## 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:

```mermaid theme={null}
flowchart LR
    W([Wire]) --> D[NIC driver]
    D --> X{XDP program}
    X -->|matches your filter| S[AF_XDP socket<br/>Wireblast]
    X -->|everything else| K[Kernel network stack<br/>SSH, DNS, your apps]
```

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](/concepts/receive) 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:

```mermaid theme={null}
flowchart LR
    G[Wireblast builds a frame] --> T[TX ring]
    T --> D[NIC driver]
    D --> W([Wire])
    K[Kernel network stack] -.->|never involved| D
```

No sockets, no `sendmsg`, 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:

```mermaid theme={null}
flowchart LR
    APP["Wireblast<br/>(owns the UMEM)"]
    DRV["NIC driver"]
    APP -->|"Fill ring: here are empty buffers"| DRV
    DRV -->|"RX ring: here is what arrived"| APP
    APP -->|"TX ring: please send these"| DRV
    DRV -->|"Completion ring: these went out"| APP
```

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](/install#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 1M` is 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](/patterns/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.

```mermaid theme={null}
sequenceDiagram
    participant W as Wireblast
    participant D as NIC driver
    participant L as Link
    W->>D: load and attach the XDP program
    D->>D: reinitialise the queues
    D-)L: carrier down
    loop every 200ms
        W->>L: is it back yet?
    end
    L-)D: carrier up
    D-->>W: link up after 11.8s
    W->>W: start the clock, reset the rate limiter
    W->>D: transmit
```

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. Pressing `r` to run again reuses it:

```text theme={null}
first run   link back in 8.4s
press r     XDP already attached, transmitting 2 seconds later
```

Wireblast only reattaches when something the attachment genuinely depends on changes: the interface, the queue count, the receive filter, or a packet size that needs a bigger UMEM frame. Rate, duration, flow count, ports and addressing all reuse it, which covers essentially everything you change between runs.

It detaches when you quit. Virtual interfaces like `veth` don't bounce at all, which is part of what makes the [namespace lab](/guides/namespace-lab) pleasant to experiment in.

## The library underneath

Everything on this page, meaning UMEM management, the rings, XDP program loading and the zero-copy paths, is [go-afxdp](https://github.com/atoonk/go-afxdp), a standalone Go library. Wireblast is the packet generator built on top of it. If you want to write your own line-rate tool in Go, that's the place to start.
