Високошвидкісне тунелювання шифрованого трафіку потребує злагодженої взаємодії між сокетними структурами та низькорівневими чергами обробки пакетів. Традиційні демони віртуальних приватних мереж здебільшого функціонують у просторі користувача (user space), передаючи мережеві кадри через віртуальні символьні пристрої TUN/TAP. За такої схеми кожен пакет викликає перемикання контексту між ядром та користувачем, а також додаткове копіювання даних між буферами застосунку та мережевими структурами ядра Linux (sk_buff).
WireGuard усуває ці вузькі місця завдяки прямій інтеграції в мережевий стек ядра. Центральну роль у цій архітектурі відіграє файл drivers/net/wireguard/socket.c — підсистема, що відповідає за створення, прив'язку, мультиплексування та маршрутизацію сокетів UDP безпосередньо в просторі ядра. Замість використання сокетів як високорівневих точок для користувацьких процесів, WireGuard підключає ядерні сокети напряму до швидкого шляху приймання пакетів.
Життєвий цикл сокетів та ініціалізація Dual-Stack
Під час створення мережевого інтерфейсу WireGuard (наприклад, wg0) модуль ядра виділяє криптографічні структури та викликає функцію wg_socket_init(). На відміну від звичайних сервісів, які використовують виклики POSIX (socket(), bind()), WireGuard взаємодіє з мережею за допомогою внутрішніх інтерфейсів ядра:
int wg_socket_init(struct wg_device *wg, u16 port)
{
struct net *net = dev_net(wg->dev);
struct socket *new_4 = NULL, *new_6 = NULL;
struct udp_port_cfg port_4 = {
.family = AF_INET,
.local_ip.s_addr = htonl(INADDR_ANY),
.local_udp_port = htons(port),
.use_saddr = false
};
#if IS_ENABLED(CONFIG_IPV6)
struct udp_port_cfg port_6 = {
.family = AF_INET6,
.local_ip6 = IN6ADDR_ANY_INIT,
.local_udp_port = htons(port),
.use_saddr = false,
.ipv6_v6only = true
};
#endif
/* Створення ядерних сокетів IPv4 та IPv6 на однаковому порті */
ret = udp_sock_create(net, &port_4, &new_4);
if (ret < 0)
return ret;
#if IS_ENABLED(CONFIG_IPV6)
ret = udp_sock_create(net, &port_6, &new_6);
if (ret < 0) {
sock_release(new_4);
return ret;
}
#endif
/* Реєстрація прямих обробників тунельної інкапсуляції */
setup_udp_tunnel_sock(net, new_4, &tunnel_cfg);
setup_udp_tunnel_sock(net, new_6, &tunnel_cfg);
return 0;
}
Архітектура dual-stack виділяє два незалежні покажчики struct socket в межах одного простору імен мережі (dev_net(wg->dev)): один для IPv4 (AF_INET) та один для IPv6 (AF_INET6). Якщо підтримку IPv6 скомпільовано в ядрі, прапорець ipv6_v6only встановлюється в true. Це усуває невизначеність зіставлення адрес IPv4 в IPv6 і запобігає зайвим витратам на перетворення протоколів.
Обидва сокети прив'язуються до одного й того самого налаштованого порту UDP. Якщо вказано порт 0, мережевий стек автоматично вибирає вільний динамічний порт для першого сокета й точно повторно призначає його для другого.
Перехоплення датаграм без копіювання через udp_tunnel_sock
У звичайній моделі сокетів вхідна датаграма пробуджує потік опитування epoll, після чого дані переміщуються в чергу сокета й зчитуються через recvmsg(). При високій швидкості передачі це спричиняє затримки та вимивання кешу процесора.
WireGuard усуває стандартну чергу приймання UDP за допомогою реєстрації прямого обробника інкапсуляції через setup_udp_tunnel_sock(). Конфігураційна структура struct udp_tunnel_sock_cfg призначає зворотний виклик encap_rcv = wg_receive.
Драйвер фізичного мережевого адаптера (Ring Buffer)
│
▼ (NAPI poll / NET_RX_SOFTIRQ)
ip_rcv() / ip6_rcv()
│
▼
udp_rcv()
│
▼
┌───────────────────────────────────────┐
│ udp_tunnel_sock_cfg.encap_rcv │ <--- Пряме перехоплення пакетів у ядрі
│ (wg_receive у drivers/net/wireguard) │ (Без перемикання контексту)
└───────────────────────────────────────┘
│
▼
Перевірка Cookie та міток MAC
│
▼
Черги паралельного дешифрування (padata)
│
▼
wg_xmit() передача в інтерфейс wg0
Коли датаграма досягає udp_rcv() базового стека, ядро перевіряє наявність зареєстрованого хука інкапсуляції. Виявивши encap_rcv, мережевий стек негайно викликає wg_receive(sk, skb) у контексті програмного переривання (NET_RX_SOFTIRQ).
Це забезпечує три суттєві технічні переваги:
- Нульове копіювання даних: Структура буфера пакета (
skb) лишається недоторканою в пам'яті ядра від кільцевого буфера мережевої карти до завершення дешифрування. - Миттєва перевірка: Модуль зчитує 4-байтовий заголовок, валідує cookie та стан рукостискання Noise ще до запуску ресурсномістких обчислень ChaCha20-Poly1305.
- Мінімізація блокувань: Оскільки пакети не затримуються в стандартних чергах сокетів, ризик блокувань та затримок буферизації зведено до нуля.
Захист від петель маршрутизації за допомогою Fwmark
Ризик виникнення петель маршрутизації (routing loops) є поширеною проблемою при налаштуванні тунелів: якщо зашифрований пакет прямує до фізичного інтерфейсу, але таблиця маршрутизації за замовчуванням (0.0.0.0/0) повертає цей UDP-пакет назад у тунельний пристрій, система потрапляє у нескінченну рекурсію.
У файлі socket.c ця проблема вирішується завдяки міткам мережевого фільтра (fwmark) та ізольованому вибору маршруту:
static struct dst_entry *wg_socket_route4(struct wg_device *wg,
struct sockaddr_in *dst,
u32 fwmark)
{
struct flowi4 fl = {
.daddr = dst->sin_addr.s_addr,
.saddr = 0,
.flowi4_proto = IPPROTO_UDP,
.flowi4_mark = fwmark,
.flowi4_flags = FLOWI_FLAG_ANYSRC
};
struct rtable *rt = ip_route_output_key(dev_net(wg->dev), &fl);
return &rt->dst;
}
Під час відправлення пакетів функція wg_socket_send_skb_to_peer() копіює значення incoming_handshake_fwmark у параметри маршрутизації (flowi4_mark або flowi6_mark). Адміністратори задають правила політики маршрутизації (ip rule add not fwmark 51820 table 51820), що скеровують згенеровані сокетом пакети поза тунельною таблицею напряму до фізичного шлюзу.
Крім того, підсистема зв'язує сокет із конкретним фізичним адаптером (sk_bound_dev_if), коли визначено параметри джерела src_if4 або src_if6.
Оптимізація пам'яті sk_buff під час відправлення
Модуль socket.c розрахований на уникнення зайвого виділення структур sk_buff під час передавання. Коли вихідний IP-пакет потрапляє в інтерфейс wg0 через wg_xmit(), драйвер заздалегідь резервує пам'ять під службові заголовки:
- 16 байтів автентифікаційного тегу Poly1305.
- 16 байтів транспортного заголовка WireGuard.
- 8 байтів заголовка UDP.
- 20 байтів IPv4 (або 40 байтів IPv6).
Завдяки точному розрахунку зміщень виклик skb_cow_head() модифікує наявний буфер на місці. Шифрування здійснюється безпосередньо в поточному буфері без дублювання даних, після чого функція udp_tunnel_xmit_skb() спрямовує пакет у чергу апаратного драйвера.
Практична оптимізація в архітектурі RiceGuard
Штатна реалізація WireGuard використовує одну пару сокетів для кожного інтерфейсу. Для клієнтських систем та серверів середнього навантаження цього цілком достатньо. Проте у високощільних Anycast-кластерах RiceGuard із сотнями тисяч паралельних тунелів поділ спільного сокета між ядрами може викликати конкуренцію ліній кешу процесора.
RiceGuard оптимізує роботу ядерних сокетів за трьома напрямками:
- Сокетна прив'язка RSS/RPS: Розподіл вхідних потоків UDP за ядрами процесора на основі хешів кінцевих точок, що унеможливлює взаємну інвалідацію кешу L1/L2 в обробниках softirq.
- Динамічний моніторинг PMTU через sk_error_report: Перехоплення повідомлень ICMP про потребу у фрагментації безпосередньо з мережевого стека, що дозволяє миттєво коригувати розмір пакетів без скидання криптографічних сесій.
- Захист черг за допомогою SO_RCVBUFFORCE: Динамічне налаштування буферів для верифікованих тунелів під час потужних UDP-атак, що зберігає стабільність підключень користувачів.
Інтеграція сокетів у ядро Linux і виключення проміжних шарів дозволяють WireGuard та RiceGuard підтримувати передбачувані мілісекундні затримки та надійну ізоляцію трафіку в сучасних мережах.



