Showing posts with label network. Show all posts
Showing posts with label network. Show all posts

Oct 31, 2026

Low-Latency Networking: Tuning NIC Ring Buffers and Kernel Socket Bypass

Reference:
https://docs.ebpf.io/


When building high-frequency trading systems, real-time audio pipelines, or distributed low-latency services, the Linux kernel's standard TCP/IP networking stack can become a major bottleneck. Every packet copy, interrupt moderation delay, and kernel-space traversal adds precious microseconds of jitter.

In this post, we explore how to optimize your network interface cards (NICs) by scaling ring buffers, disabling interrupt coalescing, and bypassing the kernel entirely using modern zero-copy primitives like AF_XDP.

1. Maximizing NIC Ring Buffers (ethtool)

Network interface cards use circular ring buffers (queues of buffer descriptors) to handle incoming (RX) and outgoing (TX) packets. Under heavy traffic bursts, default ring sizes (often 256 or 512 descriptors) fill up instantly, causing dropped packets and costly retransmissions.

Scaling Ring Sizes

Check your NIC's maximum supported ring limits and current settings:

ethtool -g eth0

Maximize the RX and TX ring sizes to their peak limits (e.g., 4096) to absorb traffic bursts:
sudo ethtool -G eth0 rx 4096 tx 4096


Eliminating Interrupt Coalescing

By default, NIC drivers use interrupt coalescing—delaying hardware interrupts for a few microseconds (rx-usecs) to batch packets and save CPU cycles. For low-latency systems, this delay is unacceptable. To force immediate interrupt delivery upon packet arrival:

sudo ethtool -C eth0 rx-usecs 0 rx-frames 1


2. Kernel Socket Bypass Technologies

Standard socket calls (recv(), send()) force packets to copy through the Linux kernel's networking stack, incurring context switches and memory allocation overhead. Kernel bypass technologies eliminate this by mapping NIC ring buffers directly into user space.

Native Zero-Copy with AF_XDP

AF_XDP is an address family built directly into the Linux kernel specifically optimized for high-performance packet processing.

  • How it works: An unprivileged application allocates an aligned memory region (UMEM), registers it with the kernel, and maps kernel ring descriptors (fill_ring, rx_ring) into user space using mmap.

  • The Result: The NIC driver performs DMA transfers directly into user-space buffers, allowing your application to poll and process packets instantly with zero syscall overhead or memory copying.

Alternative Enterprise Bypass (DPDK & Onload)

For sub-microsecond HFT environments, standard OS primitives are often replaced entirely:

  • DPDK (Data Plane Development Kit): Replaces the kernel network driver with a poll-mode driver (PMD) running in a tight user-space polling loop.

  • Solarflare OpenOnload: Transparently bypasses the OS kernel stack via user-space virtual interfaces (EF_VI) to accelerate standard TCP/UDP socket calls.

Summary Blueprint for Low-Latency Networking

  1. Enlarge Ring Buffers: Maximize RX/TX ring sizes with ethtool -G to prevent buffer overflows under burst loads.

  2. Disable Coalescing: Set rx-usecs 0 to fire hardware interrupts immediately.

  3. Deploy Kernel Bypass: Use AF_XDP or DPDK to map NIC queues directly into user space, eliminating kernel stack traversal and memory copying overhead.

C++ AF_XDP Polling Loop Example

#include <iostream>
#include <vector>
#include <cstring>
#include <unistd.h>
#include <poll.h>
#include <sys/socket.h>
#include <linux/if_xdp.h>

// Dummy structures representing your mapped ring buffers and context
// (Assumes socket, rings, and UMEM have already been initialized and mmap'd as shown previously)
struct XdpPollerContext {
    int xsk_fd = -1;
    
    // Ring pointers mapped from kernel via mmap()
    // In production, these point to the actual mmap regions for fill, rx, tx, and comp rings.
    void* umem_area = nullptr;
    
    // Tracking free frame indices for the UMEM buffer
    std::vector<__u64> free_frames;
};

// Helper to reserve and push free buffers into the Fill Ring
void replenish_fill_ring(int xsk_fd, struct xdp_ring* fill_ring, std::vector<__u64>& free_frames) {
    __u32 index;
    // Reserve slots in the fill ring
    unsigned int rcvd = xsk_ring_prod__reserve(fill_ring, free_frames.size(), &index);
    if (rcvd == 0) return;

    for (unsigned int i = 0; i < rcvd; ++i) {
        *xsk_ring_prod__fill_addr(fill_ring, index++) = free_frames.back();
        free_frames.pop_back();
    }

    // Submit the filled entries to the kernel
    xsk_ring_prod__submit(fill_ring, rcvd);
}

// Main packet processing loop
void poll_rx_packets(XdpPollerContext& ctx, struct xdp_ring* rx_ring, struct xdp_ring* fill_ring) {
    struct pollfd pfd = {};
    pfd.fd = ctx.xsk_fd;
    pfd.events = POLLIN;

    std::cout << "Starting AF_XDP packet polling loop...\n";

    bool running = true;
    while (running) {
        // 1. Ensure the Fill ring has available buffers for incoming packets
        if (!ctx.free_frames.empty()) {
            // (Assuming helper or direct ring producer macros are used)
            // replenish_fill_ring(ctx.xsk_fd, fill_ring, ctx.free_frames);
        }

        // 2. Poll the socket for incoming traffic (blocks until packets arrive or timeout)
        int ret = poll(&pfd, 1, 1000); // 1-second timeout
        if (ret < 0) {
            if (errno == EINTR) continue;
            std::cerr << "Poll error: " << std::strerror(errno) << "\n";
            break;
        } else if (ret == 0) {
            // Timeout - no packets received in this interval
            continue;
        }

        // 3. Check the RX ring for completed packet descriptors
        // unsigned int rcvd = xsk_ring_cons__peek(rx_ring, 32, &idx);
        // if (rcvd > 0) {
        //     for (unsigned int i = 0; i < rcvd; ++i) {
        //         const struct xdp_desc* desc = xsk_ring_cons__rx_desc(rx_ring, idx++);
        //         
        //         // Access packet data directly in user-space UMEM area!
        //         void* packet_data = (char*)ctx.umem_area + desc->addr;
        //         uint32_t packet_len = desc->len;
        //
        //         // Process packet here...
        //         std::cout << "Received packet of size: " << packet_len << " bytes\n";
        //
        //         // After processing, add desc->addr back to free_frames to recycle the UMEM chunk
        //         ctx.free_frames.push_back(desc->addr);
        //     }
        //     
        //     // Release consumed descriptors back to the RX ring
        //     xsk_ring_cons__release(rx_ring, rcvd);
        // }
    }
}


High-Performance Networking: Deep Dive into XDP and AF_XDP

When building ultra-low latency systems, high-throughput firewalls, or custom load balancers, the Linux kernel's standard TCP/IP networking stack can become a major bottleneck. Every packet copy, interrupt moderation delay, and kernel-space traversal adds precious microseconds of overhead.

Here is a focused, end-to-end breakdown of XDP and AF_XDP, covering how they work, when to use them, and how to implement zero-copy packet processing in C++.

1. What is XDP?

XDP stands for eXpress Data Path. It is a programmable, high-performance packet processing framework built directly into the Linux kernel network stack, operating via eBPF (Extended Berkeley Packet Filter) programs attached directly to the network interface card (NIC) driver.

  • Earliest Possible Hook: XDP runs eBPF code the exact moment a packet is received by the NIC driver—long before the kernel allocates an sk_buff memory structure or traverses the heavy TCP/IP stack.

  • Line-Rate Execution: Capable of processing millions of packets per second (PPS) for tasks like DDoS mitigation, packet filtering, and early routing.

2. What is AF_XDP and When Should You Choose It?

AF_XDP (Address Family XDP) is a specialized socket family designed for high-speed packet I/O that bridges the gap between raw kernel-bypass frameworks and standard Linux sockets.

When to Choose AF_XDP:

  1. Extreme Packet Rates: Your application needs to ingest or filter millions of packets per second where standard sockets (AF_INET / AF_PACKET) saturate the CPU.

  2. Coexistence with the Linux Kernel: Unlike legacy kernel-bypass frameworks (like DPDK) which completely take over the NIC driver, AF_XDP coexists with the Linux kernel. Regular management traffic, SSH, and standard tools (tcpdump, iptables) continue to run seamlessly on the same interface.

  3. Zero-Copy Performance: You want bare-metal DMA (Direct Memory Access) speeds—where the NIC writes incoming packets directly into your user-space memory buffers (UMEM) with zero copying.


3. Tight Busy-Polling Loop (xsk_ring_cons)

To process packets with absolute minimum latency without the context-switch overhead of poll(), use an unblocked while(true) busy-polling loop on your consumer ring (xsk_ring_cons), pinned to an isolated CPU core:


#include <linux/if_xdp.h>

void tight_busy_poll_loop(struct xsk_ring_cons* rx_ring, 
                           struct xsk_ring_prod* fill_ring, 
                           int xsk_fd, 
                           void* umem_area) {
    constexpr uint32_t BATCH_SIZE = 64;
    uint32_t rx_idx = 0;

    while (true) {
        // 1. Non-blocking peek: Check for new packet descriptors in the RX ring
        uint32_t rcvd = xsk_ring_cons__peek(rx_ring, BATCH_SIZE, &rx_idx);

        if (rcvd > 0) {
            for (uint32_t i = 0; i < rcvd; ++i) {
                const struct xdp_desc* desc = xsk_ring_cons__rx_desc(rx_ring, rx_idx++);
                
                // Zero-copy access to packet payload in registered UMEM
                void* packet_data = static_cast<char*>(umem_area) + desc->addr;
                uint32_t packet_len = desc->len;

                // Process packet instantly...
                (void)packet_data;
                (void)packet_len;
            }

            // 2. Release consumed descriptors back to the RX ring
            xsk_ring_cons__release(rx_ring, rcvd);
        } else {
            // 3. Handle kernel wakeup requirement if driver is sleeping
            if (xsk_ring_prod__needs_wakeup(fill_ring)) {
                char dummy;
                recvfrom(xsk_fd, &dummy, 0, MSG_DONTWAIT, nullptr, nullptr);
            }
        }
    }
}

Simplifying High-Performance AF_XDP Networking with libbpf Helpers

Writing raw AF_XDP code in C++ for ultra-low latency networking is powerful, but it traditionally comes with a steep learning curve and a mountain of boilerplate. Manually allocating page-aligned memory for UMEM, configuring multiple socket options via setsockopt, calculating ring offsets with mmap, and binding queues can quickly clutter your codebase.

Fortunately, libbpf provides clean, high-level helper functions—namely xsk_umem__create() and xsk_socket__create()—that drastically streamline AF_XDP setup.

The Power of libbpf XSK Helpers

Instead of manually wrestling with low-level kernel descriptors and memory mapping, libbpf wraps the entire initialization ritual into clean library APIs:

  1. xsk_umem__create(): Handles raw UMEM registration (XDP_UMEM_REG), memory allocation mapping offsets, and automatically sets up your Fill and Completion rings.

  2. xsk_socket__create(): Binds the AF_XDP socket to your target network interface and hardware queue while initializing your RX and TX consumer/producer rings.

Clean C++ Implementation

Here is how straightforward it is to initialize an AF_XDP session using libbpf helpers:



#include <iostream>
#include <vector>
#include <stdexcept>
#include <cstring>
#include <unistd.h>
#include <sys/mman.h>
#include <bpf/xsk.h>

struct XdpSession {
    struct xsk_umem* umem = nullptr;
    struct xsk_socket* xsk = nullptr;
    void* umem_buffer = nullptr;
    
    struct xsk_ring_prod fill_ring;
    struct xsk_ring_cons comp_ring;
    struct xsk_ring_cons rx_ring;
    struct xsk_ring_prod tx_ring;

    ~XdpSession() {
        if (xsk) xsk_socket__delete(xsk);
        if (umem) xsk_umem__delete(umem);
        if (umem_buffer) free(umem_buffer);
    }
};

XdpSession init_af_xdp(const char* ifname, int queue_id) {
    XdpSession session;

    // 1. Allocate page-aligned UMEM memory area (4096 frames of 4096 bytes)
    const size_t frame_size = XSK_UMEM__DEFAULT_FRAME_SIZE; 
    const size_t num_frames = 4096;
    const size_t umem_size = frame_size * num_frames;

    if (posix_memalign(&session.umem_buffer, getpagesize(), umem_size) != 0) {
        throw std::runtime_error("Failed to allocate aligned memory for UMEM");
    }

    // 2. Create and register UMEM using libbpf
    struct xsk_umem_config umem_cfg = {
        .fill_size = XSK_RING_PROD__DEFAULT_NUM_DESCS,
        .comp_size = XSK_RING_CONS__DEFAULT_NUM_DESCS,
        .frame_size = (unsigned int)frame_size,
        .frame_headroom = 0,
        .flags = 0
    };

    int ret = xsk_umem__create(&session.umem, 
                               session.umem_buffer, 
                               umem_size, 
                               &session.fill_ring, 
                               &session.comp_ring, 
                               &umem_cfg);
    if (ret) {
        throw std::runtime_error("Failed to create xsk umem: " + std::string(std::strerror(-ret)));
    }

    // 3. Create the AF_XDP socket
    struct xsk_socket_config xsk_cfg = {
        .rx_size = XSK_RING_CONS__DEFAULT_NUM_DESCS,
        .tx_size = XSK_RING_PROD__DEFAULT_NUM_DESCS,
        .libbpf_flags = XSK_SLIBBPF_FLAGS__INHIBIT_PROG_LOAD,
        .xdp_flags = 0,
        .bind_flags = XDP_USE_NEED_WAKEUP
    };

    ret = xsk_socket__create(&session.xsk, 
                             ifname, 
                             queue_id, 
                             session.umem, 
                             &session.rx_ring, 
                             &session.tx_ring, 
                             &xsk_cfg);
    if (ret) {
        throw std::runtime_error("Failed to create xsk socket: " + std::string(std::strerror(-ret)));
    }

    std::cout << "Successfully created libbpf AF_XDP socket on " << ifname << " queue " << queue_id << "\n";
    return session;
}


Summary

By leveraging libbpf's built-in XSK helpers, you eliminate dozens of lines of complex socket option and memory mapping code. This lets you jump straight into what matters most: writing high-performance, zero-copy packet processing loops for your low-latency systems.

Dec 10, 2020

[sigstop] sigstop while kernel TCP stack per process continues to receive package

Reference:
What happens to requests to a service that is stopped with SIGSTOP


From the point of view of any part of the system that isn't concerned with the process's state and stats, a process that is stopped (i.e. won't be scheduled until it receives a SIGCONT) is indistinguishable from a process that's running, but isn't responding to a particular query.

While the process is stopped, there's no such thing as a timeout in the process. There are usually no timeouts in the network stack either: the packet has been received by the machine, even if it hasn't gotten to the machine. As far as e.g. TCP transmission is concerned, the packet has been received and it's up to the application to respond.

If the socket's buffer fills up, the network stack will start dropping packets.

There's no reason why the behavior of the network stack would depend on the process's state. The process could come out of the stopped state at any time. There could be multiple processes listening on a socket, so any decision based on the process state would have to take all of them into account.

Dec 17, 2019

[nmap] tcp/ip layer detection

Recently there's a project started at company which reminds me wrote an nmap fork 8 years ago... :-)

https://github.com/verbalsaint/P538_Project_2

Nov 23, 2019

[network] TAP / Linux Bridge

Reference:
https://linux-blog.anracom.com/tag/linux-bridge-linking/
https://docs.docker.com/network/bridge/

Virtual "veth" Ethernet devices are used for connecting virtual containers to virtual bridges like an Linux bridge.

"veth" comes with pair, which is used to connect between linux network namespace.

For security reason, avoid giving a Linux bridge itself an IP.


TAP:
A virtual "tap" device is a single point to point device which can be used by a program in user-space or a virtual machine to send Ethernet packets on layer 2 directly to the kernel or receive packets from it.


Limitations of standard Linux bridges:
  • A "tap" device attached to one Linux bridge cannot be attached to another Linux bridge.
  • All attached devices are switched into the promiscuous mode.
  • The bridge itself (not a tap device at a port!) can get an IP address and may work as a standard Ethernet device. The host can communicate via this address with other guests attached to the bridge.
  • You may attach several physical Ethernet devices (without IP !) of the host to a bridge - each as a kind of "uplink" to other physical switches/hubs and connected systems. With the spanning tree protocol activated all physical systems attached to the network behind each physical interface may communicate with physical or virtual guests linked to the bridge by other physical interfaces or virtual ports.
  • Properly configured the bridge transfers packets directly between two specific bridge ports related to the communication stream of 2 attached guests - without exposing the communication to other ports and other guests. The bridge may learn and update the relevant association of MAC addresses to bridge ports.
  • The virtual bridge device itself - in its role as an Ethernet device - does not work in promiscuous mode. However, packets arriving through one of its ports for (yet) unknown addresses may be flooded to all ports.
  • You cannot bridge a Linux bridge directly by or with another Linux bridge (no Linux bridge cascading). You can neither connect a Linux bride to another Linux bridge via a "tap" device.

Mar 24, 2019

[QUIC] quick skim over protocol QUIC

So, in order to have HTTP/2 supports multiplexing by using single TCP connection, we need a way to differentiate different request/response pair through single TCP connection, thus we introduce stream ID as the key mapping to request/response.


TCP has it's own sequence number to construct the packets in order on the receiving side.


Thus, the upper HTTP layer will receive the ordered TCP response thus base on stream ID can it builds up the response/request pair.


When the HTTP/2 TCP connection is established,
the client and server exchange SETTINGS frames first,
(like TCP's MSS setting option during init. SYN handshake, can't be changed once connection is established)
which indicates how many streams can be open at a time(or how many parallel requests), how many bytes server is ready to receive for a stream and for the whole connection, and the rate at which data can be delivered or received i.e., window size.


As you can spot there's a performance issue here.
TCP also times-out if missing a package(receiving side) during transfer which triggers sender side to re-send.
(triple ACK, slow start and fast recovery, sliding window/receiving side and congestion window/sender side)


Like VxLAN, UDP comes to the rescue.
(Since it's unreliable protocol, send and forget)
QUIC also based on UDP.


Since UDP is unreliable, QUIC can build it's own flow-control/reliability base on UDP.

QUIC also uses TLS 1.3 to protect data and transport layer
(i.e protocol headers)


Drawbacks:
Since QUIC is deployed in user space, performance is a big issue(for now).
No DMA for NIC like TCP and other optimization tricks done for TCP.


Reference:
QUIC:
https://http3-explained.haxx.se/en/
https://www.fastly.com/blog/why-fastly-loves-quic-http3

QUIC Downsides discussion:
https://news.ycombinator.com/item?id=19461777

Kernel TLS:
https://blog.filippo.io/playing-with-kernel-tls-in-linux-4-13-and-go/

TCP LSO:
https://en.wikipedia.org/wiki/Large_send_offload

HTTP Head-of-line blocking:
https://en.wikipedia.org/wiki/Head-of-line_blocking

HTTP pipelining:
https://en.wikipedia.org/wiki/HTTP_pipelining

HTTP/2 multiplexing:
https://stackoverflow.com/a/36519379
http://qnimate.com/what-is-multiplexing-in-http2/

Jan 11, 2016

[network] masking

Host IP + mask = result_1
Outgoing IP + mask = result_2

if result_1 != result_2
  send to IP routing process.
else
  send ARP obtain layer 2 MAC address.

Mar 2, 2012

[network] Books reading


TCP/IP Illustrated, Volume 1: The Protocols (2nd Edition)

Kevin R. Fall did a great job in updating this classic book.
The 2nd edition not only included IPv6, but updated lots of details in TCP congestion control, alone with ICMP infos. It also gives more details than the 1st edition and make this classic more clear than ever.

Unix Network Programming, Volume 1: The Sockets Networking API (3rd Edition)

THE BEST book for network programming.
Although there are many chapters talking about SCTP which I don't see else where using it(I am not encountering any, though), the book is GREAT in details.