Шлюзы виртуальных частных сетей, подключенные к открытым магистральным каналам интернета, сталкиваются с фундаментальной вычислительной асимметрией. Когда удаленный клиент инициирует защищенный туннель, криптографический протокол выполняет ресурсоемкие асимметричные вычисления — в частности, скалярное умножение точек на эллиптической кривой Curve25519 — для безопасного согласования сессионных ключей.

На современных серверных процессорах операция умножения на Curve25519 занимает доли миллисекунды, тогда как отправка поддельного UDP-пакета размером 148 байт по сетевому каналу 10G требует всего лишь нескольких наносекунд. Злоумышленник, генерирующий поток поддельных пакетов инициализации с подменой IP-адресов источника, способен мгновенно перегрузить ядра процессора, вызывая зависание очередей и лишая легитимных клиентов возможности завершить установку соединения.

Чтобы исключить угрозу криптографического отказа в обслуживании (DoS) и исчерпания памяти, разработчики WireGuard внедрили специализированный алгоритм token bucket («ведро с жетонами») непосредственно в сетевой тракт ядра Linux (drivers/net/wireguard/ratelimiter.c). Ограничивая частоту обработки неаутентифицированных рукопожатий для каждого входящего IP-адреса до запуска тяжелых криптографических функций, WireGuard сохраняет стабильную скорость передачи данных даже при массированных атаках.

+-----------------------------------------------------------------------------+
|             КОНВЕЙЕР ОГРАНИЧЕНИЯ РУКОПОЖАТИЙ В ЯДРЕ WIREGUARD               |
|                                                                             |
| Входящий UDP-пакет (Тип 1: Инициализация рукопожатия)                       |
| [ Пройдена проверка MAC1 через BLAKE2s ]                                    |
|                     |                                                       |
|                     v                                                       |
|        wg_ratelimiter_allow_ip(net, ip_addr)                                |
|                     |                                                       |
|                     v                                                       |
|        Вход в критическую секцию rcu_read_lock()                            |
|        Вычисление корзины через SipHash: siphash(&ip, secret_key)           |
|                     |                                                       |
|                     v                                                       |
|        Поиск в корзине hlist таблицы table_v4 или table_v6                  |
|                     |                                                       |
|         +-----------+-----------+                                           |
|         |                       |                                           |
|         v                       v                                           |
|    [ Запись найдена ]      [ Запись отсутствует ]                           |
|         |                       |                                           |
|         |                       v                                           |
|         |                Выделение структуры ratelimiter_entry              |
|         |                Вставка в корзину (спинлок)                        |
|         |                       |                                           |
|         +----------->+<---------+                                           |
|                      |                                                      |
|                      v                                                      |
|         Восполнение жетонов на основе прошедшего времени                    |
|         (Емкость: 20 жетонов, Пополнение: 1 жетон каждые 100 мс)            |
|                      |                                                      |
|         +------------+------------+                                         |
|         |                         |                                         |
|         v                         v                                         |
|   [ Жетонов >= 1 ]          [ Жетоны исчерпаны (< 1) ]                      |
|         |                         |                                         |
|         v                         v                                         |
|   Списание 1 жетона         Отказ в прямом вычислении рукопожатия           |
|   rcu_read_unlock()         Запрос Cookie-челленджа MAC2 (cookie.c)         |
|   Выполнение скалярного     Сброс пакета при отсутствии валидной Cookie     |
|   умножения Curve25519                    |                                 |
|                                           v                                 |
|                                     rcu_read_unlock()                       |
+-----------------------------------------------------------------------------+

Асимметрия рукопожатий в классических архитектурах VPN

Традиционные протоколы защищенных туннелей организуют установление сессий через многоэтапные согласования, перегружающие сервер фиксацией промежуточных состояний. В классическом протоколе IPsec IKEv2 (RFC 7296) сервер создает структуру полуоткрытого состояния сразу после получения запроса IKE_SA_INIT, сохраняя случайные числа и криптографические предложения в памяти до завершения взаимной аутентификации. Лавинообразный поток пакетов быстро истощает память slab ядра, провоцируя сбои операционной системы из-за нехватки памяти (OOM).

Реализации OpenVPN опираются на конечные автоматы OpenSSL или mbedTLS в пользовательском пространстве. Каждый входящий пакет TLS Client Hello влечет за собой разбор структур ASN.1, проверку цепочек сертификатов и валидацию открытых ключей RSA или ECDSA. Поскольку эти вызовы происходят после многократных переключений контекста между ядром и пользовательским процессом, входящий поток всего в несколько тысяч пакетов в секунду загружает процессор на 100%, приводя к обрыву соединений у всех активных клиентов.

