Wireguard что это такое
WireGuard — прекрасный VPN будущего?
Наступило время, когда VPN уже не является каким-то экзотическим инструментом бородатых сисадминов. Задачи у пользователей разные, но факт в том, что VPN стал нужен вообще всем.
Проблема текущих VPN решений в том, что их тяжело правильно настроить, дорого обслуживать, а так же в них полно legacy кода сомнительного качества.
Несколько лет назад канадский специалист по информационной безопасности Jason A. Donenfeld решил, что хватит это терпеть, и начал работу над WireGuard. Сейчас WireGuard готовится к включению в состав ядра Linux, он даже получил похвалы от Линуса Торвальдса и в американском сенате.
Заявленные преимущества WireGuard над другими VPN решениями:
Неужели найдена серебрянная пуля? OpenVPN и IPSec пора закапывать? Я решил с этим разобраться, а заодно сделал скрипт для автоматической установки личного VPN сервера.
Принципы работы
Принципы работы можно описать примерно так:
Вся основная логика WireGuard занимает менее 4 тысяч строк кода, тогда как OpenVPN и IPSec имеют сотни тысяч строк. Для поддержки современных криптоалгоритмов предлагается включить в состав ядра Linux новый криптографический API Zinc. В данный момент идет обсуждение, насколько это удачная идея.
Производительность
Максимальное преимущество в производительности (по сравнению с OpenVPN и IPSec) будет заметно на Linux системах, так как там WireGuard реализован в виде модуля ядра. Кроме этого поддерживаются macOS, Android, iOS, FreeBSD и OpenBSD, но в них WireGuard выполняется в userspace со всеми вытекающими последствиями для производительности. Поддержку Windows обещают добавить в ближайшем будущем.
Результаты бенчмарков с официального сайта:
Мой опыт использования
Я не эксперт по настройке VPN. Однажды настраивал OpenVPN ручками и это было очень муторно, а IPSec даже и не пытался. Слишком много решений нужно принимать, очень легко выстрелить себе в ногу. Поэтому я всегда пользовался готовыми скриптами для настройки сервера.
Так вот, WireGuard, с моей точки зрения, вообще идеален для пользователя. Все низкоуровневые решения приняты в спецификации, поэтому процесс подготовки типичной VPN инфраструктуры занимает всего несколько минут. Нафакапить в конфигурации практически невозможно.
Процесс установки детально описан на официальном сайте, отдельно хочется отметить отличную поддержку OpenWRT.
Генерируются ключи шифрования утилитой wg :
Далее, нужно создать серверный конфиг /etc/wireguard/wg0.conf со следующим содержанием:
и поднять туннель скриптом wg-quick :
На клиентской машине, создать конфиг /etc/wireguard/wg0.conf :
И точно так же поднять туннель:
Осталось настроить NAT на сервере, чтобы клиенты могли выходить в Интернет, и все готово!
Такую простоту использования и компактность кодовой базы удалось достичь за счет отказа от функционала дистрибьюции ключей. Здесь нет сложной системы сертификатов и всего этого корпоративного ужаса, короткие ключи шифрования распространяются примерно как SSH ключи. Но в связи с этим возникает проблема: WireGuard будет не так просто внедрять в некоторых уже существующих сетях.
Из недостатков стоит отметить, что WireGuard не заработает через HTTP proxy, поскольку в качестве транспорта есть только протокол UDP. Возникает вопрос, возможно ли будет обфусцировать протокол? Конечно, это не прямая задача VPN, но для OpenVPN, например, существуют способы маскировки под HTTPS, что помогает жителям тоталитарных стран полноценно пользоваться Интернетом.
Выводы
Подводя итог, это очень интересный и перспективный проект, можно уже сейчас использовать его на личных серверах. Какой профит? Высокая производительность на Linux системах, простота настройки и поддержки, компактная и читабельная кодовая база. Однако, бросаться переводить комплексную инфраструктуру на WireGuard еще рано, стоит подождать включение в состав ядра Linux.
Для экономии своего (и вашего) времени я разработал автоматический установщик WireGuard. С его помощью можно поднять личный VPN для себя и своих знакомых даже ничего в этом не понимая.
WireGuard клиент для Windows на основе BoringTun
Введение в BoringTun
В начале 2019 года компания Cloudflare представила BoringTun, реализацию WireGuard протокола, написанную на языке Rust. Код проекта распространяется под лицензией BSD-3-Clause и состоит из двух основных частей:
Исполняемый файл boringtun с реализацией WireGuard для Linux и macOS в пространстве пользователя
Библиотеку, которая может использоваться для интеграции функциональности клиента WireGuard в произвольные приложения для любых платформ, включая iOS и Android. Библиотека реализует только протокол WireGuard без сопутствующих механизмов туннелирования, которые зависят от целевой платформы.
К настоящему времени BoringTun пережил пару релизов и лёг в основу собственного VPN сервиса от Cloudflare под названием WARP.
WireGuard VPN клиенты для Windows
На настоящий момент под Windows доступны две «универсальные» реализации WireGuard (специализированные решения различных VPN провайдеров мы здесь не рассматриваем):
Оба вышеупомянутых WireGuard клиента используют таблицу маршрутизации для перенаправления сетевого трафика на виртуальный VPN интерфейс с последующим шифрованием, инкапсуляцией и отправкой серверу через UDP сокет. Аналогичный подход используется в подавляющем большинстве VPN клиентов…
Стоит однако, упомянуть и некоторые свойственные ему ограничения:
Отсутствие возможности разделить локальные приложения, какие нам бы хотелось маршрутизировать через VPN, а какие пропускать минуя туннель. Это, вообще говоря, довольно популярная тема вопросов на Интернет форумах. Например, одному пользователю необходимо, чтобы один из браузеров, например Firefox, подключался бы только через VPN, а Google Chrome наоборот. Другому, использовать VPN исключительно для qBittorrent, и ещё желательно так, чтобы последний при неактивном VPN не мог бы подключиться к сети и т.д. и т.п..
Если необходимо исключить туннелирование для каких-либо сетей из глобального ‘0.0.0.0/0, ::0/0’, то нельзя просто указать параметр вида ‘DisallowedIPs’, нужно рассчитать адреса сетей для AllowedIPs с учетом исключённых сетей. Не критично, но не очень удобно. К слову, чтобы избежать ошибок при сложных вычислениях, можно воспользоваться готовым калькулятором.
Так возникла идея «прозрачного» клиента для WireGuard, который бы использовал надежную реализацию протоколов WireGuard, позволял определять правила для приложений, по возможности не вносил изменений в сетевую конфигурацию, и при этом был бы достаточно производительным и совместимым с «референсной» реализацией WireGuard.
Построй свой мир
Несмотря на кажущуюся сложность задачи, собрать готовое решение в виде консольного приложения (сервиса) под Windows оказалось не слишком сложно. Все основные компоненты лежат на GitHub, осталось только расположить их в правильном порядке и связать между собой.
Для перехвата сетевых пакетов воспользуемся библиотекой Windows Packet Filter. Если сильно не вдаваться в детали, то на системах начиная с Windows Vista она представляет собой NDIS 6.0 Filter Driver с API, которое позволяет приложению работающему в пользовательском режиме забирать пакеты из сетевого стека, при необходимости менять их и возвращать обратно. А поддержку VPN протоколов WireGuard нам обеспечит вышеупомянутая библиотека BoringTun.
Единственное, что меня несколько смущает в интерфейсе библиотеки BoringTun, и что хотелось бы отдельно отметить, это отсутствие возможности передать параметр PresharedKey в функцию new_tunnel (см. ниже). А поскольку PresharedKey по умолчанию используется большинством скриптов для автоматического конфигурирования WireGuard на Linux, это создаёт определенные неудобства. Так что некоторое время назад я отправил pull request, исправляющий данную ситуацию. Однако, постепенно складывается впечатление, что мейнтейнерам это не особенно интересно. Не знаю наверняка, но возможно причина в том, что в WARP PresharedKey не используется.
Итак, для начала нам понадобятся статические сборки библиотеки BoringTun:
Собрать все необходимые конфигурации библиотеки можно из командной строки или с помощью простого скрипта:
В результате мы получим статическую библиотеку, предоставляющую все необходимые функции для создания WireGuard туннеля, генерации handshake сообщений, шифрования и инкапсуляции исходящих сетевых пакетов, декапсуляции и дешифрования входящих сетевых пакетов (принадлежащих WireGuard туннелю), а так же поддержания туннеля в актуальном состоянии (генерация handshake каждые две минуты, отправка keepalive пакетов).
API библиотеки BoringTun выглядит следующим образом (см. boringtun/src/wireguard_ffi.h):
Назначение каждой функции в целом понятно из ее названия. Но поскольку документация для BorinTun доступна только для Rust, то кратенько пройдёмся, что к чему.
Таким образом, для того чтобы создать и поддерживать WireGuard туннель необходимо сделать следующее:
Вызвать функцию new_tunnel и передать ей параметры полученные из файла конфигурации.
Вызвать функцию wireguard_force_handshake и отправить handshake в UDP пакете серверу WireGuard.
Создать поток периодически вызывающий функцию wireguard_tick и отправляющий результат ее работы (handshake и keepalive сообщения) серверу WireGuard.
Запустить фильтрацию входящих и исходящих сетевых пакетов:
Для исходящих пакетов (упрощённый workflow):
Проверяем должен ли данный пакет быть отправлен через VPN туннель.
Если нет то сразу возвращаем его сетевому стеку, берём следущий пакет и возращаемся к предыдущему пункту. Если да, то переходим к следующему шагу.
Меняем IP адрес источника на IP адрес указанный в конфигурационном файле и пересчитываем необходимые контрольные суммы пакета.
Передаём пакет функции wireguard_write, добавляем к полученному результату UDP и IP заголовки, подсчитываем контрольные суммы и возвращаем изменённый пакет сетевому стеку.
Для входящих пакетов (упрощенный workflow):
Проверяем, принадлежит ли полученный WireGuard пакет нашему туннелю.
Если нет то сразу возвращаем его сетевому стеку, берём следущий пакет и возращаемся к предыдущему пункту. Если да, то переходим к следующему шагу.
Извлекаем из UDP пакета полезную нагрузку и передаём ее функции wireguard_read. Если пакет успешно расшифрован, то меняем в нем IP адрес назначения на IP адрес сетевого интерфейса, пересчитываем контрольные суммы и возвращаем изменённый таким образом пакет сетевому стеку.
Приведённая обобщенная схема клиента опускает ряд деталей реализации, подробное изложение которых вышло бы чрезмерно объемным и могло бы утомить читателя. На мой взгляд, куда интереснее посмотреть, сможет ли клиент построенный на основе библиотеки BoringTun и работающий в пространстве пользователя обеспечить производительность сравнимую с существующими решениями. Или же он представляет чисто теоретический интерес. Кстати, для тех, кто заинтересовался, текущую версию консольного WireGuard клиента можно взять отсюда.
Citius, altius, fortius
Для сравнительных тестов на стороне клиента я использовал Intel® NUC DC3217IYE (Core i3-3217u) выпущенный около девяти лет назад. Такое относительно устаревшее железо было выбрано из тех соображений, что на нем легко увидеть разницу в производительности VPN клиентов уже на гигабитной сети. Для сравнительных тестов с современными CPU потребовалась бы как минимум 10-гигабитная сеть, которой под рукой к сожалению не оказалось. В таблице ниже приведены лучшие результаты из серий по 10 тестовых прогонов используя четыре параллельных TCP потока (по одному на каждый поток процессора), для максимальной нагрузки CPU.
WireSock VPN Client v1.0.46
892 Mbits/sec
WireGuard for Windows
(kernel driver) v0.5
WireGuard. How it was
Привет. Меня зовут Алексей, и я System Infrastructure Engineer в inDriver. В этой статье на конкретных кейсах объясню, почему WireGuard — отличная VPN-система для работы, в чем разница использования ее утилит, и что надо помнить, когда с ними работаешь. Прошу под кат!
С чего все началось
Исторически сложилось, что часть сервисов в inDriver находятся на арендованных серверах. Для защиты трафика между серверами в одном дата-центре мы решили зашифровать его. Изначально рассматривали следующие решения:
Вот сравнительный анализ, где N — количество серверов:
Количество соединений в полносвязной топологии
В итоге мы остановились на WireGuard.
И понеслось
Во время тестирования появилась проблема: информации по полносвязной топологии чуть меньше, чем ничего. Несмотря на это, построить ее просто: нужно обменяться своими параметрами (endpoint_ip, allowed_ips, public_key) с другими серверами и получить их в ответ. Состряпать конфигурации не составило труда. А еще первоначально проблем не наблюдалось, потому что использовалась утилита wg-quick.
Утилита wg-quick может работать только с файлом конфигурации. Файл конфигурации состоит из полей:
PrivateKey — приватный ключ. Не путь к ключу, а именно его содержимое. Обязательно должно присутствовать.
ListenPort — порт, на котором работает WireGuard. Если нет, выбирается случайно.
FwMark — маркировка исходящего трафика.
Address — IP-адреса, которые будут назначены интерфейсу.
DNS — DNS-адреса (через запятую или несколько раз указать DNS). Требует наличие пакета resolvconf! Если нет, DNS не добавляются.
MTU — значение MTU для интерфейса. Если нет, MTU не изменяется.
Table — таблица маршрутизации, куда будут добавлены маршруты для WireGuard. Если нет, используется таблица main.
PreUp, PostUp, PreDown, PostDown — скрипты, которые будут выполняться перед и после подключения, перед и после отключения соответственно. Можно указывать несколько скриптов. Выполняться они будут в порядке, описанном в файле.
SaveConfig — флаг сохранения всех изменений в файл конфигурации.
Каждый пир должен быть представлен отдельной секцией [Peer] со следующими полями (совпадающими с параметрами запуска wg set peer. ):
PublicKey — публичный ключ. Обязательно должен присутствовать.
PresharedKey — дополнительный ключ шифрования. Если нет, дополнительное шифрование не используется.
AllowedIPs — список разрешенных подсетей. Можно указывать каждую сеть через запятую, а можно несколько раз указать это поле. Если нет, никакая сеть не закреплена за пиром.
Endpoint — IP-адрес или имя пира с обязательным указанием порта. Можно не указывать.
PersistentKeepalive — об этом упомяну отдельно.
Чтобы полносвязная топология задышала, нужно выполнить два условия:
У каждого сервера в пирах должны присутствовать сведения обо всех серверах.
Для корректной работы в allowed-ips следует указывать адрес соседа (ip/32).
Достаточно пользоваться утилитой wg-quick.
В списках пиров все серверы дата-центра.
Кажется, пора открывать шампанское! Но не все так просто.
Проблема 1. Несколько дата-центров
А если добавим еще пару-тройку дата-центров? Есть 3 решения:
Тупо добавить серверы из других дата-центров в общую кучу. Нам этот вариант не подошел, так как не у всех серверов есть внешний сетевой интерфейс.
Создать отдельное подключение для сети внутри дата-центра и отдельное соединение для связи дата-центров между собой. Это тоже не наш метод — 2 сетевых подключение на 2 больше, чем хочется.
Соединить дата-центры между собой, используя существующее соединение! Бинго!
Шлюзы в дата-центрах нужно соединить между собой, используя внешние IP-адреса и дополнительно передавая подсеть дата-центра в список allowed_ips (по-нолановски, правда?).
Для шлюзов дата-центра надо добавить в пиры другие шлюзы дата-центра и передать свою подсеть им в allowed_ips.
Проблема 2. Как добавить сервер, не выключая сетевой интерфейс?
Оказалось, wg-quick не имеет возможности применить новую конфигурацию. Попробуем сделать это через утилиту wg. Но и тут не все так просто!
В чем прелесть этой утилиты?
1. Для использования не обязательно наличие файла конфигурации. Для работы достаточно создать новый интерфейс типа WireGuard.
Примечательно, что имя интерфейса можно задать любое, исходя из практики именования сетевых интерфейсов. Выходит, к этому интерфейсу применимы все действия, которые можно проделывать с любым интерфейсом (например, менять MTU). Также можно вывести только WireGuard-интерфейсы командой:
2. Не требуется использование конфигурации. Все можно настроить вручную, используя команду wg set. Параметры можно указывать по очереди или вместе.
Коротко о параметрах этой команды:
1. listen-port
UDP-порт, на котором будет работать сервер WireGuard. Если не указывать, при каждом новом запуске сервиса будет выбран случайный порт. Это усложняет настройку сервера.
Важно помнить, что одновременно на одном порту может работать только один сервис! Если поднимать несколько WireGuard-подключений, каждому следует выделять отдельный порт.
2. fwmark
Маркировка исходящего трафика. Мне было лень искать в nftables правила маркировки, но для iptables правил не создается. Экспериментально выяснил, что помечаются пакеты в таблице FILTER цепочки OUTPUT. Следует помнить, что если приложение лезет своими битами в маркировку пакета (например, k8s), это может поломать нормальную работу WireGuard. Тогда следует задуматься о необходимости использования этого параметра.
3. private-key
Приватный ключ. Немного поэкспериментировав, я выяснил, что это просто массив из 32 байт, закодированный в base64. По сути, выполнение скрипта на python3:
Эквивалентно выполнению команды:
Для того, чтобы впихнуть ключ в WireGuard и избежать создание файла, можно применить следующее:
4. peer
Работа с пирами. Дополнительные параметры:
remove — удаление пира из списка. Полностью прекращает работу с данным пиром.
preshared-key — ключ для дополнительного шифрования трафика. Да, требуется наличие файла.
— показывает, с какого адреса и порта ждать подключение.
persistent-keepalive — об этом чуть попозже.
allowed-ips / [, / ]. — список сетей, которым предназначается трафик. Если в WireGuard-интерфейс попадает пакет, то пир, которому перенаправляют пакет, берется из этого параметра. Если в интерфейс попадет пакет, а ответственного пира нет, пакет никуда не пойдет. Для icmp ловил отлуп вида “Required key not available”.
Хорошо! А как сделать так, чтобы не пришлось вручную вводить информацию о пирах? Ответ: сформировать файл конфигурации. Следуя инструкциям, в нем должна быть одна секция [INTERFACE] со следующими полями:
PrivateKey — приватный ключ. Не путь к ключу, а именно его содержимое. Обязательно должно присутствовать.
ListenPort — порт, на котором работает WireGuard. Если нет, выбирается случайно каждый раз.
FwMark — маркировка исходящего трафика.
Каждый пир должен быть представлен отдельной секцией [Peer] со следующими полями (совпадающими с параметрами запуска wg set peer. ):
PublicKey — публичный ключ. Обязательно должен присутствовать.
PresharedKey — дополнительный ключ шифрования. Если нет, дополнительное шифрование не используется.
AllowedIPs — список разрешенных подсетей. Можно указывать каждую сеть через запятую, а можно несколько раз указать это поле. Если нет, никакая сеть не закреплена за пиром.
Endpoint — IP-адрес или DNS имя пира с обязательным указанием порта. Можно не указывать.
PersistentKeepalive — терпение, чуть ниже все расскажу.
Чтобы не марать руки, можно сгенерировать файл автоматически, используя метод showconf (выводит текущую конфигурацию):
Также есть команды setconf, addconf и syncconf. Об этом поподробнее:
setconf — применяет конфигурационный файл.
addconf — добавляет конфигурацию к текущей. В файле конфигурации не всегда должна присутствовать секция [Interface]. Будет выполняться wg set для каждого пира в файле.
syncconf — синхронизирует текущую конфигурацию и файл конфигурации. Применяет разницу. Если в текущей конфигурации ListenPort = 51820, а в файле он не указан, после применения получится случайный порт, так как изменилось значение. Это касается и приватного ключа! Будет выполняться wg set peer только для тех пиров, которые изменили значение.
Для просмотра информации о соединениях можно воспользоваться командой:
Мы получаем много информации, поэтому я чаще пользуюсь командой:
Чтобы после перезапуска сервиса все изменения сохранились, придется сохранять изменившуюся конфигурацию:
Заметил, что после изменений, сделанных утилитой wg служба wg-quick@$WG_IFACE перестает работать: остановка службы не происходит, а попытка запуска завершается ошибкой «Данный интерфейс существует».
Для восстановления конфигурации используется wg-quick, для изменения — wg.
В списках пиров все серверы дата-центра.
Для шлюзов дата-центра добавить в пиры другие шлюзы дата-центра и передать свою подсеть им в allowed_ips.
Все получилось — интерфейс поднялся, данные передаются. Ура!
Проблема 3. Маршрутизация и утилита wg
Все новые пиры на месте, но доступа к ним нет. Что же случилось? Все просто: утилита wg не следит за маршрутами, за ними надо следить самостоятельно!
Все надо делать самому: следить за маркировкой пакетов, маршрутами, правилами файрвола, заворачивать трафик. Для того, чтобы добавить пир через утилиту wg, нужно сделать это вручную и убедиться, что он есть в таблице маршрутизации. И после этого все заработает.
Выход был найден! Выяснилось, что скрипт:
Выполняет всю пыльную работу за инженера!
Значит, системе не обязательно знать, куда уходит пакет. Главное, что он падает в интерфейс WireGuard, а что, как, кому и куда — решает сам VPN! Вот она — магия WireGuard!
Есть отдельный скрипт для добавления маршрутов.
При изменении конфигурации нужно запускать скрипт добавления маршрутов.
Почти что без проблем
Ключевое слово — почти. Первая проблема возникает, если маршрут по умолчанию идет через WireGuard, а обращение — на внешний интерфейс. Объясню на пальцах:
Клиент обращается к серверу по внешнему адресу exIP.
На сервер приходит пакет на адрес exIP.
Сервер формирует ответный пакет.
Согласно таблице маршрутизации пакет отправляется… в интерфейс WireGuard.
Шлюз WireGuard пересылает пакет, используя wgIP-адрес.
Клиент получает ответ от сервера с IP-адреса wgIP и… отбрасывает его, так как он не ждет от него ответа. Ответ ожидается от exIP.
Клиент уходит в закат, не дождавшись ответа.
Ленивый инженер скажет всем клиентам отключить rp_filter, но эта история не про нас. Можно маркировать пакеты с помощью файрвола, а потом маркированные пакеты заворачивать в отдельную таблицу маршрутизации. Но я слишком ленив, чтобы ковырять еще и его.
Выход: только хардкор! Только iproute2! Достаточно заворачивать исходящий трафик с определенного адреса в отдельную таблицу маршрутизации! Для адреса 172.100.0.2 и маршрутизатора 172.100.0.1 нужно выполнить 2 действия:
Эти команды создадут дополнительную таблицу маршрутизации с номером 10000 и завернут весь исходящий трафик с IP-адреса 172.100.0.2 через маршрутизатор 172.100.0.1, что нам и нужно.
Это нужно добавить в скрипт маршрутизаци, чтобы правила применялись при поднятии интерфейса WireGuard.
Еще одна проблема возникает при мониторинге состояния WireGuard. Оказалось, что если сервер A не обращается какое-то время к серверу B, handshake не происходит! Как быть?
PersistentKeepalive — a seconds interval, between 1 and 65535 inclusive, of how often to send an authenticated empty packet to the peer for the purpose of keeping a stateful firewall or NAT mapping valid persistently.
На деле же раз в указанный промежуток пиру посылается пакет нулевого размера. Это позволяет поддерживать соединение постоянно открытым.
Заметил, что при отсутствии этого параметра возникал неприятный баг. При отсутствии соединения, даже если пинговать пир, рукопожатия не возникало, пока вручную с обратной стороны не начать пинговать пир в ответ. Сервер пытается установить рукопожатие, а с той стороны ответа нет, так как взаимодействия с сервером отсутствует. Чтобы такого не происходило, нужно использовать PersistentKeepalive на всех хостах или на центральном шлюзе.
Принято! Выходит, что:
PersistentKeepalive не должен быть равен нулю.
Резюме
Моей изначальной целью было поглубже раскрыть, что такое WireGuard и что надо помнить, когда с ним работаешь. Данная статья — результат тщательного исследования работы этого VPN.
Исследование работы WireGuard мы начали в прошлом октябре, а переход на него — в этом феврале. Следующий подход с основательным зубрением этого предмета ожидается, когда WireGuard будет поставляться из коробки со всеми ОС, включая мобильными. Буду изучать, как менее проблемно добавить к этой топологии пользовательские подключения.
Спасибо, что дочитали статью до конца. С радостью отвечу на ваши вопросы в комментариях.