packetio

package module
v0.1.10 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Sep 8, 2026 License: Apache-2.0 Imports: 6 Imported by: 0

README

packetio

build and test

A high-performance packet I/O library for Go. One API over four ways of reaching a NIC: mlx5 Direct Verbs for NVIDIA/Mellanox ConnectX and BlueField, DPDK, AF_XDP, and AF_PACKET. You write the packet loop once and pick the backend to suit the machine it lands on: a ConnectX card, an ordinary server, a laptop. Changing backend is changing the import line.

d, _ := mlx5.Open("eth0")          // or afxdp.Open, dpdk.Open, afpacket.Open
defer d.Close()

tx := d.TxQueue(0)
tx.SendFunc(64, func(i int, frame []byte) int {
        return copy(frame, myPacket)   // write straight into the NIC's memory
})

I built this because I have wanted it for a long time. Moving packets fast from Go has always meant learning a pile of machinery first: DPDK and its mempools, AF_XDP rings and UMEM, Direct Verbs queue setup, hugepages, memory ownership. That machinery is capable, but the barrier to simply writing a fast Go program that sends, receives or forwards packets is much higher than it should be. This library went through many iterations and experiments before I was happy with the abstraction; this is the version I wanted to open source.

The goal is to hide as much of that complexity as reasonably possible without hiding the semantics that matter. Who owns a frame at every moment, how many queues are open, what is steered to them, and what a given backend can and cannot do all stay explicit. The backends are similar, not pretended to be identical: Capabilities() says what you got, and a backend refuses what it cannot honour rather than approximating it.

What is not traded away is performance. No per-packet allocation, no system call, library call or cgo call per packet on the fast path, and no copies anywhere the mechanism allows it: of the four backends only AF_PACKET copies, because a packet socket is a copy. On a ConnectX-6 Dx, three Go workers on DPDK put 64-byte frames on a 100G wire at line rate (148.8 Mpps); Direct Verbs needs four, and forwards line rate on twelve. It is a Go program you go build like any other. The Performance section has the full tables, and I wrote up the whole comparison here: Four ways to do super fast packet processing in Go.


Install

go get github.com/atoonk/packetio

Nothing else, if you use the AF_XDP or AF_PACKET backends: they are pure Go.

The other two link C libraries, so they need a package and a build tag. On Ubuntu 24.04:

# mlx5 Direct Verbs
sudo apt install libibverbs-dev            # to build
sudo apt install libibverbs1 ibverbs-providers   # to run a binary built elsewhere
go build -tags mlx5 ./...

# DPDK
sudo apt install libdpdk-dev               # to build
sudo apt install dpdk                      # to run a binary built elsewhere
go build -tags dpdk ./...

Both want to pin memory, so run as root or raise the memory lock limit (ulimit -l). DPDK additionally wants hugepages on a card it takes from the kernel, and none at all on a ConnectX, which keeps its kernel interface. Each backend's README has the details.

Quick start

Both programs below run as they are. They use AF_PACKET, which needs no special hardware, so you can paste them on a laptop; change the import and the Open call and nothing else moves. They are examples/hello in two halves, and sudo go run ./examples/hello -i eth0 runs the round trip as one program.

Send
package main

import (
	"log"

	"github.com/atoonk/packetio/afpacket"
)

func main() {
	d, err := afpacket.Open("eth0")
	if err != nil {
		log.Fatal(err)
	}
	defer d.Close()

	pkt := myPacketBytes() // whatever you want on the wire

	tx := d.TxQueue(0)
	for {
		// One call is the whole cycle: reclaim finished frames, take fresh
		// ones, fill them, hand them to the NIC. 256 is the batch size.
		if _, err := tx.SendFunc(256, func(i int, frame []byte) int {
			return copy(frame, pkt)
		}); err != nil {
			log.Fatal(err)
		}
	}
}
Receive
package main

import (
	"fmt"
	"log"
	"time"

	"github.com/atoonk/packetio/afpacket"
)

func main() {
	d, err := afpacket.Open("eth0", afpacket.WithQueues(1))
	if err != nil {
		log.Fatal(err)
	}
	defer d.Close()

	rx := d.RxQueue(0)
	rx.Fill(rx.NumFreeFillSlots()) // give the NIC somewhere to put packets

	for {
		if _, err := rx.Poll(time.Second); err != nil {
			log.Fatal(err)
		}
		descs := rx.Receive(256)
		for _, d := range descs {
			pkt := rx.Region().Frame(d) // the bytes, where the NIC wrote them
			fmt.Println(len(pkt), "bytes")
		}
		rx.Recycle(descs)              // give the frames back
		rx.Fill(rx.NumFreeFillSlots()) // and re-arm
	}
}

That is the whole model: a frame belongs to exactly one of three owners at a time (the pool, your program, or the NIC), and every method hands it between them. Alloc → fill → TransmitComplete going out; FillPollReceiveRecycle coming in.

One gotcha worth knowing up front. A bare Open gives you one transmit queue and no receive queues on mlx5 and dpdk, because a generator should not pay for receive memory it never uses. Ask for receive with WithQueues(n) or WithRxQueues(n). AF_PACKET opens one of each.

Which backend?

use it when needs one core sends
mlx5 you have a ConnectX/BlueField card rdma-core, -tags mlx5 69.2 Mpps
afxdp any modern NIC, and you want to keep using it a driver with XDP support 18.7 Mpps
dpdk Intel/Broadcom/virtio, or you need DPDK's drivers libdpdk, -tags dpdk, usually hugepages 56.7 Mpps
afpacket it just has to run: a laptop, a VM, a container nothing at all 1.7 Mpps

If you have an NVIDIA/Mellanox ConnectX card, use mlx5: it is the fastest here and it costs you nothing operationally, because the kernel keeps the interface. Otherwise start with afxdp, which is fast and leaves the NIC in place. Reach for dpdk when AF_XDP is not enough or your card needs a DPDK driver, and know that on non-ConnectX hardware it takes the NIC away from Linux entirely. afpacket is the floor that always works.

Each backend's README covers what to install, what to run, and what bites.

mlx5.Open("eth0")                              // -tags mlx5
afxdp.Open("eth0", afxdp.WithSteering(filter)) // AF_XDP always wants a filter
dpdk.Open("0000:c1:00.1")                      // -tags dpdk; PCI address or name
afpacket.Open("eth0")

Steering: take the traffic you want, leave the rest

Steering is what makes this usable on a machine you are logged into. You name the packets you want; the NIC or the kernel diverts exactly those to your program, and everything else carries on to the kernel: your SSH session, ARP, monitoring, all of it.

d, err := mlx5.Open("eth0", mlx5.WithSteering(packetio.SteeringFilter{
        Match: append(
                []packetio.Match{packetio.MatchVLAN(2053)},
                packetio.MatchDstPort(packetio.IPProtoUDP, 9000)...),
}))

One vocabulary (destination MAC, VLAN, EtherType, IP protocol, source and destination prefixes, TCP and UDP ports), compiled to whatever the backend has: hardware flow rules on mlx5 and dpdk, an eBPF program on afxdp. afpacket has no steering because it cannot: it is a tap, the kernel sees every packet anyway, and rather than offer something weaker under the same name it offers nothing.

Repeated matches of one kind are alternatives, different kinds are ANDed: two MatchDstPort and one MatchVLAN means "either port, on that VLAN".

A backend that cannot express a match refuses it at Open with ErrUnsupported rather than installing a wider one. A filter that quietly delivers more than you asked for is worse than no filter at all.

Whether unmatched traffic still reaches Linux is a property of the device, not the filter: Capabilities().KernelCoexistence answers it. It is true everywhere except DPDK on a device bound to vfio-pci, where there is no kernel interface left to carry the rest.

