Туннелирование зашифрованного трафика на высоких скоростях требует согласованной работы между сокетными интерфейсами и низкоуровневыми очередями обработки пакетов. Традиционные демоны виртуальных частных сетей функционируют преимущественно в пользовательском пространстве (user space), обмениваясь сетевыми фреймами через виртуальные символьные устройства TUN/TAP. При таком подходе каждый пакет влечет за собой множественные переключения контекста между ядром и пользователем, а также копирование данных между буферами приложений и структурами sk_buff ядра Linux.
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
Одной из критических проблем туннельных соединений является риск образования маршрутной петли. Если зашифрованный пакет направляется на внешний физический интерфейс, но таблица маршрутизации по умолчанию (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 при передаче данных. Когда исходящий сетевой пакет попадает в виртуальный интерфейс 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 о необходимости фрагментации непосредственно из сетевого стека, что позволяет немедленно корректировать MTU пути без сброса криптографических сессий.
- Защита буферов с помощью SO_RCVBUFFORCE: Динамическое масштабирование очередей приема для легитимных туннелей во время масштабных UDP-атак, сохраняющее непрерывность клиентских сессий.
Интеграция сокетов непосредственно в ядро Linux и отказ от лишних уровней трансляции данных обеспечивают WireGuard и RiceGuard высокую предсказуемость задержек и надежную изоляцию трафика в современных сетях.



