Как стать автором
Обновить

WireGuard клиент для Windows на основе BoringTun

Время на прочтение10 мин
Количество просмотров24K

Введение в 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 for Windows. Может использоваться как качестве клиента, так и в качестве сервера (неофициально). До последнего времени был построен на основе wireguard-go (реализация протокола WireGuard в пространстве пользователя, написанная на языке Go) и виртуального сетевого интерфейса WinTun. Начиная с августа 2021 года эта реализация постепенно вытеснялась новой версией, реализованной полностью в режиме ядра - WireGuardNT. Последний представляет собой виртуальный сетевой интерфейс, в драйвере которого реализован WireGuard протокол, а в качестве транспорта используются Windows Kernel Sockets (WSK).

  • TunSafe - реализация в пространстве пользователя, которая может быть использована только в качестве клиента. Написана на C++ с использованием ассемблера в наиболее критичных к производительности функциях шифрования. В качестве виртуального сетевого интерфейса использует NDIS 6 TAP-Windows драйвер, тот же самый, который используется в OpenVPN.

Оба вышеупомянутых 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 не используется.

Раз уж я упомянул PresharedKey, то стоит кратко пояснить роль, которую он играет в протоколе WireGuard . Допустим, случилось «невероятное» и алгоритмы шифрования с открытым ключом построенные на эллиптических кривых оказались недостаточно стойкими. Помимо резкого обрушения биткойна и хаоса на криптовалютных биржах под угрозой окажутся и все SSH/TLS/VPN сессии, которые использовали криптографию с открытым ключом на эллиптических кривых и были заботливо записаны и сохранены провайдерами как раз для такого случая. Для того, чтобы избежать подобной малоприятной ситуации в будущем, в WireGuard и был добавлен 256-битный симметричный ключ обеспечивающий еще один дополнительный уровень криптографической стойкости.

Итак, для начала нам понадобятся статические сборки библиотеки BoringTun:

  • Добавим несколько строк в .cargo/config:

[build]
rustflags = [
    "-C", "llvm-args=-slp-recursion-max-depth=1024",
    "--emit", "asm", # For whatever reason rustc produces faster code when this flag is enabled
]

[target.x86_64-pc-windows-msvc]
rustflags = ["-Ctarget-feature=+crt-static"]

[target.i686-pc-windows-msvc]
rustflags = ["-Ctarget-feature=+crt-static"]

[target.'cfg(unix)']
runner = 'sudo -E'
  • Собрать все необходимые конфигурации библиотеки можно из командной строки или с помощью простого скрипта:

cargo build --target=i686-pc-windows-msvc --lib
cargo build --target=i686-pc-windows-msvc --lib --release
cargo build --target=x86_64-pc-windows-msvc --lib
cargo build --target=x86_64-pc-windows-msvc --lib --release

В результате мы получим статическую библиотеку, предоставляющую все необходимые функции для создания WireGuard туннеля, генерации handshake сообщений, шифрования и инкапсуляции исходящих сетевых пакетов, декапсуляции и дешифрования входящих сетевых пакетов (принадлежащих WireGuard туннелю), а так же поддержания туннеля в актуальном состоянии (генерация handshake каждые две минуты, отправка keepalive пакетов).

API библиотеки BoringTun выглядит следующим образом (см. boringtun/src/wireguard_ffi.h):

// Allocate a new tunnel
struct wireguard_tunnel *new_tunnel(const char *static_private,
                                    const char *server_static_public,
                                    const char *preshared_key,
                                    uint16_t keep_alive, // Keep alive interval in seconds
                                    uint32_t index,      // The 24bit index prefix to be used for session indexes
                                    void (*log_printer)(const char *),
                                    enum log_level log_level);

// Deallocate the tunnel
void tunnel_free(struct wireguard_tunnel *);

struct wireguard_result wireguard_write(struct wireguard_tunnel *tunnel,
                                        const uint8_t *src,
                                        uint32_t src_size,
                                        uint8_t *dst,
                                        uint32_t dst_size);

struct wireguard_result wireguard_read(struct wireguard_tunnel *tunnel,
                                       const uint8_t *src,
                                       uint32_t src_size,
                                       uint8_t *dst,
                                       uint32_t dst_size);

struct wireguard_result wireguard_tick(struct wireguard_tunnel *tunnel,
                                       uint8_t *dst,
                                       uint32_t dst_size);

