Как работает cryptokey routing в WireGuard
Поймите принцип работы cryptokey routing в WireGuard. Узнайте, почему параметр AllowedIPs одновременно выполняет функции таблицы маршрутизации и списка контроля доступа.
Принцип работы WireGuard в одном тезисе
WireGuard привязывает каждый пакет к открытому ключу. Этот механизм называется cryptokey routing, и на нем строится вся архитектура: строка AllowedIPs рядом с узлом (peer) одновременно является таблицей маршрутизации для исходящих пакетов и списком контроля доступа для входящих пакетов от этого узла. Одна настройка — две функции. Читайте AllowedIPs именно так, и любой конфигурационный файл WireGuard станет понятным.
Здесь нет таблицы сессий, привязанной к IP-адресам, и нет базы данных пользователей. Узел — это открытый ключ плюс набор адресов, которые этот ключ может использовать. Рукопожатие (handshake) и таймеры нужны для поддержания этой привязки при изменениях в нижележащей сети. Если вы хотите сначала увидеть работающий туннель, а потом теорию, создайте его с помощью собственного VPN-сервера WireGuard на своем VPS, а затем вернитесь сюда, если какая-то строка конфигурации вызовет вопросы.
AllowedIPs — это таблица маршрутизации и список доступа
Рассмотрим сначала исходящее направление. Ядро направляет пакет на устройство wg0 обычным способом через основную таблицу маршрутизации. Затем WireGuard сопоставляет адрес назначения пакета с таблицей, содержащей разрешенные префиксы всех пиров, используя алгоритм поиска по самому длинному совпадению префикса. Совпадение указывает на конкретный пир, который определяет открытый ключ, а тот, в свою очередь, определяет сессионный ключ и UDP-эндпоинт. Пакет шифруется для этого пира и отправляется по адресу.
Если ни один AllowedIPs пира не охватывает адрес назначения, пакет не отправляется, так как отсутствует ключ для шифрования.
ping: sendmsg: Required key not availableЭта ошибка означает одно: адрес, к которому вы пытаетесь обратиться, не указан ни у одного из пиров. Другая ошибка, ping: sendmsg: Destination address required, означает, что пир был найден, но у WireGuard нет для него эндпоинта, так как он не был настроен и еще не был получен.
Теперь рассмотрим входящее направление. 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, поэтому получатель отбрасывает такой пакет без какого-либо ответа. Нет ни ошибки, ни сброса соединения, ни 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, становится обычными маршрутами в основной таблице.
Единственное, что полный туннель не исправляет сам по себе, — это разрешение имен, так как резолвер, который клиент получил из локальной сети, обычно продолжает использоваться, а маршрут к нему является более специфичным. Это отдельная задача, описанная в DNS, который утекает за пределы туннеля WireGuard.
Для чего на самом деле нужен PersistentKeepalive
WireGuard не отправляет никаких данных при отсутствии трафика. Нет ни сигналов проверки связи (heartbeat), ни обновления сессии, вообще ничего. Эта тишина экономит заряд аккумулятора и помогает в описанном выше сценарии со сканированием, но она нарушает работу одной конкретной конфигурации.
Узел, находящийся за NAT (network address translation) или за stateful-файрволом, доступен извне только до тех пор, пока в этом устройстве существует правило трансляции (mapping), созданное исходящим пакетом. Типичное время жизни UDP-трансляции начинается примерно с 30 секунд. Как только время жизни истекает, пакеты из публичной сети отбрасываются промежуточным устройством, и туннель выглядит нерабочим до тех пор, пока узел за NAT не отправит что-либо. PersistentKeepalive = 25 отправляет пустой аутентифицированный пакет каждые 25 секунд, что укладывается в этот минимальный интервал, поэтому трансляция остается активной.
Настраивайте этот параметр на узле, который находится за NAT. Серверу с публичным адресом и открытым 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 автоматически создаёт записи узлов, но правила пересылки по-прежнему настраиваются на хосте. Такое же разделение сохраняется, когда слой координации распространяет префикс автоматически: объявление частной сети с VPS через subnet router Tailscale заменяет ручное редактирование AllowedIPs на каждом узле, но sysctl для пересылки пакетов и правила firewall на самом маршрутизаторе по-прежнему должны настраиваться вами.
Почему 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-адресом сохраняет текущий публичный IP другой стороны, и
wg showотображает его. Вместе с адресом туннеля, который зафиксирован в конфигурации, это создает стабильный идентификатор, следующий за пользователем при смене сетей. На собственном VPS это допустимо. Именно поэтому коммерческие сервисы добавляют дополнительный уровень поверх протокола. - Он аутентифицирует ключ, а не человека. Тот, у кого есть файл закрытого ключа, и является узлом. Храните
/etc/wireguardс правами 700, а файлы ключей — с правами 600. - В протоколе нет списка отзыва и срока действия ключей. Доступ прекращается только тогда, когда вы удаляете запись об узле с каждого сервера, где она есть, а статические ключи действуют до тех пор, пока вы их не удалите.
Ничто из этого не делает WireGuard слабым. Это делает его компактным, а компактность — главная цель: он аутентифицирует и шифрует, оставляя управление идентификацией и распределение адресов тому, что вы построите поверх него. Уровень координации, описанный в сравнении WireGuard и Tailscale, существует именно для заполнения этого пробела, используя ту же плоскость передачи данных, о которой вы только что прочитали. Как только такой уровень внедрен, следующим решением становится вопрос о том, кто может получить доступ к сервису, запущенному внутри туннеля, что и определяет выбор между Tailscale serve и funnel для конкретного порта.
FAQ
Что такое маршрутизация по криптоключам (cryptokey routing) в WireGuard?
Маршрутизация по криптоключам — это правило, которое привязывает каждый пакет к открытому ключу. Каждая запись об узле (peer) содержит список префиксов в AllowedIPs. При отправке пакета WireGuard выбирает узел, сопоставляя адрес назначения пакета со списком каждого узла (используется метод наибольшего совпадения префикса), поэтому список работает как таблица маршрутизации. При получении пакета, после расшифровки и проверки подлинности, адрес источника внутри пакета должен входить в список этого же узла, иначе пакет отбрасывается; таким образом, список работает как список контроля доступа. В WireGuard нет отдельной конфигурации маршрутизации или внутреннего межсетевого экрана, так как один этот список выполняет обе задачи.
Нужно ли настраивать PersistentKeepalive на обоих узлах?
Нет. Установите этот параметр на стороне, находящейся за NAT (network address translation) или за stateful-файрволом, что обычно соответствует клиенту. WireGuard не передает данные в режиме простоя, поэтому запись в таблице трансляций, позволяющая удаленной стороне связаться с узлом, часто истекает в течение минуты, после чего туннель перестает работать в одном из направлений. PersistentKeepalive = 25 отправляет пустой пакет с проверкой подлинности каждые 25 секунд и поддерживает соединение открытым. Узлу с публичным IP-адресом и открытым UDP-портом этот параметр не требуется.
Почему при отправке ping через туннель возникает ошибка "Required key not available"?
Потому что адрес назначения не входит ни в один список AllowedIPs ни одного из узлов. Маршрутизация по криптоключам не нашла ключ для шифрования пакета, и ядро отказалось его отправлять. Выполните wg show wg0 allowed-ips и сравните вывод с адресом, который вы пытаетесь пропинговать. Похожая ошибка Destination address required указывает на другую проблему: узел был найден, но у WireGuard нет данных о его конечной точке (endpoint), так как она не была настроена, а пакеты с проверкой подлинности от этого узла еще не поступали.
Может ли межсетевой экран обнаружить и заблокировать WireGuard?
Да. WireGuard выполняет проверку подлинности и шифрование трафика, не пытаясь скрыть его природу. Сообщения рукопожатия имеют фиксированный размер 148 и 92 байта, первый байт каждого сообщения указывает на его тип, а транспортным протоколом является UDP. Поэтому системы глубокого анализа пакетов (DPI) легко идентифицируют этот протокол. Сети, блокирующие UDP или выполняющие сигнатурный анализ протоколов, будут его блокировать. Чтобы скрыть туннель, его необходимо инкапсулировать в другой протокол, что реализуется сторонними инструментами, а не настройками самого WireGuard.