Кроме того, простые ограничители трафика на базе типовых правил пакетных фильтров создают собственные узкие места:

  1. Разрастание таблиц соединений: Отслеживание сотен тысяч поддельных адресов IPv4 и IPv6 переполняет таблицы межсетевых экранов.
  2. Конкуренция за блокировки: Многопоточные ограничители с централизованными мьютексами парализуют работу ядер CPU из-за взаимных блокировок при синхронизации кэш-линий.
  3. Деградация хеширования (Hash-DoS): Применение неключевых функций хеширования позволяет злоумышленнику сгенерировать набор IP-адресов, попадающих в одну цепочку коллизий, замедляя поиск с $O(1)$ до $O(N)$ и останавливая обработку трафика.

Устройство ratelimiter.c: алгоритм Token Bucket в ядре

WireGuard устраняет указанные проблемы благодаря выделенному механизму в файле ядра drivers/net/wireguard/ratelimiter.c. При инициализации драйвер создает две изолированные таблицы: table_v4 для протокола IPv4 и table_v6 для IPv6.

struct ratelimiter_entry {
    u64 last_time_ns;
    u32 tokens;
    spinlock_t lock;
    struct hlist_node hash;
    struct rcu_head rcu;
    union {
        __be32 ip;
        struct in6_addr ip6;
    };
};

Каждая запись в таблице хранит параметры индивидуального ведра с жетонами для конкретного удаленного IP-адреса:

  • Предельная емкость (PACKETS_BURST): Задана на уровне 20 жетонов, что дает легитимным клиентам возможность кратковременного всплеска при повторных попытках отправки или смене базовых станций.
  • Скорость генерации жетонов (PACKETS_PER_SECOND): Добавляет 10 жетонов в секунду (1 жетон каждые 100 миллисекунд), рассчитываясь динамически по системным наносекундным меткам ядра (ktime_get_coarse_boottime_ns()).
  • Минимальный объем памяти: Компактная структура позволяет хранить информацию о десятках тысяч узлов с пренебрежимо малым расходом оперативной памяти.

1. Защита от коллизий с помощью SipHash

Чтобы сам ограничитель скорости не стал мишенью алгоритмических атак, номера корзин вычисляются с использованием стойкой криптографической функции SipHash (siphash_1u32 для IPv4 и siphash_2u64 для IPv6). При старте интерфейса WireGuard генерирует случайный 128-битный ключ из пула системной энтропии:

static inline u32 entry_hash(const struct ratelimiter_entry *entry)
{
    return siphash_1u32((__force u32)entry->ip, &table_key);
}

Так как секретный ключ table_key хранится исключительно в памяти ядра и обновляется при каждом перезапуске интерфейса, внешний наблюдатель лишен возможности рассчитать адреса, приводящие к коллизиям. Распределение записей строго следует пуассоновской модели, гарантируя доступ к элементам за константное время $O(1)$.

2. Неблокирующий поиск RCU и локальные спинлоки

Для поддержки высокой пропускной способности чтение таблиц ограничителя скорости выполняется в защищенных секциях Read-Copy Update (RCU):

bool wg_ratelimiter_allow_ip(struct ratelimiter *limiter, struct sk_buff *skb)
{
    struct ratelimiter_entry *entry;
    u32 hash;

    rcu_read_lock();
    hash = entry_hash_from_skb(skb);
    hlist_for_each_entry_rcu(entry, &limiter->table_v4[hash & limiter->mask], hash) {
        if (entry->ip == ip) {
            bool allowed = deduct_token(entry);
            rcu_read_unlock();
            return allowed;
        }
    }
    rcu_read_unlock();
    return allocate_and_insert_entry(limiter, ip);
}

В штатном режиме ядра процессора находят нужный IP-адрес без захвата глобальных блокировок. После разыменования указателя ratelimiter_entry рабочий поток берет только локальный спинлок этой отдельной записи (entry->lock) для пересчета времени и списания жетона. Потоки, обрабатывающие пакеты с разных IP-адресов, работают полностью параллельно.

Если частота запросов с определенного IP-адреса превышает доступный лимит жетонов, WireGuard не отбрасывает пакет безвозвратно. Вместо этого включается многоуровневая защитная логика:

Штатный режим (жетоны в наличии):
Клиентский пакет [ Тип 1 | Без аутентификации | MAC1 ]
    --> Ограничитель разрешает операцию
    --> Скалярное умножение Curve25519
    --> Отправка ответа Тип 2

