Как работает cryptokey routing в WireGuard
Разбираем принцип работы cryptokey routing в WireGuard. Узнайте, почему параметр AllowedIPs одновременно выполняет роль таблицы маршрутизации и списка контроля доступа для узлов.
Принцип работы WireGuard в одном предложении
WireGuard привязывает каждый пакет к открытому ключу. Этот механизм называется cryptokey routing, и он составляет основу всей архитектуры: строка AllowedIPs рядом с узлом (peer) одновременно является таблицей маршрутизации для исходящих пакетов и списком контроля доступа для входящих пакетов от этого узла. Одна настройка — две функции. Читайте AllowedIPs именно так, и любой конфигурационный файл WireGuard станет понятным.
Здесь нет таблицы сессий, привязанной к IP-адресам, и нет базы данных пользователей. Узел — это открытый ключ плюс набор адресов, которые этот ключ может использовать. Рукопожатие (handshake) и таймеры нужны для поддержания этой привязки при изменениях в нижележащей сети. Если вы хотите сначала настроить рабочий туннель, а потом изучать теорию, создайте его с помощью собственного VPN на базе WireGuard на вашем VPS, а затем вернитесь сюда, если какая-то строка конфигурации вызовет вопросы.
AllowedIPs — это таблица маршрутизации и список доступа
Рассмотрим сначала исходящее направление. Ядро направляет пакет на устройство wg0 обычным способом, через основную таблицу маршрутизации. Затем WireGuard сопоставляет адрес назначения пакета с таблицей, содержащей разрешенные префиксы для каждого узла (peer), используя алгоритм поиска по самому длинному совпадению префикса. Совпадение указывает на конкретный узел, который определяет открытый ключ, а тот, в свою очередь, определяет сеансовый ключ и UDP-адрес назначения. Пакет шифруется для этого узла и отправляется по адресу.
Если ни один параметр AllowedIPs узла не охватывает адрес назначения, пакет не отправляется, так как отсутствует ключ для шифрования.
ping: sendmsg: Required key not availableЭта ошибка означает одно: адрес, к которому вы пытаетесь обратиться, не указан ни для одного из узлов. Другая ошибка, ping: sendmsg: Destination address required, означает, что узел был найден, но у WireGuard нет данных о его конечном адресе (endpoint), так как он не был настроен и еще не был получен.
Теперь рассмотрим входящее направление. UDP-пакет поступает на порт прослушивания. WireGuard находит сеанс по индексу получателя в заголовке, проверяет счетчик по скользящему окну защиты от повторов, а затем расшифровывает и проверяет подлинность полезной нагрузки. Только после этого считывается внутренний пакет, и адрес источника этого внутреннего пакета должен входить в диапазон AllowedIPs отправляющего узла. Если это не так, пакет отбрасывается. При включенной динамической отладке ядро выводит причину в строке следующего вида:
wg0: Packet has unallowed src IP (10.8.0.9) from peer 2 (203.0.113.10:51820)Именно поэтому узел на стороне сервера получает /32. Узел, настроенный с AllowedIPs = 10.8.0.2/32, может отправлять пакеты только с адреса 10.8.0.2 и ни с какого другого. Укажите там 0.0.0.0/0, и этот клиент получит право передавать пакеты с любым адресом источника внутри вашего туннеля, включая адреса других клиентов.
Перекрывающиеся префиксы разрешаются по принципу специфичности, так как поиск выполняется по самому длинному совпадению префикса. Идентичные префиксы у двух узлов ведут себя иначе: запись переходит к тому узлу, который был настроен последним, а первый узел перестает получать этот трафик без вывода каких-либо ошибок. Команда wg show wg0 allowed-ips выводит таблицу, которая фактически находится в ядре; именно она имеет значение, если файл на диске и текущее состояние системы разошлись.
Чтение конфигурационного файла с учетом маршрутизации через cryptokey
Серверная сторона:
[Interface]
Address = 10.8.0.1/24
ListenPort = 51820
PrivateKey = <server private key>
[Peer]
PublicKey = <laptop public key>
AllowedIPs = 10.8.0.2/32Клиентская сторона:
[Interface]
Address = 10.8.0.2/32
PrivateKey = <laptop private key>
[Peer]
PublicKey = <server public key>
Endpoint = vpn.example.com:51820
AllowedIPs = 0.0.0.0/0, ::/0
PersistentKeepalive = 25Одно и то же ключевое слово имеет противоположное значение на разных сторонах. На клиенте оно означает «отправлять все пакеты назначения этому узлу». На сервере оно означает «принимать от этого узла только данный адрес». Асимметрия заключается в значениях, а не в ролях.
Половина этих ключей вообще не относится к протоколу. Address, DNS, MTU, PostUp и SaveConfig принадлежат wg-quick — скрипту оболочки, который поднимает интерфейс. Ядро никогда их не видит. wg-quick strip wg0 выводит сокращенную конфигурацию, которую фактически загружает утилита wg; это самый быстрый способ увидеть данное разделение.
Что на самом деле происходит во время рукопожатия
Рукопожатие WireGuard основано на протоколе Noise_IKpsk2 из состава Noise Protocol Framework. Часть IK наиболее полезна для системного администратора: открытый статический ключ отвечающей стороны (responder) уже известен инициатору, так как он указан в PublicKey вашего блока [Peer], а инициатор передает свой собственный открытый статический ключ внутри первого сообщения в зашифрованном виде. Таким образом, обмен сертификатами и дополнительные запросы для подтверждения личности не требуются. Пассивный наблюдатель не сможет определить, какой именно ключ инициирует соединение, если у него нет закрытого ключа отвечающей стороны.
Стоимость этого процесса — один обмен сообщениями (round trip). Инициализирующее сообщение имеет размер 148 байт, ответ — 92 байта, после чего сразу начинается передача данных. Каждая сторона генерирует новую эфемерную пару ключей Curve25519 для каждого рукопожатия, а сеансовые ключи формируются из цепочки результатов Диффи-Хеллмана, объединяющей статические и эфемерные ключи. Эфемерные закрытые ключи затем удаляются, что обеспечивает совершенную прямую секретность (forward secrecy): если кто-то запишет ваш трафик сегодня и украдет закрытый ключ сервера в следующем году, он все равно не сможет расшифровать записанные данные.
Инициация рукопожатия содержит метку времени TAI64N, и каждый узел запоминает наибольшую метку времени, полученную от другого узла, поэтому повторно отправленные (replayed) сообщения отклоняются. Пакеты данных содержат 64-битный счетчик, используемый в качестве одноразового номера (nonce), а получатель поддерживает скользящее окно недавно полученных счетчиков. Это позволяет обрабатывать повторы и значительное нарушение порядка пакетов без необходимости поддерживать состояние соединения, как в TCP.
Сеансовые ключи живут недолго, а таймеры жестко заданы в коде, а не настраиваются пользователем.
The data behind this chart
[
{
"label": "REKEY_TIMEOUT",
"seconds": 5,
"notes": "resend a handshake initiation that got no answer"
},
{
"label": "KEEPALIVE_TIMEOUT",
"seconds": 10,
"notes": "send a keepalive after receiving data and sending none back"
},
{
"label": "REKEY_ATTEMPT_TIME",
"seconds": 90,
"notes": "give up on the handshake and report the peer as down"
},
{
"label": "REKEY_AFTER_TIME",
"seconds": 120,
"notes": "sender begins a fresh handshake for a new session key"
},
{
"label": "REJECT_AFTER_TIME",
"seconds": 180,
"notes": "the old session key is refused and traffic stops"
}
]Это константы из спецификации протокола, а не замеры. Всего их 5, и они управляют всем жизненным циклом сессии. После 120 секунд использования отправитель начинает новое рукопожатие, а через 180 секунд старый ключ полностью перестает приниматься, поэтому передача трафика останавливается до завершения нового рукопожатия. Инициация, на которую не получен ответ, отправляется повторно каждые 5 секунд и отменяется через 90 секунд. Именно поэтому wg show выводит latest handshake как относительное время, и именно поэтому в активном и исправном туннеле это значение остается небольшим. Если это время растет, пока вы активно передаете трафик, это означает, что рукопожатия завершаются неудачей, а не то, что туннель простаивает.
Почему у узла нет роли клиента или сервера
Оба узла используют идентичный код и одинаковый формат конфигурации. Режим сервера отсутствует. Асимметрия, которую вы наблюдаете, обусловлена Endpoint, а Endpoint является необязательным параметром.
Узел с настроенным endpoint может инициировать рукопожатие. Узел без него ожидает, а затем узнает адрес и порт другой стороны из первого пакета, который успешно проходит аутентификацию. Этот полученный endpoint сохраняется и обновляется при каждом поступлении корректного пакета с нового адреса. Именно так работает роуминг: ноутбук, переключающийся с Wi-Fi на мобильную сеть, сохраняет тот же туннель, так как сессия идентифицируется по ключу и индексу, а не по IP-адресу. Никакого переподключения не происходит, так как в понимании TCP соединение никогда не устанавливалось.
Этот же механизм создает важный факт: узел с публичным адресом всегда хранит последний известный публичный IP другой стороны, и wg show выводит его.
Фиксированные примитивы без возможности согласования
В WireGuard отсутствует список наборов шифров. Используются ChaCha20-Poly1305 для аутентифицированного шифрования, Curve25519 для согласования ключей, BLAKE2s для хеширования и HKDF для формирования ключей. Все реализации используют именно эти алгоритмы, поэтому в протоколе нет фазы согласования, которую можно было бы проанализировать, и нет возможности понизить уровень безопасности до более слабого варианта. Это осознанный компромисс: если один из этих примитивов будет скомпрометирован, исправление потребует выпуска новой версии протокола и обновления на обеих сторонах, а не просто изменения конфигурации. Такое решение позволяет исключить большую часть кода и большинство сценариев сбоев, характерных для туннелей на базе TLS, к чему, по сути, и сводится сравнение в WireGuard против OpenVPN.
Почему порт не отвечает на сканирование
Каждое сообщение рукопожатия содержит поле под названием mac1. Это MAC (код аутентификации сообщения), вычисленный для сообщения с использованием ключа, производного от статического открытого ключа отвечающей стороны. Отправитель, не знающий этого открытого ключа, не может сформировать корректный mac1, и получатель отбрасывает такой пакет без какого-либо ответа. Никаких ошибок, никаких сбросов (reset), никаких сообщений ICMP.
Видимый результат заключается в том, что UDP-сканирование не получает ничего в ответ.
sudo nmap -sU -p 51820 vpn.example.comnmap сообщает open|filtered, что является тем же ответом, который он выдает для порта, пакеты на который молча отбрасываются межсетевым экраном. Порт ведет себя одинаково независимо от того, запущен ли WireGuard, по крайней мере для любого, у кого еще нет вашего открытого ключа.
Второе поле, mac2, используется для защиты от атак типа «отказ в обслуживании» (DoS). Когда получатель находится под нагрузкой, он отвечает на корректный запрос инициализации 64-байтовым cookie-ответом, привязанным к исходному адресу отправителя, и отказывается выполнять ресурсоемкие операции с открытыми ключами, пока отправитель не вернет этот cookie обратно. Это подтверждает подлинность исходного адреса до того, как на обработку будут затрачены ресурсы процессора, и активируется только при наличии нагрузки.
Почему 0.0.0.0/0 делает пира маршрутом по умолчанию
Поскольку AllowedIPs — это таблица маршрутизации, AllowedIPs = 0.0.0.0/0, ::/0 заявляет права на все пункты назначения для этого пира. Это и есть полная настройка туннеля.
Механизм маршрутизации, который обеспечивает работу этого процесса, интереснее самой строки. Обычный маршрут по умолчанию через wg0 вызвал бы зацикливание, так как зашифрованный UDP-пакет с вашим трафиком также должен покинуть машину и попал бы под действие собственного маршрута по умолчанию. wg-quick избегает этого с помощью policy routing. Система помечает исходящие пакеты WireGuard с помощью fwmark, помещает маршрут туннеля по умолчанию в отдельную таблицу маршрутизации и добавляет правила, согласно которым в неё попадает только немаркированный трафик. Выполните ip rule show, чтобы увидеть результат:
32764: from all lookup main suppress_prefixlength 0
32765: not from all fwmark 0xca6c lookup 51820
32766: from all lookup main0xca6c — это 51820 в шестнадцатеричном виде, и 51820 — это также номер таблицы. Правило suppress_prefixlength 0 заставляет основную таблицу игнорировать собственный маршрут по умолчанию, поэтому специфические маршруты, такие как ваша локальная подсеть, сохраняют приоритет, а весь остальной трафик направляется в таблицу туннеля. Для split tunnel ничего из этого не требуется: более узкий список, такой как AllowedIPs = 10.8.0.0/24, 10.20.0.0/16, становится обычными маршрутами в основной таблице.
Единственное, что full tunnel не исправляет сам по себе — это разрешение имен, так как резолвер, который клиент получил из локальной сети, обычно продолжает использоваться, а маршрут к нему является более специфичным. Это отдельная задача, описанная в DNS, который утекает за пределы WireGuard туннеля.
Для чего на самом деле нужен PersistentKeepalive
WireGuard не передаёт никаких данных при отсутствии трафика. Нет ни сигналов проверки связи (heartbeat), ни обновления сессии, ничего. Эта тишина экономит заряд аккумулятора и помогает в описанном выше случае со сканированием, но она нарушает работу одной конкретной конфигурации.
Узел, находящийся за NAT (network address translation) или за stateful-файрволом, доступен извне только до тех пор, пока на этом устройстве существует правило трансляции, созданное исходящим пакетом. Типичное время жизни UDP-трансляции начинается примерно с 30 секунд. Как только время жизни истекает, пакеты из публичной сети отбрасываются промежуточным устройством, и туннель перестаёт работать до тех пор, пока узел за NAT не отправит что-либо самостоятельно. PersistentKeepalive = 25 отправляет пустой аутентифицированный пакет каждые 25 секунд, что укладывается в этот минимальный стандартный интервал, поэтому правило трансляции остаётся активным.
Устанавливайте этот параметр на узле, который находится за NAT. Серверу с публичным IP-адресом и открытым UDP-портом он не нужен, а его включение там лишь создаст лишний трафик. Не путайте его с автоматическим keepalive, который срабатывает через 10 секунд после того, как узел получил данные и ему нечего отправить в ответ. Этот механизм включён всегда, и его нельзя настроить.
Маршрутизация локальной сети через туннель не является функцией WireGuard
Предположим, узел B находится в домашней сети 192.168.50.0/24, и узлу A необходимо получить к ней доступ. Две независимые системы должны быть согласованы, и только одна из них — WireGuard.
Часть WireGuard: добавьте 192.168.50.0/24 в AllowedIPs узла B на узле A. Это заставит A направлять префикс на B и принимать пакеты с этими адресами отправителя от B. Без этого криптографическая маршрутизация не будет иметь ключа для назначения и разрешения для источника.
Часть ядра: на узле B параметр net.ipv4.ip_forward должен быть 1, иначе ядро отбросит каждый расшифрованный пакет, который адресован не самому B. Цепочка forward в межсетевом экране узла B должна разрешать этот трафик. Хостам в локальной сети нужен обратный маршрут к 10.8.0.0/24, либо узел B должен применять source NAT, чтобы ответы возвращались через него.
Работа WireGuard заканчивается в момент передачи расшифрованного пакета ядру. Всё, что происходит после этого, — обычная маршрутизация и фильтрация в Linux. Именно поэтому данная проблема отражается в счётчиках nft list ruleset или в ip -s link show wg0, а не в wg show. Если вы предпочитаете управлять узлами через веб-интерфейс, запуск wg-easy в Docker создаст записи для узлов автоматически, однако правила пересылки трафика по-прежнему остаются зоной ответственности хоста.
Почему WireGuard работает на уровне ядра
wg0 — это драйвер сетевого устройства. Пакеты попадают в него через стандартный стек маршрутизации, шифруются в контексте softirq и покидают систему через UDP-сокет, не переходя в пространство пользователя. Именно это обеспечивает высокую пропускную способность и позволяет модулю оставаться в пределах четырех тысяч строк кода — достаточно малый объем для аудита и включения в основную ветку Linux 5.6 в марте 2020 года. Дистрибутивы Ubuntu 24.04 и Debian 13 включают его в состав, поэтому отсутствует только пакет wireguard-tools.
Работа в качестве обычного сетевого интерфейса имеет практические преимущества. tcpdump -ni wg0 отображает внутренние пакеты в открытом виде, тогда как tcpdump -ni eth0 udp port 51820 показывает зашифрованные внешние пакеты; сравнение этих данных позволяет сразу определить, на каком этапе происходит сбой. Инструменты netfilter и средства управления трафиком обрабатывают wg0 так же, как любой другой интерфейс. В случаях, когда модуль ядра недоступен, например, при использовании контейнерной виртуализации с общим ядром хоста, wireguard-go реализует тот же протокол в пространстве пользователя через устройство TUN. Это приводит к заметному снижению пропускной способности, так как каждый пакет дважды пересекает границу ядра.
От чего WireGuard не защищает
Модель угроз намеренно ограничена, а лаконичность протокола часто порождает неоправданные ожидания. Сформулируем это прямо.
- Он не скрывает факт использования WireGuard. Сообщения рукопожатия имеют фиксированный размер, первый байт указывает на тип сообщения, а транспортным протоколом является UDP. Системы глубокого анализа пакетов (DPI) легко распознают его, поэтому сеть, где использование VPN не приветствуется, может его заблокировать. Обфускация была исключена из протокола намеренно.
- Он не скрывает объем и время передачи данных. Полезная нагрузка дополняется только до границы 16 байт, поэтому наблюдатель всё равно видит, когда вы отправляете данные и примерно в каком объеме.
- Он сохраняет последний известный адрес конечной точки. Узел с публичным адресом хранит текущий публичный IP-адрес другой стороны, и
wg showотображает его. В сочетании с адресом туннеля, который зафиксирован в конфигурации, это создает стабильный идентификатор, следующий за пользователем при смене сетей. На собственном VPS это допустимо. Именно поэтому коммерческие сервисы добавляют дополнительный уровень поверх протокола. - Он аутентифицирует ключ, а не человека. Тот, кто владеет файлом закрытого ключа, и является узлом. Храните
/etc/wireguardс правами доступа 700, а файлы ключей — с правами 600. - В протоколе нет списка отзыва и срока действия ключей. Доступ прекращается только после удаления записи об узле с каждого сервера, где она была прописана, а статические ключи действуют до тех пор, пока вы их не удалите.
Ничто из этого не делает WireGuard слабым. Это делает его компактным, а компактность — главная цель: он выполняет аутентификацию и шифрование, оставляя управление идентификацией и распределение адресов тем инструментам, которые вы выстроите поверх него. Уровень координации, описанный в сравнении WireGuard и Tailscale, существует именно для того, чтобы заполнить этот пробел, используя тот же уровень передачи данных (data plane), о котором вы только что прочитали.
FAQ
Что такое маршрутизация по криптоключам (cryptokey routing) в WireGuard?
Маршрутизация по криптоключам — это правило, которое привязывает каждый пакет к открытому ключу. Каждая запись об узле (peer) содержит список префиксов в AllowedIPs. При отправке пакета WireGuard выбирает узел, сопоставляя адрес назначения пакета со списком каждого узла (используется метод наиболее длинного совпадения префикса), поэтому этот список работает как таблица маршрутизации. При получении, после расшифровки и проверки подлинности пакета, его внутренний адрес источника должен входить в список того же узла, иначе пакет отбрасывается; таким образом, этот список работает как список контроля доступа. В WireGuard нет отдельной конфигурации маршрутизации или внутреннего межсетевого экрана, так как один этот список выполняет обе задачи.
Нужно ли настраивать PersistentKeepalive на обоих узлах?
Нет. Настройте этот параметр на стороне, находящейся за NAT (network address translation) или за stateful-файрволом — обычно это клиент. WireGuard не передаёт данные в состоянии простоя, поэтому соответствие адресов, позволяющее удалённой стороне связаться с узлом, истекает (обычно в течение минуты), и туннель начинает выглядеть нерабочим в одном направлении. PersistentKeepalive = 25 отправляет пустой аутентифицированный пакет каждые 25 секунд и поддерживает это соответствие активным. Узлу с публичным IP-адресом и открытым UDP-портом этот параметр не требуется.
Почему при пинге через туннель возникает ошибка "Required key not available"?
Потому что адрес назначения не входит ни в один список AllowedIPs узлов. Маршрутизация по криптоключам не нашла ключ для шифрования пакета, и ядро отказалось его отправлять. Выполните wg show wg0 allowed-ips и сравните вывод с адресом, который вы пытаетесь пинговать. Похожая ошибка Destination address required указывает на другую проблему: узел был найден, но у WireGuard нет данных о его конечной точке (endpoint), так как она не была настроена, а аутентифицированные пакеты от этого узла ещё не поступали.
Может ли межсетевой экран обнаружить и заблокировать WireGuard?
Да. WireGuard аутентифицирует и шифрует трафик, не пытаясь его скрыть. Сообщения рукопожатия (handshake) имеют фиксированный размер 148 и 92 байта, первый байт каждого сообщения определяет его тип, а транспортным протоколом является UDP. Поэтому системы глубокого анализа пакетов (DPI) легко идентифицируют этот протокол. Сети, блокирующие UDP или выполняющие сигнатурный анализ протоколов, смогут его остановить. Чтобы скрыть туннель, его необходимо инкапсулировать в другой протокол, что реализуется сторонними инструментами, а не настройками самого WireGuard.