Туннелирование зашифрованного трафика на высоких скоростях требует согласованной работы между сокетными интерфейсами и низкоуровневыми очередями обработки пакетов. Традиционные демоны виртуальных частных сетей функционируют преимущественно в пользовательском пространстве (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(), драйвер заранее резервирует пространство под заголовки:

  1. 16 байт аутентификационной метки Poly1305.
  2. 16 байт транспортного заголовка WireGuard.
  3. 8 байт заголовка UDP.
  4. 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 высокую предсказуемость задержек и надежную изоляцию трафика в современных сетях.