Performance

All four backends, driven through the same three loops by the same program (examples/sweep) on the same pair of machines: AMD EPYC 9275F, ConnectX-6 Dx at 100G, 64-byte frames on a tagged link. Measured 7 September 2026, medians of three passes, default configuration throughout: no flags, no tuning.

Two things about the method, because they change what the numbers mean. Rates come from the port's own counters, not the application's. And cores are measured across the whole machine, so the soft-interrupt work AF_XDP and AF_PACKET do outside your process is counted where it falls. If you only count your own process, the kernel's half of AF_XDP disappears from the benchmark even though you are still paying for it.

One core, one queue, the number that says how efficient a backend is:

transmit receive forward
mlx5 69.2 Mpps 44.1 Mpps 29.3 Mpps
dpdk 56.7 Mpps 46.1 Mpps 22.6 Mpps
afxdp 18.7 Mpps 31.8 Mpps (1.6 cores) 17.5 Mpps (2.0 cores)
afpacket 1.7 Mpps 1.4 Mpps (50 cores) 0.0 Mpps (50 cores)

Scaling to 100G line rate (148.8 Mpps at 64 bytes). One worker per queue, one queue per core. AF_XDP and AF_PACKET cells show whole-machine cores with the soft-interrupt share in parentheses; the bypass backends run 0.0 softirq.

Transmit, a single flow:

cores mlx5 dpdk afxdp afpacket
1 69.2 56.7 18.7 (0.5) 1.7 (0.2)
2 100.7 107.9 35.7 (1.0) 2.5 (0.5)
3 147.8 148.8 - -
4 148.8 148.8 70.7 (2.0) 4.6 (1.0)
8 - - 120.0 (5.3) 9.0 (1.9)
12 - - 147.1 (8.2) -
16 - - 147.6 (11.2) 17.4 (4.0)
20 - - 147.9 (14.0) -

Receive, offered 148.6:

cores mlx5 dpdk afxdp afpacket
1 44.1 46.1 31.8 on 1.6 (1.0) 1.4 on 50 (50)
2 81.5 85.0 67.3 on 3.2 (2.0) 1.6 on 51 (51)
4 117.8 124.9 121.9 on 6.5 (4.0) 1.7 on 51 (51)
6 124.9 113.0 - -
8 145.6 148.6 146.5 on 11.4 (7.5) 2.0 on 53 (53)
10 148.6 116.4 - -
12 148.6 - 145.2 on 12.1 (8.1) -
16 - - 146.6 on 11.6 (7.2) 2.3 on 53 (53)

Forwarding, offered 148.6, next hop nobody owns:

cores mlx5 dpdk afxdp afpacket
1 29.3 22.6 17.5 on 2.0 (1.0) 0.0 on 50 (50)
2 58.0 43.6 32.5 on 4.0 (2.0) 0.0 on 50 (50)
4 87.5 74.3 51.2 on 8.0 (4.0) 0.0 on 50 (50)
6 100.2 60.9 - -
8 142.1 126.8 75.1 on 16.0 (8.0) 0.0 on 50 (50)
10 146.5 - - -
12 147.4 114.7 108.0 on 24.0 (12.0) -
16 141.3 143.5 138.4 on 28.7 (12.6) 0.0 on 50 (50)

DPDK transmits line rate on three cores, mlx5 on four (and 99.3% of it on three). DPDK receives it on eight, mlx5 on ten. mlx5 forwards it on twelve, which is the one place a backend reaches the wire doing real work on every packet. AF_XDP gets close everywhere but pays about twice the cores, and half of what it spends is soft interrupt: that is the price of leaving the NIC with Linux, and depending on what you are building it is a price worth paying.

The write-up of this comparison, including how it lines up against VPP's three datapaths on the same card, is Four ways to do super fast packet processing in Go.

AF_PACKET does not degrade under load, it collapses. Its receive row is 2.3 Mpps for 53 cores, and forwarding under the same flood is essentially zero. That is receive livelock: the kernel takes all 148 million frames a second whether or not you read them, and your program is starved out. Offer it less and it behaves: 12 Mpps received at 12 offered, 7.1 forwarded. Push harder and it goes backwards. Every other backend here holds its number under a full flood.

A few things worth knowing behind those numbers:

  • Everything here is the same trade wearing different clothes: pointer descriptors are cheap per packet but drain through one ~75 Mpps device-wide door; copied descriptors cost more per packet and scale per queue to the wire. The mlx5 backend switches modes by queue count (transmit-only at two, forwarders at four), DPDK's PMD switches at eight send queues (its forwarding dip at six and jump at eight is exactly that), and since go-afxdp v0.11.0 even the kernel's XDP path is switched the same way: small fleets run pointer descriptors, which is where the AF_XDP 1-4 queue transmit numbers come from.
  • Forwarding is memory-latency work on hosts whose inbound DMA bypasses the cache (this EPYC among them): the first read of every arriving frame used to stall a full memory round-trip, capping mlx5 forwarding near 70 Mpps however many cores it got. The receive ring now prefetches each frame as its completion is consumed. One instruction, half the gap; the descriptor-mode switch above is the other half.
  • AF_XDP's kernel half is real work on real cores. At 146.5 Mpps receive, 7.5 of its 11.4 cores are soft interrupt: the driver's NAPI poll and the XDP redirect. Counting only your process would call that 4 cores; the table counts the machine.
  • The receive flat spot at three workers (both bypass backends: ~84-89 Mpps at 3, barely above 2) reproduces across passes and days and is still unexplained; it disappears by four workers.
  • These receive figures are 64-byte frames, and that matters more than it should. At 68 bytes, the smallest a VLAN-tagged frame may be, receive tops out near 130 Mpps on this card and never reaches the 142 Mpps line rate, in every stack tested including DPDK's. Four bytes on the wire, and a ceiling appears. Transmit is unaffected, and 128/512/1500-byte receive takes everything offered. The cause was chased through every layer software controls and pinned to the card's receive pipeline: the cliff sits at a 61-byte payload, in every stack.
  • The forwarding ceilings above are real, not tuning artifacts. DPDK was re-run with the PMD's inline knobs forced every way they go (txqs_min_inline=1, txq_inline_mpw=64, both): ~127 at eight queues every time, worse elsewhere. AF_XDP was re-run with NAPI flush, ring depth and pool variants: all inside the noise of its default. The numbers each stack ships with are the numbers it has.

Examples

Start with hello: about a hundred lines, both directions, runs on any interface on any Linux box.

sudo go run ./examples/hello -i eth0
example what it shows backend
hello the whole API in one file: start here any
send one frame, and what the hardware said about it mlx5
blast a generator with rate control and CPU placement mlx5
drop the receive cycle, and what it costs mlx5
steer ask for two UDP ports, watch only those arrive mlx5
l3fwd an IPv4 router forwarding in the frame it arrived in mlx5
info what a card says about itself mlx5
dpdk/info the driver, who owns the device, which offloads are real dpdk
dpdk/blast the generator, over DPDK dpdk
dpdk/drop the receive cycle with the NIC's own counters dpdk
dpdk/l3fwd the same router, sharing its forwarding code dpdk
timestamps when packets really arrived, and the gaps between them any
pingpong round trips between two machines, split by where the time went mlx5
sweep every backend through the same three loops; the Performance tables above mlx5, dpdk, afxdp
netstack/examples/tcpecho a TCP echo server, and client, on a userspace TCP/IP stack over the device any
sudo go run -tags mlx5 ./examples/steer -i eth0 -udp-port 9000 -udp-port 9001

