Documentation
¶
Overview ¶
Package afpacket drives a NIC through an ordinary AF_PACKET socket: a TPACKET_V3 memory-mapped ring on receive, and batched sendmmsg on transmit.
It is the backend that works everywhere. There is no hardware requirement, no driver requirement, no cgo, and nothing to load: if the interface exists and the process has CAP_NET_RAW, this works. That is the whole reason it is here.
It is also, by a wide margin, the slowest of the three. Every packet is copied by the kernel into the ring and copied again out of it, and transmit costs a system call per batch rather than a doorbell write. Expect a couple of million packets per second where github.com/atoonk/packetio/afxdp does tens of millions and github.com/atoonk/packetio/mlx5 does more still. Use it for correctness, for portability, and as the floor to measure the others against, not to fill a 100G link.
There is no receive filter on this backend, deliberately. AF_PACKET is a tap: the kernel processes every packet whether or not a socket takes a copy, so nothing here could keep traffic away from the kernel or steer it toward this program. A filter would only decide what is copied into the ring, and offering that under the same name the steering backends use invites the wrong expectation. The receive queues take everything the interface sees; select in your receive loop, or use a backend that steers.
Like every packetio backend, a queue belongs to one goroutine. Nothing here is synchronised, and two goroutines sharing a queue will corrupt its pool. Different queues of one Device are independent and may run concurrently.
The receive path is a TPACKET_V3 memory-mapped ring; see tpacket.go for how it works and why it is a ring rather than recvmmsg.
Index ¶
- type Device
- func (d *Device) Capabilities() packetio.Capabilities
- func (d *Device) Close() error
- func (d *Device) Interface() string
- func (d *Device) MTU() int
- func (d *Device) NumRxQueues() int
- func (d *Device) NumTxQueues() int
- func (d *Device) Region() packetio.Region
- func (d *Device) Rx(i int) *RxQueue
- func (d *Device) RxQueue(i int) packetio.RxQueue
- func (d *Device) Tx(i int) *TxQueue
- func (d *Device) TxQueue(i int) packetio.TxQueue
- type Option
- type RxQueue
- func (q *RxQueue) Close() error
- func (q *RxQueue) Err() error
- func (q *RxQueue) Fd() int
- func (q *RxQueue) Fill(n int) int
- func (q *RxQueue) NumFreeFillSlots() int
- func (q *RxQueue) NumFreeFrames() int
- func (q *RxQueue) NumReceived() int
- func (q *RxQueue) Poll(timeout time.Duration) (int, error)
- func (q *RxQueue) Receive(max int) []packetio.Desc
- func (q *RxQueue) ReceiveOffload(max int) ([]packetio.Desc, []packetio.Offload)
- func (q *RxQueue) ReceiveTimestamps(max int) ([]packetio.Desc, []uint64)
- func (q *RxQueue) Recycle(descs []packetio.Desc)
- func (q *RxQueue) Region() packetio.Region
- func (q *RxQueue) Stats() (packetio.RxStats, error)
- type TxQueue
- func (q *TxQueue) Alloc(n int) []packetio.Desc
- func (q *TxQueue) Close() error
- func (q *TxQueue) Complete(max int) int
- func (q *TxQueue) Err() error
- func (q *TxQueue) Free(descs []packetio.Desc)
- func (q *TxQueue) LastErrno() int32
- func (q *TxQueue) NumCompleted() int
- func (q *TxQueue) NumFreeFrames() int
- func (q *TxQueue) NumFreeSlots() int
- func (q *TxQueue) NumInFlight() int
- func (q *TxQueue) Reclaim(max int, out []packetio.Desc) []packetio.Desc
- func (q *TxQueue) Region() packetio.Region
- func (q *TxQueue) SendFunc(count int, build func(i int, frame []byte) int) (int, error)
- func (q *TxQueue) Stats() (packetio.TxStats, error)
- func (q *TxQueue) Transmit(descs []packetio.Desc) int
- func (q *TxQueue) TransmitGather(segs [][]byte, counts []int, offs []packetio.Offload) (int, error)
- func (q *TxQueue) TransmitOffload(descs []packetio.Desc, offs []packetio.Offload) (int, error)
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
This section is empty.
Types ¶
type Device ¶
type Device struct {
// contains filtered or unexported fields
}
Device is an open AF_PACKET capture of one interface.
Several receive queues means several sockets joined into a PACKET_FANOUT group, so the kernel spreads received packets across them by flow hash. Transmit queues are independent: any socket can send anything.
func (*Device) Capabilities ¶
func (d *Device) Capabilities() packetio.Capabilities
Capabilities describes what this backend can do, which is not much: the kernel copies every packet in both directions, and the only way to spread receive over several queues is the fanout group's flow hash.
func (*Device) Close ¶
Close releases the queues, the rings, the sockets and the frame memory.
It is safe to call on a half-built device, safe to call twice, and safe to call from another goroutine than the one using the queues: closing a receive queue wakes a goroutine blocked in its Poll, which then returns packetio.ErrClosed. It is still not safe to call while a goroutine is part-way through Receive or Transmit, which read memory this unmaps.
func (*Device) Interface ¶
Interface returns the captured interface's name, and MTU its MTU as it was when the device was opened.
func (*Device) NumRxQueues ¶
NumRxQueues is how many receive queues were opened.
func (*Device) NumTxQueues ¶
NumTxQueues is how many transmit queues were opened.
type Option ¶
type Option func(*config)
Option configures a Device.
func WithFrameSize ¶
WithFrameSize sets the size of one frame. A packet starts a little way into its frame, so the largest packet is smaller than this; ask Capabilities().MaxFrameSize rather than assuming. It must be a power of two. Without WithGSO the ceiling is the ring's 64 KiB block; the ring delivers packets up to about 65400 bytes (the block less its headers) and counts anything longer oversize. A 16384-byte frame carries a 9000-byte MTU whole.
func WithFrames ¶
WithFrames sets how many frames the region holds, shared out evenly between the queues.
func WithGSO ¶
func WithGSO() Option
WithGSO turns on PACKET_VNET_HDR, so segmentation and checksum offload metadata travels with each frame in both directions.
With it on, the kernel delivers a TCP super-frame of up to 64 KB whole rather than dropping or truncating it, and a frame transmitted through TxQueue.TransmitOffload is segmented by the kernel instead of here. That is worth a great deal: one descriptor carries what would otherwise be forty packets. The cost is memory, because every frame must now be big enough to hold a super-frame; use WithFrames to keep the region a sensible size.
It also makes the queues implement packetio.OffloadReceiver and packetio.OffloadTransmitter.
func WithMultiBuffer ¶ added in v0.1.2
func WithMultiBuffer() Option
WithMultiBuffer lets a received packet span several frames.
Without it a packet too big for one frame is counted oversize and dropped, because a caller who was handed the first frame of one and told nothing would forward a fragment. With it the packet is laid across as many frames as it takes, every one but the last marked packetio.OptContinued, and [Capabilities.MultiBuffer] says so.
It exists for a forwarder whose buffers are smaller than the packets it carries -- a segmentation-offloaded stream arrives in 64 KB super-frames -- so the ring is copied straight into those buffers instead of into one large frame and out of it again. That copy is free while a core has time to spare and is the whole cost once it does not.
Two things become the caller's business. A packet of n bytes needs ceil(n/[Device.MaxFrameSize]) descriptors, so the max passed to Receive must be at least that or the packet is dropped and counted -- 64 KB over 2 KB frames wants 33. And a partial checksum on a chained packet is left unfinished, because no one frame holds all the bytes it covers; the caller finishes it after reassembly, and nothing detects the omission.
func WithPromiscuous ¶
func WithPromiscuous() Option
WithPromiscuous puts the interface into promiscuous mode for as long as the device is open, so it takes packets not addressed to it. The kernel undoes this when the socket closes, including if the process dies.
func WithRxQueues ¶
WithRxQueues sets how many receive queues to open. More than one joins the sockets into a PACKET_FANOUT group, so the kernel spreads received packets across them by flow hash. Zero is allowed, for a transmit-only device.
func WithSocketBuffer ¶
WithSocketBuffer sets SO_RCVBUF and SO_SNDBUF on every socket.
func WithTxQueues ¶
WithTxQueues sets how many transmit queues to open. Zero is allowed, for a receive-only device.
type RxQueue ¶
type RxQueue struct {
// contains filtered or unexported fields
}
RxQueue receives from one socket's TPACKET_V3 ring.
The ring is the buffer the kernel fills, so unlike the other backends there is nothing to hand it in advance: Fill reports how many frames are free to copy into, and Receive copies out of the ring into them. That copy is not the expensive part of this backend, and removing it would mean handing callers pointers into a block the kernel wants back, which is a much worse trade.
One goroutine per queue. Nothing here is synchronised except the wake channel, which exists so Close can be called from another one.
func (*RxQueue) Close ¶
Close stops the queue, waking any goroutine blocked in Poll, and unmaps its ring. The socket belongs to the Device. It is safe to call twice.
func (*RxQueue) Err ¶
Err reports that the queue is out of service, or nil.
A packet socket has no failure that outlives one call: a ring that fills is backpressure and its drops are counted, not a fault. So this is nil until the queue is closed.
func (*RxQueue) Fd ¶ added in v0.1.1
Fd is the receive socket's file descriptor, for a caller that must wait on several queues at once.
Poll covers the ordinary case: one queue, its own wakeup on close. A caller driving several devices from one goroutine needs them all in a single poll(2) alongside its own wake descriptors, and cannot get that by calling Poll per queue. This is the afpacket twin of the afxdp backend's Socket().
The descriptor belongs to the queue: poll it, do not read, close, or otherwise operate on it, and do not use it after Close.
func (*RxQueue) Fill ¶
Fill reports how many frames are free to receive into. The kernel owns the ring it fills, so there is nothing to post; this exists so that code written against the other backends works unchanged.
func (*RxQueue) NumFreeFillSlots ¶
NumFreeFillSlots is how many frames could be received into now. The kernel owns the ring, so a free frame is a free slot.
func (*RxQueue) NumFreeFrames ¶
NumFreeFrames is how many frames are in the free pool.
func (*RxQueue) NumReceived ¶
NumReceived is at least how many packets Receive would return now: it counts the block the ring is on, and Receive drains across every block that is ready, so a busy queue often has more waiting than this reports.
func (*RxQueue) Poll ¶
Poll waits for packets and returns how many are ready.
A negative timeout waits indefinitely, zero returns at once, and a positive one waits that long. It returns packetio.ErrClosed if the queue is closed while it waits.
func (*RxQueue) Receive ¶
Receive copies up to max packets out of the ring into free frames and returns descriptors naming them. The returned slice is reused by the next call.
With WithMultiBuffer a packet larger than one frame is laid across several, every one but the last carrying packetio.OptContinued; without it such a packet is counted oversize and dropped.
Either way a packet is delivered whole or not at all: half a packet is not a shorter packet, it is a fragment the caller cannot recognise as one. What happens to a packet that will not fit depends on which room ran out. If the frames are momentarily gone, or this batch is simply full, it stays in the ring and arrives next call. If it needs more slots than max, no call of this size can ever take it, so it is counted oversize and dropped rather than retried forever -- waiting would stop the queue for good. Size the batch for the traffic: a packet of n bytes needs ceil(n/[Device.MaxFrameSize]) slots, so a forwarder carrying 64 KB super-frames over 2 KB frames wants max of at least 33.
func (*RxQueue) ReceiveOffload ¶
ReceiveOffload is Receive, and also returns the segmentation and checksum metadata the kernel reported for each frame. Without WithGSO every Offload is the zero value, which means an ordinary frame.
An ordinary frame delivered with only a partial checksum is finished here, and its OffloadNeedsCsum is cleared, because after that nobody downstream has to finish anything. A super-frame is left alone: its partial is what the segmenter works from, so the flag stays set and the frame keeps its partial all the way to whoever cuts it up. A packet spread across several frames by WithMultiBuffer is likewise left alone, because no one frame holds all the bytes the sum covers: its OffloadNeedsCsum stays set on the first descriptor and finishing it is the caller's job, after the chain is back together. A frame whose offsets did not fit is delivered untouched with the flag still set, and counted in Stats().Backend["bad_checksum"].
It implements packetio.OffloadReceiver.
func (*RxQueue) ReceiveTimestamps ¶ added in v0.1.2
ReceiveTimestamps is Receive, and also returns when the kernel stamped each frame, in nanoseconds.
The clock is CLOCK_REALTIME: this backend does not offer PACKET_TIMESTAMP, so it is always the kernel's wall-clock reading as the frame went into the ring, not the wire time a NIC would record. Being the wall clock, it can step: an adjustment moves these stamps with it, so a long measurement should treat a large jump as the clock being set rather than as traffic.
Offload metadata is not carried on this path. A device opened WithGSO whose caller needs both should use ReceiveOffload and read the times from its own clock, or open a second queue.
It implements packetio.TimestampReceiver.
func (*RxQueue) Recycle ¶
Recycle returns received frames to the pool. A frame this pool does not own is refused there and counted, not silently accepted.
func (*RxQueue) Stats ¶
Stats reports what this queue received, plus what the kernel dropped on its behalf. The kernel figure is the important one: it is the only evidence that a receiver fell behind, and it is invisible everywhere else.
Packets counts frames, not wire packets: a packet delivered as a chain of several under WithMultiBuffer counts once per frame, which is what keeps it consistent with the pool arithmetic. Backend["oversize"] is every packet dropped for not fitting, and Backend["batch_too_small"] is the part of that caused by the caller's max rather than its frame size -- the part a bigger batch would have fixed.
type TxQueue ¶
type TxQueue struct {
// contains filtered or unexported fields
}
TxQueue transmits with batched sendmmsg.
A transmit ring (PACKET_TX_RING) was tried in the code this is ported from and measured slower than sendmmsg, so it is deliberately not here: TPACKET_V2 is the only transmit ring the kernel offers, its frames are fixed size, and the doorbell is still a system call. One sendmmsg per batch with iovecs pointing straight into the region is both faster and much simpler.
sendmmsg is synchronous, so a frame the kernel accepted is finished the moment Transmit returns. It is still not handed back until Complete or Reclaim asks for it, because that is the contract every backend shares. The forwarding cycle in the packetio package doc transmits frames belonging to a receive queue and gets them back through Reclaim; a backend that quietly returned them to its own pool would starve that receive queue and alias its frames into this one. So sent frames wait on a pending list -- this backend's stand-in for a ring -- until they are asked for.
One goroutine per queue, as everywhere in packetio. There is no lock: the pool is unsynchronised by design and the scratch slices are reused.
func (*TxQueue) Alloc ¶
Alloc takes up to n frames from this queue's pool, never more than the pending list has room for. The returned slice is reused by the next call.
func (*TxQueue) Close ¶
Close stops the queue. The socket belongs to the Device and is closed there.
func (*TxQueue) Complete ¶
Complete returns up to max sent frames to this queue's pool and reports how many. Frames that came from a receive queue must go back through Reclaim instead; this pool refuses them.
func (*TxQueue) Err ¶
Err reports that the queue is out of service, or nil.
A packet socket has no failure that outlives one call: a send that fails fails for that batch, and the queue is as usable afterwards as before. So this is nil until the queue is closed, and that is the honest answer rather than a stub that can never say anything.
func (*TxQueue) Free ¶
Free returns frames to the pool without transmitting them. A frame this pool does not own is refused there and counted, not silently accepted.
func (*TxQueue) LastErrno ¶
LastErrno returns the errno that ended the most recent failed batch, or zero. Specific to this backend: it is the only way to tell a full device queue from an interface that has gone away.
func (*TxQueue) NumCompleted ¶
NumCompleted is how many frames Complete or Reclaim would hand back now.
func (*TxQueue) NumFreeFrames ¶
NumFreeFrames is how many frames are in the free pool.
func (*TxQueue) NumFreeSlots ¶
NumFreeSlots is how many more frames Transmit will accept before the pending list is full.
func (*TxQueue) NumInFlight ¶
NumInFlight is the same number. sendmmsg is synchronous, so everything the kernel took is already on the wire and merely waiting to be handed back; there is no moment when the NIC owns a frame this queue does not.
func (*TxQueue) Reclaim ¶
Reclaim hands up to max sent frames to the caller instead of returning them to this queue's pool, for frames that belong to a receive queue.
func (*TxQueue) SendFunc ¶
SendFunc is the whole transmit cycle in one call, as packetio.TxQueue describes it: reclaim, allocate, build, transmit.
func (*TxQueue) Stats ¶
Stats reports this queue's counters. Backend holds "rejected_descs", the descriptors refused before the syscall, and "pool_rejected", frames the pool refused as foreign or surplus; either being non-zero is a bug in the caller.
Packets counts frames, not wire packets: a packet sent as a chain counts once per frame, and a TransmitGather packet once per segment. That is the unit the pool and Completed are in, and mixing the two in one counter would make any rate computed from it move with the traffic mix.
func (*TxQueue) Transmit ¶
Transmit sends descs and returns how many the kernel accepted, always a prefix. Accepted frames wait for Complete or Reclaim; the unaccepted suffix still belongs to the caller. A packet may be given as several frames, each but the last marked OptContinued: the kernel then gathers them straight out of the region and nothing is copied to make the packet contiguous first. Such a packet is taken whole or not at all, so the prefix this returns never stops inside one. Chained transmit needs no option: a batch is chained if its descriptors say so. Capabilities.MultiBuffer describes the receive side, which does need one -- see WithMultiBuffer.
func (*TxQueue) TransmitGather ¶ added in v0.1.2
TransmitGather sends packets whose bytes are the caller's, not this queue's.
segs holds every packet's slices back to back and counts[i] says how many belong to packet i; offs, when not nil, is one Offload per packet, zero for an ordinary one. It returns how many packets were accepted, always a prefix, and a packet goes whole or not at all.
There is no ownership to hand over and nothing to reclaim, which is why this can exist at all: sendmmsg copies into the kernel before it returns, so the bytes are the caller's again the moment the call does. Nothing is taken from the pool, nothing waits on the pending list, and Complete has nothing of this to give back.
It exists for a forwarder whose packets already sit in its own buffers. Copying them into this queue's region first would protect nothing -- the region buys safety where a NIC reads memory after the call, and here none does -- while costing a copy of every byte, which is the whole of the difference between this backend and a native one on offloaded traffic.
It implements packetio.GatherTransmitter, and only a backend whose hardware reads nothing after the call can: [Capabilities.GatherTx] says which. mlx5 and dpdk cannot, because their devices read caller memory long after, and it must be registered with them first.
func (*TxQueue) TransmitOffload ¶
TransmitOffload is Transmit with segmentation and checksum metadata for each frame, so a super-frame is cut up by the kernel rather than here. A zero Offload sends an ordinary frame.
It implements packetio.OffloadTransmitter: it returns the accepted prefix, and an error when a descriptor or its Offload was refused, or when this queue was not opened with WithGSO.