Експлуатація високопродуктивних шлюзів VPN, що обслуговують десятки тисяч паралельних клієнтських тунелів, висуває високі вимоги до мережевого стека Linux. Навіть на сучасних багатопроцесорних серверах із 64–128 апаратними потоками втрата мережевих пакетів часто починається задовго до вичерпання фізичної пропускної здатності інтерфейсів. У системах із високою щільністю сесій головною причиною деградації є не криптографічне шифрування чи шина оперативної пам'яті, а підсистема відстеження з'єднань Netfilter (nf_conntrack).

Спроектована для міжмережевих екранів із контролем стану, nf_conntrack зберігає глобальний стан для кожного двонаправленого потоку пакетів, що проходить через ядро. У концентраторах VPN, де мільйони короткоживучих сесій UDP та TCP постійно відкриваються та закриваються щохвилини, цей механізм спричиняє жорстку конкуренцію за блокування (spinlocks), вичерпання виділеної пам'яті ядра та втрату трафіку.

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

Під час обробки кожного вхідного IP-пакета Netfilter обчислює хеш кортежу з 5 параметрів (IP відправника, IP одержувача, порт відправника, порт одержувача, протокол L4) для пошуку або створення структури 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

Найбільш дієвий спосіб запобігти переповненню conntrack у шлюзах VPN — виключити тунельні пакети з процедури перевірки стану.

Завдяки таблиці 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 (Швидкість каналу)
Навантаження softirq на ядра 82% Насичення ядер 7% Штатне завантаження
Використання пам'яті ядра ~1,4 ГБ Слябовий пул ~256 МБ Фіксована карта
Поведінка під час переповнення Скидання пакетів (Збій) Плавне витіснення застарілих даних

Виробниче впровадження у шлюзах RiceGuard Anycast

RiceGuard повністю виключає відстеження з'єднань Netfilter у глобальних Anycast-кластерах:

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

Заміна механізму conntrack декларативними правилами NOTRACK та безблокувальними картами eBPF забезпечує лінійне масштабування та роботу на повній фізичній швидкості інтерфейсів.