There is no separate AF_XDP example because none is needed: the loop in any of these runs on it unchanged once you open with a steering filter, which is two lines shown in its README. AF_XDP-specific tooling lives in go-afxdp.

TCP on top: netstack

Fast receive and transmit are only useful if something can speak the protocols on them. netstack/ runs gVisor's TCP/IP stack over any packetio device and hands back net.Listener and net.Conn, so a TCP server -- or a client, or a load balancer that terminates connections -- runs in userspace with the kernel nowhere on the path. It is a separate Go module, so programs that only move frames do not carry gVisor, and the gVisor it carries is a patched one (netstack/gvisor, go-afxdp's ten patches on a pinned upstream commit, the stack that produced its numbers). Its README says which address to give the stack on each backend, which is the one thing that differs between them.

Timestamps: how long you held a packet

The device records when each packet arrived, before your program is involved. Ask for those times and you can measure two things you otherwise cannot: how long packets spend inside your program, and how evenly traffic is arriving.

if rx, ok := d.RxQueue(0).(packetio.TimestampReceiver); ok && d.Capabilities().RxTimestamps {
        descs, ts := rx.ReceiveTimestamps(256)   // ts[i] is when descs[i] arrived
}

That is the whole difference from an ordinary receive loop: one type assertion, and Receive becomes ReceiveTimestamps.

Why not just call time.Now() in the loop? Because that measures the loop. If your program is busy for a millisecond, the fifty packets that arrived during it all get read at the same instant and look simultaneous. The card stamped each one as it came off the wire, before the transfer to memory, before the completion, before any of your code ran, so those stamps stay true whatever your program was doing.

Nothing is written into the packet. The time goes in the completion the card writes beside it, so the sender neither cooperates nor notices, and traffic from anyone can be measured.

What you can and cannot learn from it

You get one fixed reference point per packet: the moment it reached the port. Both ends of any interval you build from it are on your machine.

forwarding residency   arrival stamp  ->  when you hand it to transmit
your receive path      arrival stamp  ->  when your code first sees it
arrival jitter         one packet's stamp -> the next one's

What you cannot get is how long a packet was on the wire, or a one-way delay from some sender to you. Both need a timestamp taken when the packet was sent, and no such thing travels in the packet. Those need two clocks disciplined to a common source, which this package does not do.

What it costs

Nothing, unless you ask. Receive never reads the field, and no offload is switched on at the device: the card writes the timestamp into every completion whether or not anybody reads it. Measured against the build before the feature, receive was 44.19 -> 44.17 Mpps on one queue and 144.14 -> 143.97 on eight, both inside run-to-run noise.

Two things to know

The slices belong to the queue. descs and ts are overwritten by the next receive call. Copy anything you mean to keep.

On a link with offloads on, one timestamp can cover many packets. The kernel coalesces received segments into one super-frame (GRO) and stamps that, at the moment it coalesced rather than when each segment arrived. A stream that would have been forty-odd samples becomes one, with a skew nobody sees in the numbers. Turn offloads off on the link you are measuring, or measure something that is not being coalesced.

Arrival order is not delivery order. A card stamps at the port and places the packet in a queue afterwards, so while it is dropping traffic the two come apart: at rates it keeps up with, stamps rise packet by packet (2 out of 9.1 million out of order), but offer 148 Mpps to a queue that can take 44 and about a third arrive out of stamp order. Sort if you need order.

backend stamps with resolution epoch
mlx5 the card, at the port 1 ns the device's own, meaningless on its own
afpacket the kernel, filling the ring nanoseconds CLOCK_REALTIME, so it can step
afxdp, dpdk not yet
What it looks like in practice

examples/pingpong times round trips between two machines and uses the stamps to say where the time went. Measured back to back on ConnectX-6 Dx, one queue, one frame in flight, no tuning of any kind:

frame round trip (median) of which, this program's receive path
64 B 5.58 us 0.49 us
1500 B 6.32 us 0.42 us

A round trip is a software number: it covers both machines' send and receive paths, and on a back-to-back cable the wire is tens of nanoseconds. It is not a network measurement, and the far end here is packetio too, so a full reflector cycle is inside every figure. What the stamp adds is the second column: without it there is only "5.58", and no way to say whose microseconds those were.

Capabilities().RxTimestamps is the authority on whether a device really stamps. A queue may carry the method without the device having a clock, and then ReceiveTimestamps returns nothing rather than inventing zeroes: a zero would be a claim that a packet arrived at the epoch, and nothing downstream could tell that from a real reading. examples/timestamps is a working jitter meter in about a hundred lines, and it runs on any Linux box.

Offload

Offload carries segmentation and checksum metadata alongside a frame, so a 64 KB TCP super-frame crosses the device whole and is cut up by the kernel or a virtio peer instead of by you. It is virtio_net_hdr field for field, the common currency of PACKET_VNET_HDR, vhost-user, tap and memif, and it rides optional interfaces:

if r, ok := rq.(packetio.OffloadReceiver); ok {
        descs, offs := r.ReceiveOffload(64)
}

A 64 KB packet does not need a 64 KB frame. Where Capabilities().MultiBuffer says so, one packet may lie across several descriptors, each but the last marked OptContinued - the AF_XDP convention. It goes both ways: hand Transmit a chain and the device gathers it, and on receive you get the same shape back. That lets a forwarder keep small frames and still carry segmentation-offloaded traffic, instead of sizing every frame for the largest packet it will ever see.

for _, d := range rq.Receive(64) {
        if d.Options&packetio.OptContinued != 0 {
                // more of this packet follows
        }
}

If your packets already live in your own memory, GatherTransmitter skips the region entirely and sends from your slices.

What is here

packetio               Desc, Region, TxQueue, RxQueue, Device: the API
match.go               SteeringFilter and Match: what to steer
offload.go             virtio_net_hdr: segmentation and checksum metadata
internal/pool          the free-frame list, one per queue per direction
internal/conform       the contract in BACKENDS.md, as a test every backend runs

mlx5                   Direct Verbs; see mlx5/README.md
afxdp                  AF_XDP over go-afxdp; see afxdp/README.md
dpdk                   a poll-mode driver in-process; see dpdk/README.md
afpacket               TPACKET_V3 and sendmmsg; see afpacket/README.md

BACKENDS.md is the contract a backend must honour; internal/conform is that contract as a runnable suite.

Testing without a NIC

go test ./...
go test -race ./...

mlx5/internal/mocknic reads the same doorbell records, parses the same work queue entries and writes the same completions as real hardware, over the same memory: the ring under test cannot tell the difference. The suite was checked by breaking the driver on purpose (publishing a doorbell index in the wrong byte order, releasing a frame one slot too far, believing a completion that names the wrong buffer) and confirming each is caught.

The conformance suite runs against afpacket on a veth pair, so it needs no hardware at all.

Requirements

Linux, amd64 or arm64 (the dpdk backend is amd64 only). CAP_NET_RAW everywhere. mlx5 additionally needs rdma-core and a memory lock limit large enough for the frame region; dpdk needs libdpdk and, on most cards, hugepages and an IOMMU.

License

Apache 2.0. See LICENSE.

Documentation

Overview

Package packetio is a small, backend-neutral Ethernet packet I/O API.

The library moves frames between an application and a NIC with no per-packet allocation, and with no copies on every backend whose mechanism allows it -- of the four, only AF_PACKET copies, because a packet socket is a copy. It is deliberately not a networking framework: it owns queues and buffers, and nothing above that.

Backends

A backend implements Device, TxQueue and RxQueue over some kernel or hardware mechanism:

mlx5     NVIDIA/Mellanox ConnectX and BlueField via mlx5 Direct Verbs.
         Queue memory and doorbells are mapped into the process, so the
         packet path is ordinary loads and stores plus one MMIO write per
         batch: no syscall, no library call, no cgo call per packet.
