Експлуатація високопродуктивних корпоративних VPN-концентраторів та граничних мереж Anycast вимагає надійного прийому й розподілу мережевого трафіку через тисячі публічних IP-адрес. Коли мережевий провайдер виділяє спеціальні блоки IPv4 (діапазони /24 або ширші) та префікси IPv6 (/48) для різних клієнтських середовищ, спрямування вхідних пакетів до відповідного фонового процесу в просторі користувача стає критичним вузьким місцем.

Історично системні інженери Linux використовували два традиційні підходи: безпосереднє прив'язування тисяч сокетів до виділених IP-адрес на локальних інтерфейсах або організацію прозорого проксі за допомогою цілі Netfilter TPROXY разом із параметром IP_TRANSPARENT. Обидві архітектури мають відчутні обмеження. Одночасне утримання тисяч окремих сокетів виснажує оперативну пам'ять ядра, роздуває таблиці хешування та уповільнює обробку з'єднань. З іншого боку, конфігурація TPROXY змушує кожен пакет проходити перевірку станів у підсистемі Netfilter (nf_conntrack), що спричиняє конкуренцію блокувань процесора та зниження продуктивності на високошвидкісних лініях зв'язку.

Впровадження типу програм BPF_PROG_TYPE_SK_LOOKUP у ядрі Linux 5.9 повністю вирішує цю проблему. Завдяки розміщенню програмованої логіки eBPF безпосередньо в точці вибору сокетів транспортного рівня, інженери можуть динамічно спрямовувати довільні діапазони IP-адрес і портів до потрібного сокета, уникаючи використання громіздких хеш-таблиць і позбавляючись накладних витрат Netfilter.

Порівняння архітектур маршрутизації: TPROXY проти eBPF sk_lookup:

Традиційна схема TPROXY:
Мережева карта ──> IP-рівень ──> Netfilter PREROUTING ──> nf_conntrack ──> iptables TPROXY ──> inet_lookup() ──> Сокет
                                 (Перевантаження таблиці станів та високе завантаження CPU)

Оптимізована схема eBPF sk_lookup:
Мережева карта ──> IP-рівень ──> Точка виклику L4 ──> eBPF sk_lookup ──> bpf_sk_assign() ──> Цільовий сокет
                                 (Без участі Netfilter, динамічний вибір за сталий час O(1))

Обмеження статичного прив'язування сокетів та TPROXY

Щоб з'ясувати практичну важливість динамічного вибору сокетів, розглянемо механізм пошуку сокета ядром Linux під час надходження пакета TCP SYN або датаграми UDP. У класичному мережевому стеку функція __inet_lookup_listener() послідовно аналізує комірки глобальної таблиці хешування відкритих сокетів (listening_hash):

  1. Ядро розраховує ключ хешування на основі локального порту та простору імен мережі.
  2. Система послідовно перевіряє зв'язаний список сокетів, зіставляючи локальну IP-адресу або шукаючи універсальну прив'язку (INADDR_ANY).
  3. Сокети, прив'язані до конкретних адрес, перевіряються першими, що вимагає лінійного сканування всієї комірки.

Якщо VPN-шлюз опрацьовує префікс /20, що налічує 4096 публічних IP-адрес на різних портах, призначення кожної адреси окремому сокету перевантажує listening_hash. Зростання глибини списків викликає промахи кешу процесора, а перезапуск мережевої служби триває хвилинами через повільне вивільнення й повторну реєстрацію тисяч з'єднань.

Для подолання цих складнощів адміністратори часто обирали конфігурацію TPROXY:

# Традиційне налаштування TPROXY для прозорого перехоплення
iptables -t mangle -A PREROUTING -p tcp -d 198.51.100.0/24 --dport 443 -j TPROXY \
  --on-port 8443 --on-ip 127.0.0.1 --tproxy-mark 0x1/0x1

Хоча TPROXY дає можливість спрямовувати трафік невиділених адрес на єдиний локальний сокет, системна ціна такого рішення висока. Кожен пакет має пройти фільтри Netfilter, створити запис у nf_conntrack та виконати перевірку в таблицях правил маршрутизації (ip rule). Під час потужних хвиль SYN flood або за наявності понад 500 000 активних з'єднань сервери з TPROXY вичерпують ліміти оперативної пам'яті й втрачають трафік.

Внутрішня будова BPF_PROG_TYPE_SK_LOOKUP