Режим перегрузки (жетоны исчерпаны):
Клиентский пакет [ Тип 1 | Превышение лимита пакетов | MAC1 ]
    --> Ограничитель запрещает прямые криптографические вычисления
    --> Если поле MAC2 пустое: Отправка Cookie-ответа Тип 3
    --> Клиент вычисляет MAC2 и повторяет отправку Тип 1 [ MAC1 + MAC2 ]
    --> Сервер проверяет Cookie без сохранения состояния и завершает сессию

В моменты сильного флуда шлюз отказывается выполнять вычисления Curve25519 для непроверенных отправителей. Сервер принуждает клиента вернуть криптографическую Cookie (формируемую из IP-адреса, порта и периодически сменяемого секретного ключа). Поскольку для создания Cookie требуется лишь симметричное хеширование BLAKE2s, сервер справляется с миллионами запросов на скорости интерфейса. Легитимный клиент автоматически формирует MAC2 и восстанавливает сессию, в то время как потоки с поддельными адресами мгновенно нейтрализуются.

Фоновая очистка неактивных записей памяти

При распределенных DoS-атаках с использованием ботнетов из миллионов адресов таблица отслеживания могла бы бесконтрольно расти.

Для гарантированного удержания расхода памяти в строгих границах WireGuard запускает периодическую фоновую очистку через системные отложенные очереди задач (schedule_delayed_work):

static void cleanup_entries(struct work_struct *work)
{
    struct ratelimiter_entry *entry;
    struct hlist_node *n;
    u64 now = ktime_get_coarse_boottime_ns();

    spin_lock_bh(&table_lock);
    for (int i = 0; i < HASH_SIZE; ++i) {
        hlist_for_each_entry_safe(entry, n, &table[i], hash) {
            if (now - entry->last_time_ns > EXPIRATION_INTERVAL_NS) {
                hlist_del_rcu(&entry->hash);
                kfree_rcu(entry, rcu);
            }
        }
    }
    spin_unlock_bh(&table_lock);
}

Фоновый процесс регулярно выполняет следующие действия:

  • Удаление по таймауту: Любая запись, от которой не поступало новых пакетов в течение заданного интервала (обычно несколько секунд), исключается из хеш-таблицы с помощью функции hlist_del_rcu().
  • Безопасная деаллокация с RCU: Фактическое освобождение памяти выполняется через kfree_rcu(), что исключает гонки состояний при одновременном обращении из других потоков.
  • Адаптивный контроль лимитов: При превышении общего числа записей во время интенсивной атаки алгоритм принудительно удаляет наиболее старые записи, гарантируя стабильность оперативной памяти шлюза.

Настройка параметров сетевого стека и мониторинг

Инженеры Linux могут отслеживать работу сетевых очередей и оптимизировать поведение системы стандартными средствами администрирования.

Проверка очередей сокета UDP на сетевом порту WireGuard:

# Просмотр состояния буферов сокета UDP
ss -u -a -m -p | grep 51820

Анализ балансировки программных прерываний (softirq) по ядрам процессора:

# Мониторинг распределения нагрузки прерываний сетевой карты
mpstat -P ALL 1 5

Корректировка системных параметров для поддержания высокой пропускной способности:

# Увеличение длины очереди сетевых устройств и размеров буферов
sysctl -w net.core.netdev_max_backlog=100000
sysctl -w net.core.rmem_max=67108864
sysctl -w net.core.wmem_max=67108864
sysctl -w net.ipv4.udp_rmem_min=16384

Устойчивость к перегрузкам в инфраструктуре VPNRice

Поддержание качественной виртуальной частной сети корпоративного уровня требует предсказуемой устойчивости к распределенным атакам. Сеть VPNRice повсеместно использует многоуровневую защиту в узлах Anycast:

  1. Защита на аппаратной скорости: Реализация механизма token bucket на уровне ядра позволяет серверам VPNRice отсекать всплески паразитного трафика до фазы асимметричных вычислений, сохраняя вычислительные мощности для передачи пользовательских данных.

  2. Автоматический запуск Cookie-испытаний: При обнаружении перегрузок шлюзы VPNRice активируют верификацию Cookie MAC2, благодаря чему реальные пользователи продолжают работу без разрывов соединений, а флуд нейтрализуется на аппаратном уровне.

  3. Изолированные ключи SipHash: Каждый сетевой домен узла VPNRice генерирует независимые соли хеширования, предотвращая перекрестные алгоритмические коллизии между клиентами.

Интеграция алгоритма token bucket, хеш-таблиц SipHash и неблокирующей архитектуры RCU обеспечивает шлюзам VPN надежную передачу данных и минимальные задержки при любых нагрузках.