afxdp    Linux AF_XDP, an adapter over github.com/atoonk/go-afxdp. Works
         on any driver with XDP support, and keeps the interface usable.
dpdk     Any NIC DPDK has a poll-mode driver for -- Intel, Broadcom,
         virtio, and the ConnectX too. A custom mempool keeps packetio's
         frame-ownership model intact under the driver; the packet path
         is one cgo crossing per burst, never one per packet. x86-64
         only, behind the dpdk build tag.
afpacket Linux AF_PACKET: a TPACKET_V3 mmap ring on receive and batched
         sendmmsg on transmit. Needs no hardware support and no cgo, and
         is far slower than the others. It is the fallback that works
         anywhere, and the floor the others are measured against.

The model

Every backend exposes the same shape, borrowed from AF_XDP because it maps cleanly onto hardware descriptor rings as well:

  • A Region is one contiguous chunk of frame memory the NIC can DMA to and from. It is allocated and registered once, at open.
  • A Desc names one frame inside that Region: an offset and a length. It is a value, not a pointer, and it is the unit of ownership.
  • Transmit is Alloc, fill, Transmit, Complete. Receive is Fill, Poll, Receive, Recycle. In both directions a frame is owned by exactly one of the pool, the application, or the NIC, and the verbs are the transitions.

What is deliberately absent

The common API describes packet and buffer movement, not the mechanics of a particular kernel interface. AF_XDP's need-wakeup kicks and mlx5's completion queue arming are not methods on TxQueue or RxQueue; a backend performs whatever its hardware or kernel requires inside Transmit, Poll and friends. Where an application genuinely needs backend-specific control, it type asserts for an optional interface such as Offload metadata.

Concurrency

A queue is owned by exactly one goroutine. Two goroutines may drive a TxQueue and an RxQueue of the same Device concurrently; two goroutines may not drive the same queue. This is what keeps the frame pools free of locks.

Index

Constants

View Source
const (
	// OptContinued marks a frame that is one fragment of a larger packet, with
	// at least one more fragment following. The last fragment does not have it.
	OptContinued uint32 = 1 << 0

	// OptChecksumOK reports that the NIC verified the L3 and L4 checksums of a
	// received frame and found them correct.
	OptChecksumOK uint32 = 1 << 1

	// OptL3ChecksumOK reports that the NIC verified the received packet's IP
	// header checksum and found it correct. A forwarder that would otherwise
	// verify the header itself can trust this and skip the work; it says
	// nothing about the payload.
	OptL3ChecksumOK uint32 = 1 << 2

	// OptionsBackendShift is the first Options bit available to backends.
	OptionsBackendShift = 16
)

Option bits defined by this package. A backend that cannot express one simply never sets it.

View Source
const (
	IPProtoTCP uint8 = 6
	IPProtoUDP uint8 = 17
)

IP protocol numbers, for MatchIPProto and the port matches.

View Source
const (
	OffloadNeedsCsum uint8 = 1 // VIRTIO_NET_HDR_F_NEEDS_CSUM

	OffloadGSONone  uint8 = 0    // VIRTIO_NET_HDR_GSO_NONE
	OffloadGSOTCPv4 uint8 = 1    // VIRTIO_NET_HDR_GSO_TCPV4
	OffloadGSOUDP   uint8 = 3    // VIRTIO_NET_HDR_GSO_UDP
	OffloadGSOTCPv6 uint8 = 4    // VIRTIO_NET_HDR_GSO_TCPV6
	OffloadGSOECN   uint8 = 0x80 // VIRTIO_NET_HDR_GSO_ECN, or'd into GSOType
)

Virtio-net header flags and segmentation types (linux/virtio_net.h). These are the values that go on the wire, so they are fixed, not ours to choose.

View Source
const MaxRules = 16

MaxRules bounds how many rules one SteeringFilter may expand to. A filter needing more is better expressed as a wider match than as a long list, and every backend has some limit on what it will install.

View Source
const MaxVLAN = 4095

MaxVLAN is the largest 802.1Q tag identifier: the field is twelve bits.

View Source
const OffloadHdrLen = 10

OffloadHdrLen is the size of the virtio-net header as it appears on the wire in front of a frame, without the mergeable-receive-buffer field.

Variables

View Source
var (
	// ErrClosed is returned by a method on a queue or device that has been
	// closed.
	ErrClosed = errors.New("packetio: closed")

	// ErrQueueFailed is returned when the hardware reported an error that put
	// the queue out of service. The queue accepts no further work; close it and
	// open a new one.
	ErrQueueFailed = errors.New("packetio: queue failed")

	// ErrBadLength is returned when a build callback reports a length outside
	// the frame it was given. Nothing is transmitted.
	ErrBadLength = errors.New("packetio: build returned invalid length")

	// ErrUnsupported is returned for an option or operation this backend or
	// this NIC cannot provide.
	ErrUnsupported = errors.New("packetio: unsupported")
)

Functions

This section is empty.

Types

type Capabilities

type Capabilities struct {
	// Backend names the implementation: "mlx5", "afxdp", "afpacket", "dpdk".
	Backend string

	// ZeroCopy is true when the NIC DMAs directly to and from the Region, with
	// no copy in the kernel.
	ZeroCopy bool

	// KernelCoexistence is true when the kernel keeps its own interface to
	// this device while it is open: packets the steering filter does not
	// match still reach Linux, so SSH, ARP and monitoring carry on. It is
	// false when opening the device took the port away from the kernel
	// entirely -- then nothing but these queues sees the wire, whatever the
	// filter says. A property of the opened device, not the backend: dpdk
	// answers true on a bifurcated ConnectX and false on an Intel card bound
	// to vfio-pci. A program must not open a device it also manages itself
	// over -- its management NIC -- unless this is true.
	KernelCoexistence bool

	// MultiBuffer is true when a packet may span several frames, marked with
	// OptContinued.
	MultiBuffer bool

	// RSS is true when receive traffic can be spread over several queues by a
	// per-flow hash -- the NIC's RSS, or the kernel's fanout hash where the
	// backend is a packet socket. What a caller may rely on is the spreading,
	// and that one flow stays on one queue; not where the hash is computed.
	RSS bool

	// TxChecksumOffload is true when the NIC can compute L3 and L4 checksums on
	// transmit.
	TxChecksumOffload bool

	// Offload is true when the queues implement [OffloadReceiver] and
	// [OffloadTransmitter], so segmentation and checksum metadata travels with
	// each frame and a super-frame can cross this device whole.
	Offload bool

	// RxChecksumFlags is true when received descriptors carry OptChecksumOK.
	RxChecksumFlags bool

	// GatherTx is true when the transmit queues implement
	// [GatherTransmitter], so a packet may be sent straight out of the
	// caller's memory without being copied into a frame first.
	//
	// It is false wherever the hardware reads the bytes after the call
	// returns, which is every device that does its own DMA: there the memory
	// has to be registered first, and a Region is what registered memory
	// looks like here.
	GatherTx bool

	// RxTimestamps is true when the receive queues implement
	// [TimestampReceiver], so every packet arrives with the time the device
	// recorded for it.
	RxTimestamps bool

	// BlockingPoll is true when RxQueue.Poll can sleep rather than spin.
	BlockingPoll bool

	// SharedRegion is true when all queues draw on one Region, so a frame can
	// move between queues without a copy.
	SharedRegion bool

	// HandsBackFrames is true when TxQueue.Reclaim can name the frames it
	// reclaimed and hand them to the caller.
	//
	// Where it is false the backend cannot see which frames came back -- AF_XDP
	// drains its completion ring straight into a pool -- so Reclaim completes
	// and returns nothing, and a forwarder must arrange for the frames to reach
	// the receive side another way. A forwarder written to the interface should
	// check this rather than discover it as a queue that slowly stops
	// receiving.
	HandsBackFrames bool

	// MaxFrameSize is the largest packet one frame carries -- the frame less
	// whatever headroom the backend or the kernel keeps in front of it -- and
	// MaxQueues the most queues of either direction that can be opened: the
	// backend's ceiling, lowered to the device's own limit where the backend
	// can see it.
	MaxFrameSize int
	MaxQueues    int
}

