Эксплуатация высокопроизводительных шлюзов VPN, обслуживающих десятки тысяч параллельных клиентских туннелей, неизбежно сталкивается с узкими местами сетевого стека Linux. Даже на современных многопроцессорных серверах с 64–128 аппаратными потоками потеря сетевых пакетов часто начинается задолго до насыщения физической пропускной способности каналов. В системах с высокой плотностью сессий первопричиной деградации является не криптографическая нагрузка или пропускная способность памяти, а встроенная подсистема отслеживания соединений Netfilter (nf_conntrack).

Изначально спроектированная для межсетевых экранов с контролем состояния, nf_conntrack ведет учет каждого двунаправленного потока пакетов, проходящего через ядро. В VPN-концентраторах, где миллионы короткоживущих потоков UDP и TCP непрерывно сменяют друг друга, такой подход приводит к жесткой конкуренции за блокировки (spinlocks), переполнению памяти ядра и потере трафика.

Проблема Conntrack: конкуренция за спинлоки и исчерпание таблиц

При поступлении каждого IP-пакета подсистема Netfilter вычисляет хэш 5-кортежа (IP отправителя, IP получателя, порт отправителя, порт получателя, транспортный протокол) для поиска или создания объекта struct nf_conn в глобальной хэш-таблице:

[Стандартный путь входящих пакетов с Conntrack]
Входящий пакет ──> Хук PRE_ROUTING ──> nf_conntrack_in()
                                            │
     Захват блокировки корзины (nf_conntrack_locks) ◄── [Конфликт между 64+ ядрами CPU]
                                            │
               Поиск / Выделение struct nf_conn (Кэш слябов: kmalloc-512)
                                            │
         Освобождение блокировки ──> Маршрутизация / Пересылка ──> Egress

При масштабных нагрузках в этой схеме возникают критические сбои:

  1. Переполнение таблицы и сброс пакетов: Максимальный размер таблицы ограничен значением net.netfilter.nf_conntrack_max. Когда количество сессий превышает этот лимит, ядро выдает ошибку nf_conntrack: table full, dropping packet. Входящие пакеты TCP SYN и инициации рукопожатий WireGuard отбрасываются, нарушая стабильность клиентских соединений.
  2. Конкуренция за атомарные спинлоки: Хэш-таблица защищена массивом спинлоков (nf_conntrack_locks). Когда многоочередные сетевые карты распределяют трафик между десятками ядер процессора через механизм RSS, независимые потоки ядра одновременно пытаются захватить блокировку одной и той же корзины хэша. Это приводит к постоянному сбросу кэш-линий между процессорными сокетами и росту нагрузки на softirq до 100%.
  3. Фрагментация памяти слябов: Каждое отслеживаемое соединение занимает около 320–384 байт невыгружаемой памяти ядра (kmalloc-512). Сопровождение 3 миллионов активных записей требует более 1,2 ГБ оперативной памяти ядра, создавая нагрузку на таблицы страниц и буферы TLB.

Устранение накладных расходов: декларативные правила NOTRACK

Наиболее эффективный способ защитить шлюзы VPN от переполнения conntrack — исключить пакеты инкапсулированных туннелей из проверки состояния.

С помощью таблицы raw, которая обрабатывает трафик до запуска подсистемы conntrack на хуках PREROUTING и OUTPUT, сетевой стек освобождается от выделения ресурсов для транзитных сессий:

# Конфигурация nftables для отключения conntrack для трафика WireGuard
nft add table inet raw
nft add chain inet raw prerouting { type filter hook prerouting priority -300 \; }
nft add chain inet raw output { type filter hook output priority -300 \; }

# Отметка входящего и исходящего порта UDP 51820 правилом notrack
nft add rule inet raw prerouting udp dport 51820 notrack
nft add rule inet raw output udp sport 51820 notrack

В синтаксисе iptables применяется аналогичная конфигурация:

iptables -t raw -A PREROUTING -p udp --dport 51820 -j NOTRACK
iptables -t raw -A OUTPUT -p udp --sport 51820 -j NOTRACK

Применение NOTRACK снижает потребление процессорных тактов на пакет до 28% на внешнем транспортном уровне UDP. При этом фильтрация трафика возлагается на бездатчиковую проверку списков контроля доступа или специализированные модули eBPF.

Безблокировочный учет потоков в ядре с помощью eBPF

Для интеллектуальной маршрутизации, ограничения скорости и контроля сессий без снижения масштабируемости современные архитектуры переносят таблицы состояний в eBPF с использованием карт BPF_MAP_TYPE_LRU_HASH.

В отличие от глобальных блокировок Netfilter, хэш-таблицы eBPF LRU (Least Recently Used) автоматически освобождают память без блокировок. При достижении предельной емкости карты неактивные старые записи вытесняются новыми сессиями без отбрасывания входящих пакетов.

