Масштабування багатокористувацьких шлюзів VPN до швидкостей у десятки гігабіт на секунду вимагає ефективного розподілу мільйонів зашифрованих датаграм UDP між десятками процесорних ядер. У класичній мережевій архітектурі робочі потоки, прив'язані до єдиного порту прослуховування, конкурують за пакети через централізовану чергу сокета, що створює блокування (spinlocks) і значно знижує продуктивність паралельної обробки.
Хоча системна опція Linux SO_REUSEPORT дозволяє кільком незалежним сокетам одночасно прив'язуватися до однакової комбінації IP-адреси та порту, стандартний механізм ядра використовує фіксоване обчислення 4-компонентного хешу (IP джерела, порт джерела, IP призначення, порт призначення). У середовищах шифрованого трафіку цей статичний підхід створює критичні технічні проблеми: порушується неперервність сесій під час перемикання мереж мобільними пристроями, виникає неефективний доступ до пам'яті між вузлами NUMA (Non-Uniform Memory Access) та втрачається локальність кешу L3 між процесорами.
Для подолання цих бар'єрів мережеві інженери поєднують SO_REUSEPORT із класифікаторами сокетів eBPF (BPF_PROG_TYPE_SK_REUSEPORT). Виконуючи логіку програмованої маршрутизації безпосередньо під час пошуку сокета в ядрі, шлюзи досягають стабільної прив'язки сесій, розподілу навантаження без блокувань та суворої локалізації пам'яті в межах вузлів NUMA.
Обмеження масштабування стандартного хешування SO_REUSEPORT
У стандартному ядрі Linux вхідні датаграми UDP, що відповідають групі SO_REUSEPORT, розподіляються між сокетами за алгоритмом reciprocal_scale(skb_get_hash(skb), num_sockets). Хоча цей механізм добре функціонує для звичайного веб-трафіку, для тунелів VPN він спричиняє три істотні ускладнення:
Вхідна датаграма UDP
│
▼
┌─────────────────────────────────┐
│ Хешування ядра за 4 полями │
│ (src_ip, src_port, dst_ip, dst) │
└──────────────┬──────────────────┘
│
├─────────────────┬─────────────────┐
▼ ▼ ▼
┌──────────────┐ ┌──────────────┐ ┌──────────────┐
│ Сокет 0 │ │ Сокет 1 │ │ Сокет 2 │
│ Ядро 0 (NUMA0)│ │ Ядро 4 (NUMA0)│ │ Ядро 8 (NUMA1)│
└──────────────┘ └──────────────┘ └──────┬───────┘
│ <--- Затримка шини UPI між вузлами
▼ (Скидання ліній кешу L3)
Віддалений кеш L3
- Роумінг клієнтів і зміна портів NAT: Мобільні пристрої VPN регулярно змінюють мережеве підключення (перемикаючись між Wi-Fi та стільниковою мережею) або стикаються зі зміною портів на боці операторського NAT. При зміні IP або порту 4-компонентний хеш миттєво змінюється, спрямовуючи пакети активної сесії на інший робочий потік. Новий потік не містить сесійних ключів у локальному кеші, що викликає блокування для синхронізації або вимушене повторне встановлення з'єднання.
- Затримка міжпроцесорної пам'яті NUMA: На двосокетних серверах мережевий адаптер PCIe підключений до конкретного роз'єму процесора (вузол NUMA 0). Коли стандартний хеш ядра призначає пакет потоку на вузлі NUMA 1, кожна транзакція з пам'яттю проходить через міжпроцесорну шину Ultra Path Interconnect (UPI). Це збільшує затримку обробки пакета з 800 наносекунд до понад 3,2 мікросекунди.
- Нерівномірний розподіл трафіку: Хешування має суто ймовірнісний характер. Коли кілька високошвидкісних потоків випадково призначаються на одне ядро, воно перевантажується на 100%, спричиняючи втрату пакетів, тоді як сусідні ядра залишаються вільними.
Архітектура програмованої диспетчеризації: BPF_PROG_TYPE_SK_REUSEPORT
Програми BPF_PROG_TYPE_SK_REUSEPORT разом із картою BPF_MAP_TYPE_REUSEPORT_SOCKARRAY замінюють статичну функцію хешування ядра гнучкими правилами маршрутизації.
Вхідна зашифрована датаграма
│
▼
┌────────────────────────────────────────────────────────┐
│ Класифікатор eBPF: BPF_PROG_TYPE_SK_REUSEPORT │
│ - Аналізує корисне навантаження UDP (індекс сесії) │
│ - Зчитує smp_processor_id() для прив'язки до NUMA │
│ - Викликає допоміжну функцію bpf_sk_select_reuseport()│
└──────────────────────────┬─────────────────────────────┘
│
▼
┌──────────────────────────────────┐
│ BPF_MAP_TYPE_REUSEPORT_SOCKARRAY │
└────────────────┬─────────────────┘
│
┌───────────────────┴───────────────────┐
▼ ▼
┌──────────────┐ ┌──────────────┐
│ Робоче ядро 0│ (Локальний вузол NUMA 0)│ Робоче ядро 1│ (Локальний вузол NUMA 0)
│ Дескриптор 0 │ │ Дескриптор 1 │
└──────────────┘ └──────────────┘
Коли вхідний пакет потрапляє на етап пошуку сокетів UDP, ядро викликає програму eBPF перед додаванням пакета до будь-якої черги. Програма опрацьовує структуру struct sk_reuseport_md:
skb->dataіskb->data_end: Прямі вказівники на байти заголовків і корисного навантаження пакета.sk: Сокет-кандидат на отримання.ip_protocol: Протокол транспортного рівня (UDP або TCP).bind_inany: Прапорець режиму прив'язки сокета.
Викликаючи допоміжну функцію ядра bpf_sk_select_reuseport(md, &reuseport_map, &key, flags), код eBPF безпосередньо визначає індекс сокета в масиві карти для доставки пакета.
Реалізація в ядрі: Маршрутизація за індексом одержувача WireGuard
У протоколі WireGuard кожен зашифрований пакет даних містить 32-бітне поле receiver_index зі зміщенням 4 байти в корисному навантаженні UDP. Це значення унікально ідентифікує активну сесію піра.
Наведена програма eBPF демонструє вилучення ідентифікатора сесії та вибір процесорного ядра з прив'язкою до локального вузла NUMA:
#include <linux/bpf.h>
#include <linux/filter.h>
#include <bpf/bpf_helpers.h>
#define WG_DATA_PACKET_TYPE 4
#define NUMA_NODE_CORES 16
struct {
__uint(type, BPF_MAP_TYPE_REUSEPORT_SOCKARRAY);
__uint(max_entries, 64);
__type(key, __u32);
__type(value, __u32);
} reuseport_sock_map SEC(".maps");
SEC("sk_reuseport")
int select_vpn_socket(struct sk_reuseport_md *ctx)
{
void *data = ctx->data;
void *data_end = ctx->data_end;
/* Перевірка меж пам'яті: мінімальний розмір навантаження UDP */
if (data + 8 > data_end)
return SK_PASS;
__u8 packet_type = *(__u8 *)data;
__u32 target_socket_index = 0;
if (packet_type == WG_DATA_PACKET_TYPE) {
/* Отримання 32-бітного receiver_index зі зміщенням 4 байти */
__u32 receiver_index = *(__u32 *)(data + 4);
/* Збереження прив'язки сесії до пулу потоків */
target_socket_index = receiver_index % NUMA_NODE_CORES;
} else {
/* Пакети рукостискання направляються поточному ядру */
__u32 current_cpu = bpf_get_smp_processor_id();
target_socket_index = current_cpu % NUMA_NODE_CORES;
}
/* Вибір сокета без системних блокувань */
int ret = bpf_sk_select_reuseport(ctx, &reuseport_sock_map, &target_socket_index, 0);
if (ret == 0)
return SK_PASS;
return SK_DROP;
}
char _license[] SEC("license") = "GPL";
Завдяки цьому алгоритму пакети однієї криптографічної сесії стабільно обробляються на одному ядрі незалежно від змін IP-адреси чи портів клієнта. Початкові пакети рукостискання обробляються на тому ядрі, яке отримало апаратне переривання від мережевої карти, підтримуючи високу локальність кешів L1/L2.
Підключення з простору користувача через SO_ATTACH_REUSEPORT_EBPF
Для активації диспетчеризації робочі сокети створюються та реєструються в масиві eBPF за допомогою викликів POSIX:
int setup_reuseport_cluster(int epoll_fd, int bpf_prog_fd, int sock_map_fd)
{
int num_cores = 16;
for (int i = 0; i < num_cores; i++) {
int sfd = socket(AF_INET, SOCK_DGRAM, 0);
int opt = 1;
setsockopt(sfd, SOL_SOCKET, SO_REUSEPORT, &opt, sizeof(opt));
struct sockaddr_in addr = {
.sin_family = AF_INET,
.sin_port = htons(51820),
.sin_addr.s_addr = htonl(INADDR_ANY)
};
bind(sfd, (struct sockaddr *)&addr, sizeof(addr));
/* Збереження дескриптора сокета в карту sockarray */
__u32 key = i;
bpf_map_update_elem(sock_map_fd, &key, &sfd, BPF_ANY);
/* Прив'язка програми eBPF до першого сокета групи */
if (i == 0) {
setsockopt(sfd, SOL_SOCKET, SO_ATTACH_REUSEPORT_EBPF,
&bpf_prog_fd, sizeof(bpf_prog_fd));
}
}
return 0;
}
Після прив'язки до першого сокета ядро автоматично поширює логіку eBPF на всі сокети кластера SO_REUSEPORT.
Застосування в архітектурі RiceGuard
У глобальній мережі Anycast платформа RiceGuard впроваджує BPF_PROG_TYPE_SK_REUSEPORT для мінімізації міжпроцесорних затримок:
- Усунення затримок NUMA: Робочі потоки RiceGuard прив'язані до ядер процесора, безпосередньо з'єднаних із шиною PCIe мережевого адаптера 100G. Дешифрування та комутація пакетів відбуваються виключно в межах локального кешу L3, що зменшує затримку шлюзу на понад 60%.
- Стійкість до роумінгу мобільних пристроїв: Коли користувач змінює базові станції або типи зв'язку, класифікатор eBPF визначає токен сесії та спрямовує пакети активному потоку без повторного узгодження ключів.
- Оновлення шлюзів без втрати пакетів: Під час планового оновлення серверного програмного забезпечення нові сокети реєструються в
REUSEPORT_SOCKARRAY, тоді як старі поступово завершують поточні сесії, виключаючи скидання з'єднань.
Поєднання SO_REUSEPORT із програмованим керуванням сокетами через eBPF забезпечує максимальну масштабованість та надійну ізоляцію трафіку в сучасних VPN-шлюзах.