Capabilities reports what a Device supports. A field that is false or zero means "not available here", never "unknown".

type Desc

type Desc struct {
	// Addr is the byte offset of the frame within the Region. For a received
	// frame it points at the first byte of the Ethernet header, which need not
	// be the start of the frame if the backend reserved headroom.
	Addr uint64

	// Len is the number of valid bytes at Addr: the Ethernet frame length,
	// excluding the FCS the NIC appends on transmit and strips on receive.
	Len uint32

	// Options carries backend-defined flags. Bits below OptionsBackendShift are
	// reserved for this package; the rest belong to the backend that produced
	// the descriptor. Transmit ignores these bits, so a forwarder may pass a
	// received descriptor straight back without clearing them.
	Options uint32
}

Desc identifies one frame within a Region: where it starts and how many bytes of it are meaningful.

The layout matches AF_XDP's xdp_desc field for field, which keeps the AF_XDP adapter's conversion trivial. It is a field-by-field copy, not a cast: nothing here depends on the two structs having the same memory layout, and nothing should start to without a compile-time assertion.

type Device

type Device interface {
	// Capabilities describes what this backend and this NIC can do.
	Capabilities() Capabilities

	// NumTxQueues and NumRxQueues are how many queues were opened.
	NumTxQueues() int
	NumRxQueues() int

	// TxQueue and RxQueue return queue i, or nil if i is out of range.
	TxQueue(i int) TxQueue
	RxQueue(i int) RxQueue

	// Close shuts down every queue and releases the Region. No queue method may
	// be running in another goroutine.
	Close() error
}

Device is a NIC opened for packet I/O: a set of queues sharing one Region.

type GatherTransmitter added in v0.1.2

type GatherTransmitter interface {
	TxQueue

	// TransmitGather sends packets whose bytes are the caller's. segs holds
	// every packet's slices back to back, counts[i] is how many belong to
	// packet i, and offs, when not nil, is one Offload per packet.
	//
	// It returns how many packets were accepted, always a prefix, and takes a
	// packet whole or not at all. There is nothing to complete or reclaim:
	// the memory is the caller's again as soon as this returns.
	TransmitGather(segs [][]byte, counts []int, offs []Offload) (int, error)
}

GatherTransmitter is implemented by a transmit queue that can send a packet out of the caller's own memory, rather than out of frames taken from its pool. Use it through a type assertion, and only where Capabilities.GatherTx is true:

if g, ok := tq.(packetio.GatherTransmitter); ok {
        n, err := g.TransmitGather(segs, counts, offs)
}

It exists for a caller whose packets already sit in its own buffers -- a forwarder carrying one packet as several -- for which copying them into the queue's region first is a copy of every byte that buys nothing.

Only a backend whose hardware has finished with the memory by the time the call returns can offer it. AF_PACKET can, because the kernel copies into an skb before sendmmsg returns. A NIC that reads the bytes by DMA long afterwards cannot: its memory has to be registered with the device first, which is what a Region is.

type Match

type Match struct {
	Kind MatchKind

	// MAC is set for MatchDstMAC.
	MAC [6]byte

	// VLAN is set for MatchVLAN, EtherType for MatchEtherType, IPProto for
	// MatchIPProto, and Port for the port matches.
	VLAN      uint16
	EtherType uint16
	IPProto   uint8
	Port      uint16

	// Prefix is set for MatchSrcIP and MatchDstIP. A single address is a
	// prefix with a full-length mask.
	Prefix netip.Prefix
}

A Match is one condition on a packet. Use the Match* constructors; the fields are exported so backends can compile them, not so callers can build them by hand.

func MatchDstIP

func MatchDstIP(p netip.Prefix) Match

MatchDstIP matches packets to an address or a prefix.

func MatchDstMAC

func MatchDstMAC(mac [6]byte) Match

MatchDstMAC matches packets addressed to one Ethernet address.

func MatchDstPort

func MatchDstPort(proto uint8, port uint16) []Match

MatchDstPort matches one destination port of the given protocol; see MatchSrcPort for why it emits the protocol match too.

func MatchEtherType

func MatchEtherType(t uint16) Match

MatchEtherType matches one EtherType, for example 0x0800 for IPv4.

func MatchIPProto

func MatchIPProto(p uint8) Match

MatchIPProto matches one IP protocol number, for example IPProtoUDP.

func MatchSrcIP

func MatchSrcIP(p netip.Prefix) Match

MatchSrcIP matches packets from an address or a prefix.

func MatchSrcPort

func MatchSrcPort(proto uint8, port uint16) []Match

MatchSrcPort and MatchDstPort match one L4 port of the given protocol.

The protocol is part of the match because that is how the hardware works: a card looks for a port at a fixed offset inside a protocol it was told to expect, so a port without a protocol would match the same offset in something else. Both matches are emitted, and a SteeringFilter carrying a port match of one protocol and MatchIPProto of another is refused.

func MatchVLAN

func MatchVLAN(id uint16) Match

MatchVLAN matches one 802.1Q tag identifier. Only the identifier is matched, so a packet's priority bits do not decide whether it arrives.

An identifier outside the twelve bits of a tag is refused by Validate rather than masked, because masking turns a typed digit into a filter for a VLAN the caller never named.

func (Match) String

func (m Match) String() string

String renders a match the way a person would say it, for the line a program prints at startup.

type MatchKind

type MatchKind uint8

MatchKind names what a Match tests.

const (
	MatchKindDstMAC MatchKind = iota + 1
	MatchKindVLAN
	MatchKindEtherType
	MatchKindIPProto
	MatchKindSrcIP
	MatchKindDstIP
	MatchKindSrcPort
	MatchKindDstPort
)

The kinds of match, one per constructor.

type Offload

type Offload struct {
	// Flags is OffloadNeedsCsum when the L4 checksum is only the pseudo-header
	// partial and somebody downstream must finish it.
	Flags uint8

	// GSOType is OffloadGSONone for an ordinary frame, or one of the
	// OffloadGSO* values for a super-frame to be segmented.
	GSOType uint8

	// HdrLen is how many bytes of headers (L2, L3 and L4 together) are
	// replicated into every segment.
	HdrLen uint16

	// GSOSize is the MSS: payload bytes per segment.
	GSOSize uint16

	// CsumStart is the offset from the start of the frame to the L4 header,
	// and CsumOff the offset from there to the two-byte checksum field.
	CsumStart uint16
	CsumOff   uint16
}

Offload describes segmentation and checksum work that something other than this program will do: the kernel, a NIC, or a virtio peer.

The layout is deliberately virtio_net_hdr, because that is the common currency of every path this library is likely to grow. PACKET_VNET_HDR hands it to an AF_PACKET socket, vhost-user puts it in front of every buffer between guest and device, and tap and memif use the same fields. A backend that speaks any of them can fill this in without translating, and code above packetio does not have to know which one it got.

The point of carrying it at all is throughput. A GSO "super-frame" is one buffer of up to 64 KB that the receiver segments to MTU, so one descriptor does the work of forty. Dropping the metadata means dropping to one packet per MSS, which is most of the difference between line rate on a core and not.

func UnmarshalOffload