struct wireguard_result wireguard_force_handshake(struct wireguard_tunnel *tunnel,
                                                  uint8_t *dst,
                                                  uint32_t dst_size);

struct stats wireguard_stats(struct wireguard_tunnel *tunnel);

Назначение каждой функции в целом понятно из ее названия. Но поскольку документация для BorinTun доступна только для Rust, то кратенько пройдёмся, что к чему.

new_tunnel - создаёт и инициализирует WireGuard туннель. В текущей версии BoringTun, отсутствует параметр PresharedKey, поэтому приведённая выше функция является небольшим расширением оригинальной функции.

tunnel_free - освобождает существующий WireGuard туннель.

wireguard_write - принимает на вход IP пакет, шифрует и добавляет заголовок WireGuard протокола. Важное замечание, передаваемые в функцию буфера памяти не должны накладываться.

wireguard_read - принимает на вход пакет WireGuard протокола, декапсулирует его (убирает WireGuard заголовок) и расшифровывает. Замечание относительно наложения входных и выходных буферов памяти для этой функции так же справедливо.

wireguard_tick - данная функция должна периодически быть вызываема для каждого активного WireGuard туннеля. Рекомендуемый разработчиками интервал составляет 100ms. Функция может вернуть handshake (каждые две минуты) или keepalive пакет (в зависимости от значения параметра PersistentKeepalive), который надлежит отправить серверу.

wireguard_force_handshake - генерирует пакет WireGuard handshake. Функция обычно используется при первом подключении к серверу (в дальнейшем handshake пакеты генерирует функция wireguard_tick) или когда необходимо переподключиться к серверу в следствие изменения сетевого подключения, IP адреса и т.д. и т.п..

wireguard_stats - запрашивает текущую статистику по туннелю, которая включает в себя время происшедшее с последнего handshake, количество принятых/отправленных байт, оценочное значение потерь пакетов и RTT (Round-trip delay time).

Поскольку Windows Packet Filter включает достаточное количество примеров того, как можно организовать фильтрацию пакетов на определенном сетевом интерфейсе, я не буду останавливаться на этом подробно. Например, можно воспользоваться готовым C++ классом simple_packet_filter передав ему две лямбда функции для обработки входящих и исходящих пакетов соответственно. В дальнейшем будем считать, что у нас есть две такие функции фильтрующие сетевой интерфейс подключённый к Интернет .

Таким образом, для того чтобы создать и поддерживать WireGuard туннель необходимо сделать следующее:

  1. Вызвать функцию new_tunnel и передать ей параметры полученные из файла конфигурации.

  2. Вызвать функцию wireguard_force_handshake и отправить handshake в UDP пакете серверу WireGuard.

  3. Создать поток периодически вызывающий функцию wireguard_tick и отправляющий результат ее работы (handshake и keepalive сообщения) серверу WireGuard.

  4. Запустить фильтрацию входящих и исходящих сетевых пакетов:

    • Для исходящих пакетов (упрощённый workflow): 

      1. Проверяем должен ли данный пакет быть отправлен через VPN туннель.

      2. Если нет то сразу возвращаем его сетевому стеку, берём следущий пакет и возращаемся к предыдущему пункту. Если да, то переходим к следующему шагу.

      3. Меняем IP адрес источника на IP адрес указанный в конфигурационном файле и пересчитываем необходимые контрольные суммы пакета.

      4. Передаём пакет функции wireguard_write, добавляем к полученному результату UDP и IP заголовки, подсчитываем контрольные суммы и возвращаем изменённый пакет сетевому стеку.

    • Для входящих пакетов (упрощенный workflow):

      1. Проверяем, принадлежит ли полученный WireGuard пакет нашему туннелю.

      2. Если нет то сразу возвращаем его сетевому стеку, берём следущий пакет и возращаемся к предыдущему пункту. Если да, то переходим к следующему шагу.

      3. Извлекаем из UDP пакета полезную нагрузку и передаём ее функции wireguard_read. Если пакет успешно расшифрован, то меняем в нем IP адрес назначения на IP адрес сетевого интерфейса, пересчитываем контрольные суммы и возвращаем изменённый таким образом пакет сетевому стеку. 

