Забезпечення багатогігабітної пропускної здатності у користувацьких 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 і прямує до стандартного мережевого стека ядра.

Будова пулу UMEM та чотири кільцеві черги без блокувань

Основою функціонування AF_XDP є UMEM — безперервна область віртуальної пам'яті, зареєстрована у ядрі викликом setsockopt(..., SOL_XDP, XDP_UMEM_REG, ...). Вона ділиться на кадри фіксованого розміру (зазвичай 2048 або 4096 байтів).

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

  1. Кільце заповнення (Fill Ring, Користувач -> Ядро): Застосунок передає адреси вільних кадрів UMEM драйверу карти для підготовки буферів під нові пакети.
  2. Кільце прийому (Rx Ring, Ядро -> Користувач): Під час надходження пакетів драйвер записує дескриптори кадрів (зміщення пам'яті та довжину пакета) у кільце прийому для обробки рушієм VPN.
  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;
        
    /* Спрямування порту UDP 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.