func UnmarshalOffload(src []byte) Offload

UnmarshalOffload reads a virtio-net header from src, which must be at least OffloadHdrLen bytes.

Every field is written by something outside this program -- the kernel, or a guest -- so nothing here is trusted. Callers must bounds-check CsumStart and CsumOff against the frame before using them.

func (Offload) Marshal

func (o Offload) Marshal(dst []byte)

Marshal writes the header little-endian into dst, which must be at least OffloadHdrLen bytes.

func (Offload) Segmented

func (o Offload) Segmented() bool

Segmented reports whether this descriptor is a super-frame that somebody has to cut up, as opposed to an ordinary frame.

type OffloadReceiver

type OffloadReceiver interface {
	RxQueue

	// ReceiveOffload is Receive, and additionally returns one Offload per
	// descriptor. Both slices have the same length and are reused by the next
	// call, exactly as Receive's is.
	ReceiveOffload(max int) ([]Desc, []Offload)
}

OffloadReceiver is implemented by a receive queue that can report per-packet offload metadata. Use it through a type assertion:

if r, ok := rq.(packetio.OffloadReceiver); ok {
    descs, offs := r.ReceiveOffload(64)
}

A queue that does not implement it delivers ordinary frames, already segmented by whatever was in front of it.

type OffloadTransmitter

type OffloadTransmitter interface {
	TxQueue

	// TransmitOffload is Transmit with one Offload per descriptor; a zero
	// Offload sends an ordinary frame. It returns how many were accepted,
	// always a prefix, and an error when a descriptor or its Offload was
	// refused -- an offset past the frame, a segmented frame with no segment
	// size, or offs not matching descs in length -- or ErrUnsupported when
	// this queue was not opened with offload enabled. A full ring is not an
	// error: it is a short return with a nil error, exactly as for Transmit.
	TransmitOffload(descs []Desc, offs []Offload) (int, error)
}

OffloadTransmitter is implemented by a transmit queue that can carry offload metadata alongside each frame, so a super-frame is segmented by the kernel or the peer rather than here.

type Region

type Region interface {
	// Bytes returns the whole region. The slice aliases the mapping; it is not
	// a copy, and writing outside a frame the caller owns corrupts traffic.
	Bytes() []byte

	// Frame returns the bytes a descriptor names: Bytes()[d.Addr:d.Addr+d.Len].
	Frame(d Desc) []byte

	// Writable returns the whole of the frame containing d, from d.Addr to the
	// end of that frame. Use it to build a packet whose final length is not
	// known yet, then set Desc.Len before transmitting.
	Writable(d Desc) []byte

	// FrameSize is the size of one frame in bytes, and so the largest single
	// frame a packet can occupy.
	FrameSize() int

	// NumFrames is how many frames the region holds.
	NumFrames() int
}

Region is a contiguous run of frame memory shared with the NIC. It is allocated and registered once when a Device is opened, which is what pins it, and it stays valid until the Device is closed.

Frames are fixed size and laid out end to end: frame i occupies [i*FrameSize(), (i+1)*FrameSize()). Backends hand out descriptors whose Addr falls inside a frame, not necessarily at its start.

type Rule

type Rule struct {
	MAC    [6]byte
	MACSet bool

	VLAN    uint16
	VLANSet bool

	EtherType uint16 // 0 means not matched

	IPProto    uint8
	IPProtoSet bool

	SrcPrefix, DstPrefix netip.Prefix // invalid means not matched

	SrcPort, DstPort       uint16
	SrcPortSet, DstPortSet bool

	Promiscuous bool
}

A Rule is one conjunction a backend installs: every set field must hold for a packet to match. A SteeringFilter becomes one or more Rules through Rules, and a packet matching any of them is delivered.

Backends compile Rules, not Filters, so the expansion from "these ports on this VLAN" into one rule per port is done once, here, and means the same thing on a card, in an XDP program, and in a classic BPF program.

type RxQueue

type RxQueue interface {
	// Region is the frame memory this queue receives into.
	Region() Region

	// Fill posts up to n frames from the pool for the NIC to receive into and
	// returns how many it posted.
	Fill(n int) int

	// Poll waits until at least one packet has arrived or timeout elapses, and
	// returns how many packets are ready.
	//
	// A zero timeout polls without blocking. A negative timeout waits
	// indefinitely, and is only useful on a backend whose Close can wake it --
	// Capabilities.BlockingPoll says which. Backends that have no way to block
	// spin for up to timeout.
	//
	// Poll returns ErrClosed if the queue is closed while it waits.
	Poll(timeout time.Duration) (int, error)

	// Receive takes up to max received packets and returns their descriptors.
	// The returned slice is owned by the queue and is reused by the next call.
	//
	// The frames belong to the caller until Recycle, and belong to it alone:
	// no later Receive, no Fill, nothing the queue does in between writes to
	// one, hands it out again, or moves it, and Region.Bytes is one mapping
	// for the life of the device. So a forwarder may carry received frames
	// through whatever it does next rather than copying them out first.
	Receive(max int) []Desc

	// Err reports that the queue is out of service, or nil while it is
	// healthy. It wraps ErrQueueFailed, and is safe to call from any
	// goroutine.
	//
	// A receive queue that has stopped looks exactly like a quiet link:
	// Receive returns nothing either way, and the two want opposite
	// responses. A receiver that has seen no packets for a while should ask.
	Err() error

	// Recycle returns received frames to the pool.
	Recycle(descs []Desc)

	// NumFreeFillSlots is how many more frames the receive ring can hold.
	NumFreeFillSlots() int

	// NumReceived is how many packets are ready for Receive right now.
	NumReceived() int

	// NumFreeFrames is how many frames are in the free pool.
	NumFreeFrames() int

	// Stats reports counters for this queue. Like Err, it is safe to call
	// from another goroutine while the queue's own goroutine drives it.
	Stats() (RxStats, error)

	// Close releases the queue.
	Close() error
}

RxQueue is one hardware receive queue.

An RxQueue is owned by one goroutine. The receive cycle is:

q.Fill(q.NumFreeFillSlots())    // give the NIC frames to receive into
n, err := q.Poll(timeout)       // wait for packets
descs := q.Receive(n)           // take them
... use descs ...
q.Recycle(descs)                // give the frames back

A backend may need frames posted before it can receive anything, so Fill comes first, and a receiver that never recycles will starve itself.

type RxStats

type RxStats struct {
	// Packets and Bytes are what was handed to the application by Receive.
	Packets uint64
	Bytes   uint64

	// Filled is how many frames were posted for the NIC to receive into.
	Filled uint64

	// Batches is how many times Receive asked the hardware and was given
	// something: one call that returned packets. Packets divided by Batches is
	// the effective receive batch size, and it is worth watching.
	//
	// A receive loop that looks busy while taking a fraction of the offered
	// load is usually taking it a handful of packets at a time, paying a
	// call's fixed cost over too few of them and leaving the card short of
	// posted buffers between bursts. Nothing else in this struct shows that:
	// packets, bytes and drops all look the same whether they arrived sixty
	// at a time or five. A backend once lost half its receive rate to exactly
	// that, invisibly, because this counter was not published.
	Batches uint64

	// Polls is how many times Poll was called, and Completions how many
	// completion events were consumed.
	Polls       uint64
	Completions uint64

	// PoolEmpty counts refills that posted nothing because no frame was free,
	// which is the application holding on to received frames too long.
	PoolEmpty uint64

	// Errors counts error completions and malformed descriptors.
	Errors uint64

	// Dropped is what the NIC or kernel discarded before the application saw
	// it, when the backend can report it. It is not always attributable to one
	// queue; a backend that cannot tell leaves it zero and reports the device
	// wide figure in Backend.
	Dropped uint64

	// Backend holds counters only this backend has.
	Backend map[string]uint64
}