Приведённая обобщенная схема клиента опускает ряд деталей реализации, подробное изложение которых вышло бы чрезмерно объемным и могло бы утомить читателя. На мой взгляд, куда интереснее посмотреть, сможет ли клиент построенный на основе библиотеки BoringTun и работающий в пространстве пользователя обеспечить производительность сравнимую с существующими решениями. Или же он представляет чисто теоретический интерес. Кстати, для тех, кто заинтересовался, текущую версию консольного WireGuard клиента можно взять отсюда.

Citius, altius, fortius

Для сравнительных тестов на стороне клиента я использовал Intel® NUC DC3217IYE (Core i3-3217u) выпущенный около девяти лет назад. Такое относительно устаревшее железо было выбрано из тех соображений, что на нем легко увидеть разницу в производительности VPN клиентов уже на гигабитной сети. Для сравнительных тестов с современными CPU потребовалась бы как минимум 10-гигабитная сеть, которой под рукой к сожалению не оказалось. В таблице ниже приведены лучшие результаты из серий по 10 тестовых прогонов используя четыре параллельных TCP потока (по одному на каждый поток процессора), для максимальной нагрузки CPU.

iperf3 -c 10.66.66.1 -P 4
(upload)

iperf3 -c 10.66.66.1 -R -P 4
(download)

WireSock VPN Client v1.0.46

879 Mbits/sec

892 Mbits/sec

WireGuard for Windows 
(kernel driver) v0.5

892 Mbits/sec

719 Mbits/sec

WireGuard for Windows
(WinTun) v0.4.1

288 Mbit/sec

325 Mbits/sec

TunSafe v1.4

435 Mbits/sec

284 Mbits/sec

Признаться, я и сам был несколько удивлён результатом. WireGuard клиент работающий в пространстве пользователя по своей пропускной способности оказался способен сравняться с представленной в августе реализацией референсного клиента в режиме ядра (WireGuardNT). И по всей видимости, не в последнюю очередь благодаря библиотеке BoringTun, которая служит прекрасной иллюстрацией того, где и как может использоваться Rust в современных реалиях.

Помимо впечатляющей производительности, новый клиент может предложить и некоторые полезные на мой взгляд функциональные расширения:

  • Выборочное туннелирование приложений посредством дополнительного параметра AllowedApps секции Peer.

  • Исключение указанных адресов сетей из туннеля посредством дополнительного параметра DisallowedIPs секции Peer.

  • Совместимость с Windows 10 Mobile Hotspot. Клиенты подключённые к точке доступа будут работать через VPN туннель сконфигурированный на хосте.

  • В комбинации с референсным WireGuard for Windows возможно организовать вложенный двойной VPN полностью на стороне клиента без необходимости конфигурировать сервера. Впрочем, если добавить немного кода, то и без референсного клиента в этой схеме можно вполне обойтись. Всегда есть к чему стремиться…

В заключение, я хотел бы продемонстрировать конфигурацию (значения ключей и порта изменены), которую сам использую для того, чтобы Google Chrome, подключался через VPN ко всем IP адресам за исключением локальной сети, а весь остальной трафик ходил как обычно.

[Interface]
PrivateKey = AD9GaupPbRlfjPTfhLm1/lm5qtgwvFcB1rGpKOZkXXE=
Address = 10.66.66.2/32, fd42:42:42::2/128
DNS = 94.140.14.14, 94.140.15.15
MTU = 1420

[Peer]
PublicKey = tRb3/FxzJBhinaVPY/tyoX40PS7EY1mmzFyrL/dAnwY=
AllowedIPs = 0.0.0.0/0, ::/0
Endpoint = ora.sshvpn.me:51820
AllowedApps = chrome
DisallowedIPs = 192.168.1.0/24

Благодарю за внимание и терпение, всех кто дочитал до конца. Буду рад любым отзывам и замечаниям.

P.S. WARP

Работая над анголоязычной версией статьи, я решил поближе познакомиться с клиентом WARP для Windows. Как оказалось, его архитектура близка к вышеописанной (отсутствует виртуальный адаптер, трафик перехватывается), единственная отличие заключается в драйвере использованном для перехвата пакетов. Если описанный выше клиент использовал Windows Packet Filter, то WARP использует WinDivert. Вероятно, последний дает более высокую нагрузку CPU на скриншоте ниже.

Теги:
Хабы:
Всего голосов 5: ↑5 и ↓0+5
Комментарии3

Публикации

Истории

Работа

Ближайшие события