Обеспечение многогигабитной пропускной способности в пользовательских VPN-шлюзах требует пересмотра архитектуры передачи пакетов между физическими сетевыми картами (NIC) и средой выполнения приложения. Традиционные программные туннели, созданные на Go, Rust или C, взаимодействуют с операционной системой через виртуальные интерфейсы TUN/TAP (/dev/net/tun). Хотя устройства TUN гарантируют широкую совместимость, они накладывают серьезные накладные расходы: каждый пакет вызывает переключение контекста между ядром и пространством пользователя, выделение метаданных struct sk_buff и физическое копирование памяти через copy_to_user().

На скоростях 10 Гбит/с и 40 Гбит/с эти системные издержки перегружают шину памяти и вызывают непрерывный сброс процессорных кэшей, ограничивая производительность одного процессорного ядра планкой примерно в 1,5 миллиона пакетов в секунду (Mpps). Комплект DPDK решает эту проблему за счет полного захвата оборудования, однако при этом сетевой адаптер полностью исчезает из операционной системы, делая невозможным применение стандартных сетевых утилит Linux (iproute2, nftables, tcpdump) и работу других служб.

Интерфейс Address Family XDP (AF_XDP, также известный как XSK) устраняет этот барьер. Объединяя перенаправление пакетов eBPF на уровне сетевого драйвера с выделенными пулами памяти UMEM в пространстве пользователя, AF_XDP обеспечивает аппаратную передачу данных без копирования (zero-copy) прямо в приложение, полностью сохраняя экосистему ядра Linux.

Сравнение трактов обработки: TUN/TAP и AF_XDP

Для наглядности рассмотрим путь зашифрованного пакета VPN в обеих архитектурных моделях:

[Классический тракт TUN/TAP]
Очередь RX карты ──> alloc_skb() ──> Стек IP ядра / Netfilter ──> Драйвер TUN
                                                                       │
                Переключение контекста + Копирование (copy_to_user) <──┘
                                       │
                                       ▼
                       Приложение VPN (Дешифрование)

─────────────────────────────────────────────────────────────────────────

[Тракт Zero-Copy AF_XDP]
Очередь RX карты ──> Прямой доступ к памяти (DMA) ──> Буфер UMEM пользователя
                          │
             Фильтр XDP: bpf_redirect_map()
                          │
                          ▼ (Zero-Copy, передача дескрипторов без блокировок)
             Движок VPN (Дешифрование прямо в оперативной памяти)

В конвейере AF_XDP ядро не создает структуры struct sk_buff. Контроллер прямого доступа к памяти (DMA) сетевой карты записывает входящие кадры Ethernet непосредственно в заранее зарегистрированные блоки пользовательской памяти (UMEM). Программа eBPF, выполняемая в точке перехвата драйвера (BPF_PROG_TYPE_XDP), анализирует сетевые заголовки. Если пакет предназначен для VPN-шлюза (соответствует порту UDP или криптографической сигнатуре), программа вызывает bpf_redirect_map(&xsk_map, queue_id, 0). Весь остальной трафик получает статус XDP_PASS и передается штатному сетевому стеку Linux.

Архитектура памяти UMEM и четыре бесоблочных кольцевых буфера

Фундаментальной основой AF_XDP выступает UMEM — непрерывная область виртуальной памяти, регистрируемая в ядре с помощью системного вызова setsockopt(..., SOL_XDP, XDP_UMEM_REG, ...). Область UMEM разбивается на кадры фиксированного размера (как правило, 2048 или 4096 байт).

Взаимодействие между пространством пользователя и драйвером ядра организовано через четыре бесоблочных кольцевых буфера типа SPSC (один производитель — один потребитель):

  1. Кольцо заполнения (Fill Ring, Пользователь -> Ядро): Приложение передает адреса пустых блоков UMEM драйверу сетевой карты, подготавливая буферы для приема новых входящих пакетов.
  2. Кольцо приема (Rx Ring, Ядро -> Пользователь): При поступлении пакетов сетевой драйвер записывает дескрипторы кадров (смещение адреса и размер пакета) в кольцо приема для обработки приложением.
  3. Кольцо передачи (Tx Ring, Пользователь -> Ядро): При отправке зашифрованных пакетов приложение помещает дескрипторы заполненных кадров в очередь передачи для отправки в физический канал.
  4. Кольцо завершения (Completion Ring, Ядро -> Пользователь): После фактической отправки кадра в сеть драйвер возвращает адрес буфера в кольцо завершения, позволяя приложению повторно использовать освобожденную память.

Поскольку эти кольца функционируют через атомарные операции без мьютексов и спинлоков, процессорные ядра обрабатывают пакеты пакетами по 32 или 64 штуки с высочайшей эффективностью.

Реализация в ядре: Перенаправление XDP на уровне драйвера

Компонент ядра представляет собой легковесный классификатор eBPF, подключенный к сетевому интерфейсу:

#include <linux/bpf.h>
#include <linux/if_ether.h>
#include <linux/ip.h>
#include <linux/udp.h>
#include <bpf/bpf_helpers.h>

struct {
    __uint(type, BPF_MAP_TYPE_XSKMAP);
    __uint(max_entries, 64);
    __type(key, __u32);
    __type(value, __u32);
} qidconf_map SEC(".maps");

SEC("xdp")
int xdp_vpn_redirect(struct xdp_md *ctx)
{
    void *data = (void *)(long)ctx->data;
    void *data_end = (void *)(long)ctx->data_end;
    
    /* Проверка границ заголовка Ethernet */
    if (data + sizeof(struct ethhdr) > data_end)
        return XDP_PASS;
        
    struct ethhdr *eth = data;
    if (eth->h_proto != __constant_htons(ETH_P_IP))
        return XDP_PASS;
        
    struct iphdr *iph = data + sizeof(struct ethhdr);
    if ((void *)(iph + 1) > data_end || iph->protocol != IPPROTO_UDP)
        return XDP_PASS;
        
    struct udphdr *udph = (void *)iph + (iph->ihl * 4);
    if ((void *)(udph + 1) > data_end)
        return XDP_PASS;
        
    /* Перенаправление порта 51820 в сокет AF_XDP */
    if (udph->dest == __constant_htons(51820)) {
        return bpf_redirect_map(&qidconf_map, ctx->rx_queue_index, XDP_PASS);
    }
    
    return XDP_PASS;
}

char _license[] SEC("license") = "GPL";

Данный код отрабатывает за доли наносекунд в контексте прерывания драйвера. Пакеты VPN поступают напрямую в очереди пространства пользователя, в то время как системные соединения SSH и протоколы маршрутизации BGP продолжают работать без сбоев.

Инициализация сокета и опрос zero-copy

В пространстве пользователя инициализация сокета включает выравнивание памяти через posix_memalign(), настройку UMEM и привязку с флагом аппаратного zero-copy:

#include <xdp/xsk.h>
#include <stdlib.h>
#include <sys/socket.h>

#define NUM_FRAMES 4096
#define FRAME_SIZE 2048

struct xsk_umem_info {
    struct xsk_ring_prod fq;
    struct xsk_ring_cons cq;
    struct xsk_umem *umem;
    void *buffer;
};

int init_af_xdp_socket(struct xsk_umem_info *umem_info, const char *ifname, int queue_id)
{
    struct xsk_socket_config xsk_cfg;
    struct xsk_umem_config umem_cfg = {
        .fill_size = NUM_FRAMES,
        .comp_size = NUM_FRAMES,
        .frame_size = FRAME_SIZE,
        .frame_headroom = XDP_PACKET_HEADROOM,
        .flags = 0
    };
    
    /* Выделение выровненного непрерывного блока памяти */
    posix_memalign(&umem_info->buffer, getpagesize(), NUM_FRAMES * FRAME_SIZE);
    
    /* Регистрация пула UMEM в ядре Linux */
    xsk_umem__create(&umem_info->umem, umem_info->buffer, 
                     NUM_FRAMES * FRAME_SIZE, &umem_info->fq, 
                     &umem_info->cq, &umem_cfg);
                     
    xsk_cfg.rx_size = NUM_FRAMES;
    xsk_cfg.tx_size = NUM_FRAMES;
    xsk_cfg.libbpf_flags = 0;
    xsk_cfg.xdp_flags = XDP_FLAGS_UPDATE_IF_NOEXIST | XDP_FLAGS_DRV_MODE;
    xsk_cfg.bind_flags = XDP_ZEROCOPY | XDP_USE_NEED_WAKEUP;
    
    return 0;
}

Флаг XDP_USE_NEED_WAKEUP снижает число лишних системных вызовов: ядро активирует опрос драйвера только тогда, когда приложение явно сообщает о необходимости пополнения очереди.

Практика развертывания в сети RiceGuard Anycast

RiceGuard внедряет технологию AF_XDP на узлах Anycast для сохранения устойчивой производительности в условиях экстремальной плотности сессий:

  • Высокая плотность обработки: Узлы RiceGuard обрабатывают свыше 24 миллионов пакетов в секунду (Mpps) на двухсокетный сервер, на порядок превосходя традиционные интерфейсы TUN.
  • Микросекундный отклик: Исключение блокировок сокетов ядра и копирования данных удерживает задержку обработки на уровне менее 1,2 микросекунды.
  • Сохранение стандартной сетевой модели: Сетевой интерфейс остается полноправным устройством Linux, что позволяет инструментам мониторинга и сервисам маршрутизации BGP работать параллельно с оптимизированным конвейером VPN.

Использование AF_XDP и технологии UMEM позволяет архитектурам WireGuard и RiceGuard достигать физического предела скорости передачи данных при сохранении удобства управления системой Linux.