RxStats counts what one receive queue has done.

type SteeringFilter

type SteeringFilter struct {
	// Match is the set of conditions a packet must satisfy.
	//
	// Matches of different kinds are ANDed, and repeated matches of one kind
	// are alternatives. Two MatchDstPort and one MatchVLAN means "either port,
	// on that VLAN" -- two rules. A filter that needs more than MaxRules of
	// them is refused rather than narrowed.
	Match []Match

	// Promiscuous takes every packet the port sees, whoever it is addressed
	// to, and ignores Match entirely.
	Promiscuous bool
}

A SteeringFilter says which packets are steered to a device: taken away from the kernel and delivered to this program's receive queues. Packets it does not match are left to the kernel, so the interface keeps working -- your SSH session, ARP, and everything else carry on while you take the traffic you asked for.

That second half is a property of the device, not of the filter: Capabilities.KernelCoexistence says whether the kernel still has the interface at all. Where it is false -- DPDK on a device bound to vfio-pci -- the filter still selects what these queues see, but there is no kernel for the rest to carry on to.

Steering is a property of the backend. mlx5 compiles it to hardware flow rules the card matches at no cost per packet; AF_XDP to an eBPF program. AF_PACKET has no steering at all: it is a tap, the kernel sees every packet whatever a socket takes, and so it offers no SteeringFilter rather than one that would mean something weaker under the same name. A backend that cannot express a match refuses it at Open with ErrUnsupported rather than installing a wider one -- a filter that silently delivers more than it was asked for is worse than none, because nothing downstream can tell.

The zero SteeringFilter means "use the backend's default", which is packets addressed to this interface.

func (SteeringFilter) Rules

func (f SteeringFilter) Rules() ([]Rule, error)

Rules expands a filter into the conjunctions a backend installs.

Within a SteeringFilter, repeated matches of one kind are alternatives and different kinds are ANDed: two MatchDstPort and one MatchVLAN is "either port, on that VLAN", and becomes two rules. It validates first, so a contradictory filter is refused before any backend sees it.

func (SteeringFilter) String

func (f SteeringFilter) String() string

String renders a filter for the startup line.

func (SteeringFilter) Validate

func (f SteeringFilter) Validate() error

Validate reports whether a filter is self-consistent, independent of any backend. It catches the contradictions that would otherwise become a rule meaning something other than what was asked.

type TimestampReceiver added in v0.1.2

type TimestampReceiver interface {
	RxQueue

	// ReceiveTimestamps is Receive, and additionally returns the arrival time
	// of each packet in nanoseconds. Both slices have the same length and are
	// reused by the next call, exactly as Receive's is.
	//
	// The clock is the device's, not the wall's: it advances with an epoch
	// nobody promises, so two timestamps from one queue may be subtracted and
	// a timestamp on its own means nothing. Comparing across devices needs
	// them disciplined to a common source, which this package does not do,
	// and neither does anything here tell you when a packet was SENT: no such
	// time travels in a packet. What can be measured is time on this machine
	// -- how long a packet was held before it went back out, how long it sat
	// between the device and this code, and how evenly traffic arrived.
	//
	// Both slices belong to the queue and are overwritten by the next receive
	// call on it, including a plain Receive. Copy what must outlive that.
	//
	// A timestamp says when a packet ARRIVED, which is not the same as the
	// order it was delivered in. A NIC stamps at the port and places into the
	// queue afterwards, and when it is dropping -- offered more than it can
	// place -- the two orders come apart: measured on a ConnectX-6 Dx, stamps
	// rise packet by packet at rates the card keeps up with, and about a third
	// of them arrive out of order once it is discarding two thirds of the
	// wire. That is the hardware answering honestly about arrival, so code
	// that needs ordering must sort, and code measuring the wire should prefer
	// the stamps to the order they came in.
	ReceiveTimestamps(max int) ([]Desc, []uint64)
}

TimestampReceiver is implemented by a receive queue whose device records when each packet arrived. Use it through a type assertion:

if r, ok := rq.(packetio.TimestampReceiver); ok {
        descs, ts := r.ReceiveTimestamps(64)
}

The point of a timestamp taken by the device is that it is not a measurement of this program. A NIC stamps a frame as it arrives at the port, before the DMA, before the completion, before any of this code runs; the interval between two of them is what happened on the wire, and it stays true however long a receive loop was busy elsewhere. Reading the clock in the receive loop instead measures the loop.

Capabilities.RxTimestamps is the authority on whether a device really stamps, and is worth asking first: a queue may carry the method while its device has no clock, and it then returns nothing rather than inventing zeroes.

type TxQueue

type TxQueue interface {
	// Region is the frame memory this queue draws from.
	//
	// Whether it is the same Region as another queue's is
	// Capabilities.SharedRegion. Where it is, a frame received on one queue
	// may be transmitted on another without a copy; where it is not -- AF_XDP
	// maps one region per socket -- moving a frame between queues means
	// copying it. A queue refuses a foreign descriptor where it can tell --
	// the address is outside its region -- but two regions of one size look
	// alike to a bounds check, so keeping descriptors with the device they
	// came from is the caller's half of the contract.
	Region() Region

	// Alloc takes up to n frames from the queue's free pool and returns
	// descriptors for them, with Len set to zero. It returns fewer than n, or
	// none at all, when the pool or the transmit ring is short.
	//
	// The returned slice is owned by the queue and is reused by the next call
	// to Alloc; copy it if it must outlive that.
	Alloc(n int) []Desc

	// Transmit hands descriptors to the NIC and returns how many it accepted,
	// always a prefix of descs -- and, where a backend carries a packet as
	// several frames chained with OptContinued, a prefix of whole packets:
	// half a packet accepted would leave the caller with a tail nothing can
	// interpret. Frames in the accepted prefix now belong to
	// the NIC and must not be touched until Complete or Reclaim returns them.
	// Frames in the unaccepted suffix still belong to the caller, who must
	// transmit them later or return them with Free. No backend returns an
	// accepted frame to its pool on its own.
	//
	// Every descriptor must name at least one byte inside one frame of the
	// Region. A backend that checks refuses the first descriptor that does
	// not, returning the prefix before it, so a short return with a free ring
	// points at the offending descriptor.
	//
	// Transmit publishes the batch to the hardware before it returns; there is
	// no separate kick or flush step.
	//
	// A short return is backpressure, not an error: the ring is full, or a
	// descriptor was refused. Ask Err whether the queue is still alive.
	Transmit(descs []Desc) int

	// Err reports that the queue is out of service, or nil while it is
	// healthy. It wraps ErrQueueFailed, and is safe to call from any
	// goroutine.
	//
	// A dead queue and a full one both make Transmit return zero, so without
	// this a caller cannot tell "try again in a moment" from "this will never
	// work again" -- and the second one looks exactly like a slow link
	// forever. A caller that loops on Transmit should ask after a run of
	// zeroes. Close it and open a new device; nothing revives a failed queue.
	Err() error

	// Complete reclaims frames whose transmission has finished, returning them
	// to the pool, and reports how many it reclaimed.
	//
	// max bounds the work rather than the result: a backend stops looking once
	// it has that many, but it will not split what one completion covers, and
	// on a backend that reports one completion per batch that means a whole
	// batch comes back at once. Pass the largest number that is useful and
	// treat the result as the answer.
	Complete(max int) int

	// Reclaim is Complete for frames that belong somewhere else: it takes up
	// to max frames whose transmission has finished and appends them to out
	// instead of returning them to this queue's pool. The caller owns them
	// and must Recycle them to the receive queue they came from, or Free them
	// here. max bounds the work the same way it does for Complete.
	Reclaim(max int, out []Desc) []Desc

	// Free returns frames to the pool without transmitting them. It is for
	// descriptors Alloc handed out that will not be sent, and for the suffix
	// Transmit did not accept.
	Free(descs []Desc)

	// NumCompleted is how many frames Complete would reclaim right now.
	NumCompleted() int

	// NumInFlight is how many frames the NIC currently owns.
	NumInFlight() int

	// NumFreeSlots is how many more frames the transmit ring can accept.
	NumFreeSlots() int

	// NumFreeFrames is how many frames are in the free pool.
	NumFreeFrames() int

	// SendFunc is the whole transmit cycle in one call: it reclaims
	// completions, takes up to count frames, calls build for each, and
	// transmits them. build writes into frame and returns the packet length.
	// Zero means there is nothing more to send: the batch ends there and what
	// was built is transmitted. A length below zero or past len(frame) is
	// ErrBadLength: the entire batch is abandoned, every frame returns to the
	// pool, and nothing is transmitted. SendFunc reports how many packets
	// reached the NIC.
	SendFunc(count int, build func(i int, frame []byte) int) (int, error)

	// Stats reports counters for this queue. Like Err, it is safe to call
	// from another goroutine while the queue's own goroutine drives it --
	// that is what it is for, and every example does it.
	Stats() (TxStats, error)

	// Close releases the queue. It is not safe to call while another goroutine
	// is inside any other method of this queue.
	Close() error
}