Ядро Linux усуває цю суперечність шляхом відокремлення цільової IP-адреси пакета від ідентифікації сокета. Програми типу sk_lookup вбудовуються безпосередньо в базові механізми вибору сокетів транспортного рівня: inet_lookup_run_bpf() для протоколу IPv4 та inet6_lookup_run_bpf() для IPv6.

Коли вхідний пакет потрапляє до функцій tcp_v4_rcv() чи udp_rcv() і починається процедура пошуку сокета, ядро активує приєднану програму sk_lookup до перегляду внутрішніх таблиць. Програма отримує компактну структуру контексту:

struct bpf_sk_lookup {
    union {
        __bpf_md_ptr(struct bpf_sock *, sk);
    };
    __u32 family;
    __u32 protocol;
    __u32 remote_ip4;
    __u32 remote_ip6[4];
    __u32 local_ip4;
    __u32 local_ip6[4];
    __u32 remote_port;
    __u32 local_port;
    __u32 ingress_ifindex;
};

Цей контекст надає програмі eBPF прямий доступ для читання до параметрів 4-tuple, типу протоколу та індексу вхідного мережевого інтерфейсу. Замість статичних правил прив'язки програма eBPF перевіряє цільову IP-адресу й порт, звертається до структур даних у пам'яті (таких як карти префіксів LPM trie) і вибирає необхідний сокет.

Головною ланкою всього процесу є виклик функції bpf_sk_assign():

long bpf_sk_assign(struct bpf_sk_lookup *ctx, struct bpf_sock *sk, __u64 flags);

Щойно програма eBPF знаходить потрібний сокет у карті BPF_MAP_TYPE_SOCKMAP чи BPF_MAP_TYPE_SOCKHASH, вона викликає bpf_sk_assign(ctx, target_sk, 0). Ядро фіксує це посилання, припиняє стандартний пошук сокетів і негайно передає з'єднання обраному сокету. Якщо програма повертає значення BPF_PASS без призначення сокета, ядро повертається до звичайного алгоритму вибору.

Схема керування в ядрі через eBPF sk_lookup:

Вхідний пакет (TCP SYN / UDP)
         │
         ▼
  inet_lookup_run_bpf()
         │
   [Програма sk_lookup]
         │
         ├─► Аналіз контексту (local_ip, local_port, ifindex)
         ├─► Пошук у картах eBPF (LPM trie, маршрутні таблиці)
         ├─► Визначення сокета (з SOCKMAP)
         │
         ▼
  bpf_sk_assign(ctx, target_sk, 0)
         │
         ├─► Успіх: Пряме повернення сокета (без перевірки listening_hash)
         └─► Резерв (BPF_PASS): Стандартний пошук у хеш-таблицях

Оскільки виклик bpf_sk_assign() повністю виконується в оперативній пам'яті на транспортному рівні, система не створює записів у Netfilter і не потребує конфігурації віртуальних IP-адрес на інтерфейсах.

Побудова маршрутизатора сокетів за допомогою eBPF

Реалізація надійного диспетчера сокетів складається з програми eBPF мовою C та керуючого сервісу в просторі користувача. Програма контролює мережеві потоки, спрямовані до відповідної підмережі, і передає їх до робочих процесів VPN.

Наведений фрагмент коду спрямовує пакети цілої підмережі /24 (198.51.100.0/24) до виділеного процесу VPN:

#include <linux/bpf.h>
#include <bpf/bpf_helpers.h>
#include <bpf/bpf_endian.h>

/* Карта сокетів, керована користувацьким процесом */
struct {
    __uint(type, BPF_MAP_TYPE_SOCKMAP);
    __uint(max_entries, 8);
    __type(key, __u32);
    __type(value, __u64);
} vpn_sockets SEC(".maps");

SEC("sk_lookup")
int vpn_sk_lookup_handler(struct bpf_sk_lookup *ctx)
{
    /* Обробка виключно трафіку IPv4 */
    if (ctx->family != 2) /* AF_INET */
        return BPF_PASS;

    __u32 dst_ip = bpf_ntohl(ctx->local_ip4);
    __u32 port = ctx->local_port;

    /* Зіставлення з діапазоном: 198.51.100.0/24 (0xC6336400) */
    if ((dst_ip & 0xFFFFFF00) == 0xC6336400) {
        __u32 sock_key = 0; /* Основний сокет обробки VPN */

        /* Порт 443 спрямовується на веб-реле, інші порти — на VPN Core */
        if (port == 443)
            sock_key = 1;

        struct bpf_sock *sk = bpf_map_lookup_elem(&vpn_sockets, &sock_key);
        if (sk) {
            long err = bpf_sk_assign(ctx, sk, 0);
            bpf_sk_release(sk);
            return err ? BPF_DROP : BPF_OK;
        }
    }

    return BPF_PASS;
}

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

У просторі користувача керуючий процес відкриває сокети на локальних портах, зберігає їхні дескриптори в карті vpn_sockets і прив'язує програму eBPF до простору імен мережі:

# Завантаження та закріплення програми sk_lookup у просторі імен мережі
bpftool prog load vpn_sk_lookup.o /sys/fs/bpf/vpn_sk_lookup type sk_lookup
bpftool prog attach pinned /sys/fs/bpf/vpn_sk_lookup netns /proc/self/ns/net

Після активації кожен пакет, адресований будь-якій із 256 адрес діапазону 198.51.100.0/24, миттєво потрапляє до зареєстрованого сокета без ручного налаштування IP-аліасів чи правил міжмережевого екрана.

Тестування продуктивності та оптимізація пам'яті ядра

Переваги переходу від моделі TPROXY до архітектури sk_lookup підтверджуються результатами випробувань. Стрес-тести на апаратному забезпеченні 100 GbE фіксують значне зростання пропускної здатності та зниження затримок:

Порівняння швидкості встановлення сесій та навантаження CPU:
────────────────────────────────────────────────────────────────
Параметр тесту             Сокет на кожну IP TPROXY        eBPF sk_lookup
────────────────────────────────────────────────────────────────
Затримка рукостискання (P99) 4.82 мс          3.91 мс       1.14 мс
Швидкість обробки з'єднань 185K з'єдн/сек    240K з'єдн/с  680K з'єдн/с
Використання пам'яті RAM   480 МБ            310 МБ        12 МБ
Завантаження ядер CPU      74% (блокування)  82% (conntrack) 26% (чистий L4)
────────────────────────────────────────────────────────────────

Завдяки алгоритмічній складності вибору сокета $O(1)$, час встановлення з'єднань залишається незмінним незалежно від того, чи керує шлюз 10 адресами, чи одним мільйоном IP-адрес.

Для забезпечення максимальної стабільності за умов високого навантаження рекомендується скоригувати параметри черг ядра у файлі /etc/sysctl.d/99-network-performance.conf:

# Максимальна глибина черги прослуховування відкритих сокетів
net.core.somaxconn = 65535

# Ліміт черги вхідних пакетів мережевої підсистеми
net.core.netdev_max_backlog = 250000

# Максимальна місткість черги напіввідкритих сесій TCP SYN
net.ipv4.tcp_max_syn_backlog = 65535

Ці налаштування гарантують стабільну роботу мережевих черг під час швидкісного розподілу потоків через eBPF.

Застосування в корпоративній архітектурі VPN

Окрім високої швидкості обробки, механізм BPF_PROG_TYPE_SK_LOOKUP ефективно розв'язує два критичні практичні завдання:

  1. Безшовне оновлення сервісів (Zero-Downtime Reload): Під час розгортання нової версії демона VPN новий процес запускається, створює сокети та додає їх у додатковий слот карти vpn_sockets. Атомарне оновлення карти eBPF миттєво перенаправляє нові запити на оновлений сервіс, тоді як попередня версія завершує роботу з наявними сесіями без скидання трафіку.
  2. Програмне розділення користувачів: Оператори мереж можуть сегментувати клієнтські потоки виключно програмним способом. Пакети, які надходять на спільні порти, розподіляються між ізольованими процесами на основі репутації IP джерела чи динамічних політик доступу.

VPNRice впроваджує цей сучасний підхід у своїй платформі RiceGuard. Поєднання гнучкого розподілу сокетів через eBPF, надійного шифрування ChaCha20-Poly1305 та автоматизованого захисту від витоків DNS дозволяє RiceGuard надавати корпоративні канали зв'язку на повній швидкості середовища передачі. Така архітектура усуває потребу в складних наборах правил фільтрації та гарантує приватність користувачів у хмарних інфраструктурах.

Для мережевих архітекторів, які розробляють сучасні VPN-шлюзи, прозорі проксі чи вузли Anycast-маршрутизації, BPF_PROG_TYPE_SK_LOOKUP є надійним стандартом побудови мереж. Заміна об'ємних таблиць Netfilter компактною програмою eBPF забезпечує стійкість, мінімальні затримки та відмінну масштабованість інфраструктури.