Ниже представлен классификатор eBPF, подключенный к хуку Traffic Control (clsact), фиксирующий метрики сессий:

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

struct flow_key {
    __u32 src_ip;
    __u32 dst_ip;
    __u16 src_port;
    __u16 dst_port;
    __u8  protocol;
};

struct flow_metrics {
    __u64 packets;
    __u64 bytes;
    __u64 last_seen;
};

/* Безблокировочная LRU-карта для масштабируемого учета потоков */
struct {
    __uint(type, BPF_MAP_TYPE_LRU_HASH);
    __uint(max_entries, 2097152); /* 2 миллиона сессий */
    __type(key, struct flow_key);
    __type(value, struct flow_metrics);
} vpn_flow_table SEC(".maps");

SEC("tc")
int track_vpn_flows(struct __sk_buff *skb)
{
    void *data = (void *)(long)skb->data;
    void *data_end = (void *)(long)skb->data_end;

    /* Проверка границ заголовка Ethernet */
    if (data + sizeof(struct ethhdr) > data_end)
        return TC_ACT_OK;

    struct ethhdr *eth = data;
    if (eth->h_proto != __constant_htons(ETH_P_IP))
        return TC_ACT_OK;

    struct iphdr *iph = data + sizeof(struct ethhdr);
    if ((void *)(iph + 1) > data_end || iph->protocol != IPPROTO_UDP)
        return TC_ACT_OK;

    struct udphdr *udph = (void *)iph + (iph->ihl * 4);
    if ((void *)(udph + 1) > data_end)
        return TC_ACT_OK;

    /* Фильтрация портов службы VPN */
    if (udph->dest == __constant_htons(51820) || udph->source == __constant_htons(51820)) {
        struct flow_key key = {
            .src_ip = iph->saddr,
            .dst_ip = iph->daddr,
            .src_port = udph->source,
            .dst_port = udph->dest,
            .protocol = iph->protocol
        };

        struct flow_metrics *val = bpf_map_lookup_elem(&vpn_flow_table, &key);
        if (val) {
            __sync_fetch_and_add(&val->packets, 1);
            __sync_fetch_and_add(&val->bytes, skb->len);
            val->last_seen = bpf_ktime_get_ns();
        } else {
            struct flow_metrics init = {
                .packets = 1,
                .bytes = skb->len,
                .last_seen = bpf_ktime_get_ns()
            };
            bpf_map_update_elem(&vpn_flow_table, &key, &init, BPF_NOEXIST);
        }
    }

    return TC_ACT_OK;
}

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

Благодаря тому, что вызов bpf_map_lookup_elem в картах LRU выполняется с семантикой RCU без использования взаимных блокировок, процессорные ядра обновляют счетчики параллельно без задержек на межъядерную синхронизацию.

Сравнительные показатели эффективности

Тестирование на двухпроцессорном сервере с чипами AMD EPYC 9654 (192 физических ядра) при нагрузке в 5 миллионов параллельных сессий UDP демонстрирует кардинальное преимущество архитектуры eBPF:

Параметр эффективности Стандартный nf_conntrack Raw NOTRACK + eBPF LRU
Максимум параллельных сессий 1 048 576 (Жесткий лимит) 4 000 000+ (Динамический LRU)
Пропускная способность (Mpps) 1,8 Mpps (Упор в спинлоки) 21,4 Mpps (Скорость линии)
Нагрузка на CPU по softirq 82% Насыщение ядер 7% Штатная работа
Расход памяти ядра ~1,4 ГБ Слябовый кэш ~256 МБ Предвыделенная карта
Поведение при переполнении Сброс пакетов (Сбой) Плавное вытеснение старых записей

Практическая реализация в Anycast-узлах RiceGuard

Платформа RiceGuard применяет архитектуру полной разгрузки отслеживания соединений в распределенных Anycast-кластерах:

  • Устойчивость к исчерпанию ресурсов: Распределенные DDoS-атаки часто подменяют случайные UDP-порты для переполнения таблиц conntrack. Шлюзы RiceGuard обрабатывают входящие дейтаграммы без сохранения состояния через eBPF до криптографической аутентификации Noise, сводя к нулю угрозу отказа в обслуживании.
  • Микросекундное время пересылки: Исключение тяжелых хуков Netfilter и спинлоков удерживает время обработки пакетов в многогигабитных туннелях на уровне менее 1,5 микросекунды.
  • Сохранение стандартных инструментов Linux: Сетевая подсистема ядра и маршрутизаторы BGP Anycast продолжают функционировать в штатном режиме, обеспечивая прозрачную телеметрию без падения скорости передачи.

Отказ от устаревшего механизма conntrack в пользу декларативных правил NOTRACK и безблокировочных карт eBPF обеспечивает линейную масштабируемость и стабильную передачу данных на физической скорости канала.