TxQueue is one hardware transmit queue.

A TxQueue is owned by one goroutine. The transmit cycle is:

q.Complete(q.NumCompleted())    // reclaim frames the NIC is done with
descs := q.Alloc(n)             // take frames from the pool
for i := range descs {          // fill them
        b := q.Region().Writable(descs[i])
        descs[i].Len = uint32(build(b))
}
sent := q.Transmit(descs)       // hand them to the NIC

Alloc never returns more frames than the following Transmit can accept, so a caller that transmits exactly what it allocated can never leak a frame.

A forwarder sends frames it received rather than frames it allocated. Every queue of a Device shares one Region, so that needs no copy, but the frames belong to the receive queue's pool and must find their way back there:

descs := rx.Receive(n)          // frames from the receive pool
... rewrite them in place, set Len ...
sent := tx.Transmit(descs)      // the NIC owns them now
back = tx.Reclaim(max, back[:0])
rx.Recycle(back)                // home again
rx.Fill(rx.NumFreeFillSlots())

Complete would put them on the transmit pool instead, where the receive queue can never find them again. A receive queue and every transmit queue that carries its frames must be driven by the same goroutine, because the pools are not synchronised.

type TxStats

type TxStats struct {
	// Packets and Bytes are what reached the NIC: frames accepted by Transmit,
	// and the sum of their lengths.
	Packets uint64
	Bytes   uint64

	// Completed is how many frames the NIC has finished sending and Complete
	// has reclaimed. Packets minus Completed is what the NIC still owns.
	Completed uint64

	// Batches is how many times a batch was published to the hardware: one
	// doorbell on mlx5, one kick or suppressed kick on AF_XDP. Packets divided
	// by Batches is the effective batch size.
	Batches uint64

	// Completions is how many completion events were consumed: completion queue
	// entries on mlx5, completion ring entries on AF_XDP. On a backend that
	// signals once per batch this is far smaller than Completed.
	Completions uint64

	// RingFull counts calls that could queue nothing because the transmit ring
	// had no free slots, and PoolEmpty calls that could allocate nothing
	// because every frame was in flight. Both mean the NIC is the limit.
	RingFull  uint64
	PoolEmpty uint64

	// Errors counts hardware or kernel errors reported for this queue:
	// error completions on mlx5, failed kicks on AF_XDP.
	Errors uint64

	// Backend holds counters only this backend has. Keys are lowercase and
	// stable within a backend, for example mlx5's "cqe_err" or AF_XDP's
	// "tx_invalid_descs".
	Backend map[string]uint64
}

TxStats counts what one transmit queue has done. Counters are cumulative since the queue was opened and never reset.

Directories

Path Synopsis
Package afpacket drives a NIC through an ordinary AF_PACKET socket: a TPACKET_V3 memory-mapped ring on receive, and batched sendmmsg on transmit.
Package afpacket drives a NIC through an ordinary AF_PACKET socket: a TPACKET_V3 memory-mapped ring on receive, and batched sendmmsg on transmit.
Package afxdp is the packetio backend for Linux AF_XDP.
Package afxdp is the packetio backend for Linux AF_XDP.
dpdk
internal/mbuf
Package mbuf reads and writes the DPDK packet buffer header from Go.
Package mbuf reads and writes the DPDK packet buffer header from Go.
internal/queue
Package queue holds the frame-ownership logic of the DPDK backend, with no cgo and no DPDK in sight.
Package queue holds the frame-ownership logic of the DPDK backend, with no cgo and no DPDK in sight.
examples
hello command
Command hello is the smallest complete packetio program: it sends one frame and prints the next few it receives.
Command hello is the smallest complete packetio program: it sends one frame and prints the next few it receives.
internal/affinity
Package affinity pins a goroutine to a CPU.
Package affinity pins a goroutine to a CPU.
internal/frame
Package frame builds the Ethernet frames the examples transmit.
Package frame builds the Ethernet frames the examples transmit.
internal/metrics
Package metrics measures what a run cost: how busy the processors were and what the NIC's own counters say.
Package metrics measures what a run cost: how busy the processors were and what the NIC's own counters say.
internal/prefetch
Package prefetch hints frame memory into the cache ahead of first use.
Package prefetch hints frame memory into the cache ahead of first use.
steer command
Command filter receives with a filter and reports what arrives, by destination port, so you can see the filter doing its job.
Command filter receives with a filter and reports what arrives, by destination port, so you can see the filter doing its job.
sweep command
sweep drives every backend through the same three loops -- transmit, receive, forward -- so a comparison between them measures the backend and not four different programs.
sweep drives every backend through the same three loops -- transmit, receive, forward -- so a comparison between them measures the backend and not four different programs.
timestamps command
Command timestamps shows when packets actually arrived, using the time the device recorded for each one rather than the time this program got round to looking.
Command timestamps shows when packets actually arrived, using the time the device recorded for each one rather than the time this program got round to looking.
internal
affinity
Package affinity decides where a backend's workers run.
Package affinity decides where a backend's workers run.
conform
Package conform is the shared test suite every packetio backend must pass.
Package conform is the shared test suite every packetio backend must pass.
pool
Package pool holds the free-frame list shared by packetio backends.
Package pool holds the free-frame list shared by packetio backends.
mlx5
internal/arch
Package arch holds the memory-ordering and memory-mapped I/O primitives the mlx5 packet path needs.
Package arch holds the memory-ordering and memory-mapped I/O primitives the mlx5 packet path needs.
internal/clock
Package clock turns the tick counter in a completion into nanoseconds.
Package clock turns the tick counter in a completion into nanoseconds.
internal/mocknic
Package mocknic is a software model of the part of an mlx5 NIC that a send or receive queue talks to.
Package mocknic is a software model of the part of an mlx5 NIC that a send or receive queue talks to.
internal/ring
Package ring drives an mlx5 send or receive queue: it decides what goes in the queue, tracks who owns each frame, and reads completions back.
Package ring drives an mlx5 send or receive queue: it decides what goes in the queue, tracks who owns each frame, and reads completions back.
internal/wqe
Package wqe encodes and decodes mlx5 work queue entries and completion queue entries.
Package wqe encodes and decodes mlx5 work queue entries and completion queue entries.
netstack module

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL