Кроссплатформенная реализация

Описание пользовательской реализации WireGuard для кросс-платформенной работы и интерфейса конфигурации

Кроссплатформенная пользовательская реализация

Хотя WireGuard изначально был разработан для ядра Linux для максимальной производительности, он может работать в пользовательском пространстве с использованием отдельной реализации. В настоящее время wireguard-go вполне функционален, а wireguard-rs находится в разработке.

Что такое пользовательская реализация? В отличие от реализации в ядре, которая работает на уровне операционной системы, пользовательская реализация работает как обычное приложение. Это позволяет запускать WireGuard на платформах, где нет доступа к ядру, или когда требуется более простая интеграция с приложениями.

В любой момент документации, когда вы видите ip link add wg0 type wireguard, вы можете вместо этого написать wireguard-go wg0. Всё остальное должно быть идентичным.

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

Интерфейс командной строки

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

# userspace-wg [-f/--foreground] INTERFACE-NAME

Объяснение параметров:

  • -f или --foreground — опциональный флаг, который заставляет процесс оставаться на переднем плане (не демонизироваться). Это полезно для отладки и при запуске из системных менеджеров.
  • INTERFACE-NAME — имя создаваемого интерфейса (например, wg0).

Например, реализация на Go будет вызываться следующим образом для создания интерфейса wg0:

# wireguard-go wg0

Что происходит при запуске? Выполнение этой команды создаёт виртуальное TUN-устройство с именем wg0 и затем демонизируется (переходит в фоновый режим). После успешной демонизации и поднятия интерфейса создаётся /var/run/wireguard/wg0.sock (или /run/wireguard/wg0.sock в зависимости от платформы) — это UNIX-сокет домена, работающий в потоковом режиме.

Что такое TUN-устройство? Это виртуальный сетевой интерфейс, который работает на уровне IP. Приложения могут читать и писать в него IP-пакеты, как если бы они работали с обычным сетевым интерфейсом. WireGuard использует TUN для получения и отправки зашифрованных пакетов.

Что такое UNIX-сокет? Это механизм межпроцессного взаимодействия (IPC), который позволяет процессам обмениваться данными. В данном случае он используется для того, чтобы утилита wg(8) могла управлять процессом WireGuard.

На Windows используются те же семантики с двунаправленным именованным каналом (named pipe) в \\.\pipe\WireGuard\wg0. Именованные каналы — это аналог UNIX-сокетов в Windows.

Использование wg(8) для конфигурации

Инструмент wg(8) используется для настройки интерфейса, что обеспечивает полное единообразие интерфейсов конфигурации во всех реализациях.

Почему это важно? Пользователи и скрипты могут использовать одни и те же команды для управления WireGuard независимо от того, работает ли он в ядре или в пользовательском пространстве. Это значительно упрощает автоматизацию и написание документации.

Инструмент wg(8) ищет интерфейсы в /var/run/wireguard/*.sock (или /run/wireguard/*.sock). Пользовательские реализации должны корректно завершать работу в ответ на SIGINT/SIGTERM, удаление TUN-интерфейса или удаление файла UNIX-сокета.

Как это работает:

  1. Вы запускаете wireguard-go wg0 — создаётся процесс и сокет
  2. Вы запускаете wg setconf wg0 config.conf — утилита wg(8) находит сокет и отправляет команду
  3. Процесс WireGuard получает команду через сокет и применяет конфигурацию
  4. Всё прозрачно работает как с ядерной версией

Инструмент wg(8) подключается к этим сокетам и отправляет и получает следующий текстовый протокол.

Протокол конфигурации

Реализация WireGuard должна отвечать на две команды: get и set, обе версии 1 на момент написания.

Что такое версия протокола? Это позволяет в будущем добавлять новые функции без нарушения совместимости. Номер версии указывает, какой формат команд и ответов используется.

Команда get

wg(8) отправляет команду get, которая выглядит так:

get=1
{empty line}

Объяснение: Команда get запрашивает текущую конфигурацию и статус интерфейса. Пустая строка в конце указывает на завершение команды.

Пользовательская реализация отвечает на команду get так:

key1=value1
key2=value2
key3=value3
key4=value4
key5=value5
...
errno=0
{empty line}

Объяснение ответа: После всех ключей и значений добавляется errno=0 (означает “успех”) и пустая строка. Если произошла ошибка, errno будет содержать код ошибки.

Команда set

wg(8) отправляет команду set, которая выглядит так:

set=1
key1=value1
key2=value2
key3=value3
key4=value4
key5=value5
...
{empty line}

Объяснение: Команда set отправляет новую конфигурацию. В отличие от get, она содержит ключи и значения для применения.

Пользовательская реализация отвечает на команду set так:

errno=0
{empty line}

Объяснение: Ответ на set проще — только код ошибки и пустая строка.

Если произошла ошибка, errno — соответствующее целое число из errno.h (стандартные коды ошибок POSIX).

Ключи и значения

Уровень интерфейса

Эти ключи применяются ко всему интерфейсу:

КлючОписаниеФормат значенияПримечания
private_keyПриватный ключ интерфейсаШестнадцатеричная строка (в нижнем регистре)Все нули = удалить ключ
listen_portПорт для прослушиванияДесятичное целое число
fwmarkМетка для исходящих пакетовДесятичное целое число0 = удалить fwmark
replace_peers=trueЗаменить всех пировТолько ключПоследующие пиры заменяют существующих

Пример использования replace_peers: Если у вас уже есть 5 пиров, и вы отправляете set с replace_peers=true и 3 новыми пирами, старые 5 пиров будут удалены, а останутся только 3 новых.

Уровень пира

Эти ключи применяются к конкретному пиру и следуют после ключа public_key:

КлючОписаниеФормат значенияПримечания
public_keyПубличный ключ пираШестнадцатеричная строка (в нижнем регистре)Начинает блок пира
remove=trueУдалить пираТолько ключПрименяется к предыдущему пиру
update_only=trueОбновить только существующегоТолько ключОшибка, если пир не существует
preshared_keyПредварительный общий ключШестнадцатеричная строкаВсе нули = удалить ключ
endpointКонечная точкаIP:port или [IP]:portДля IPv6 используются квадратные скобки
persistent_keepalive_intervalИнтервал keepaliveДесятичное целое число0 = отключить
replace_allowed_ips=trueЗаменить все разрешённые IPТолько ключЗаменяет список, а не добавляет
allowed_ipРазрешённый IP-адресIP/cidrМожет быть указан несколько раз
rx_bytesПолучено байтДесятичное целое числоТолько для get
tx_bytesОтправлено байтДесятичное целое числоТолько для get
last_handshake_time_secВремя рукопожатия (сек)Десятичное целое числоТолько для get, время Unix
last_handshake_time_nsecВремя рукопожатия (нс)Десятичное целое числоТолько для get, время Unix
protocol_versionВерсия протокола1Обычно не используется

Важные замечания о ключах

  1. Порядок важен: Все ключи уровня интерфейса должны идти до ключей уровня пира. Это логично, так как сначала определяются общие параметры интерфейса, а затем параметры отдельных пиров.

  2. Обработка allowed_ip: Если одинаковое значение allowed_ip уже существует у другого пира, запись IP будет удалена у того пира и добавлена к текущему. Это обеспечивает уникальность разрешённых IP-адресов.

  3. Формат ключей: Все ключи в шестнадцатеричном формате должны быть в нижнем регистре. Это стандарт для криптографических ключей в WireGuard.

Пример диалога

Команда get

wg(8) отправляет:

get=1
{empty line}

Пользовательская реализация WireGuard отвечает:

private_key=e84b5a6d2717c1003a13b431570353dbaca9146cf150c5f8575680feba52027a
listen_port=12912
public_key=b85996fecc9c7f1fc6d2572a76eda11d59bcd20be8e543b15ce4bd85a8e75a33
preshared_key=188515093e952f5f22e865cef3012e72f8b5f0b598ac0309d5dacce3b70fcf52
allowed_ip=192.168.4.4/32
endpoint=[abcd:23::33%2]:51820
public_key=58402e695ba1772b1cc9309755f043251ea77fdcf10fbe63989ceb7e19321376
tx_bytes=38333
rx_bytes=2224
allowed_ip=192.168.4.6/32
persistent_keepalive_interval=111
endpoint=182.122.22.19:3233
public_key=662e14fd594556f522604703340351258903b64f35553763f19426ab2a515c58
endpoint=5.152.198.39:51820
allowed_ip=192.168.4.10/32
allowed_ip=192.168.4.11/32
tx_bytes=1212111
rx_bytes=1929999999
protocol_version=1
errno=0
{empty line}

Разбор примера:

  • Интерфейс имеет приватный ключ, слушает на порту 12912
  • Пир 1: публичный ключ b859..., preshared_key, разрешённый IP 192.168.4.4/32, конечная точка IPv6
  • Пир 2: публичный ключ 5840..., передано 38333 байт, получено 2224 байт, разрешённый IP 192.168.4.6/32, keepalive 111 секунд
  • Пир 3: публичный ключ 662e..., два разрешённых IP, большая передача данных

Команда set

wg(8) отправляет:

set=1
private_key=e84b5a6d2717c1003a13b431570353dbaca9146cf150c5f8575680feba52027a
fwmark=0
listen_port=12912
replace_peers=true
public_key=b85996fecc9c7f1fc6d2572a76eda11d59bcd20be8e543b15ce4bd85a8e75a33
preshared_key=188515093e952f5f22e865cef3012e72f8b5f0b598ac0309d5dacce3b70fcf52
replace_allowed_ips=true
allowed_ip=192.168.4.4/32
endpoint=[abcd:23::33%2]:51820
public_key=58402e695ba1772b1cc9309755f043251ea77fdcf10fbe63989ceb7e19321376
replace_allowed_ips=true
allowed_ip=192.168.4.6/32
persistent_keepalive_interval=111
endpoint=182.122.22.19:3233
public_key=662e14fd594556f522604703340351258903b64f35553763f19426ab2a515c58
endpoint=5.152.198.39:51820
replace_allowed_ips=true
allowed_ip=192.168.4.10/32
allowed_ip=192.168.4.11/32
public_key=e818b58db5274087fcc1be5dc728cf53d3b5726b4cef6b9bab8f8f8c2452c25c
remove=true
{empty line}

Разбор примера:

  • replace_peers=true — все старые пиры будут удалены
  • fwmark=0 — fwmark отключён
  • Добавляются 3 пира, последний (e818...) помечен remove=true для удаления
  • У каждого пира используется replace_allowed_ips=true для замены списков разрешённых IP

Пользовательская реализация WireGuard отвечает:

errno=0
{empty line}

Практические примеры использования

Настройка интерфейса через командную строку

# Запуск пользовательской реализации
# wireguard-go wg0

# Настройка с использованием wg(8)
# wg set wg0 private-key /etc/wireguard/private.key listen-port 51820
# wg set wg0 peer xTIBA5rboUvnH4htodjb6e697QjLERt1NAB4mZqp8Dg= allowed-ips 10.0.0.2/32 endpoint 192.95.5.69:51820
# ip address add dev wg0 10.0.0.1/24
# ip link set up dev wg0

Использование с конфигурационным файлом

# Создаём конфигурационный файл /etc/wireguard/wg0.conf
# wg setconf wg0 /etc/wireguard/wg0.conf
# wg-quick up wg0

Получение статуса

# wg show wg0

Преимущества пользовательской реализации

  1. Кроссплатформенность — работает на Windows, macOS, BSD и других системах без поддержки в ядре
  2. Простота отладки — ошибки легче отслеживать в пользовательском пространстве
  3. Гибкость — можно интегрировать в приложения как библиотеку
  4. Безопасность — изоляция от ядра может быть преимуществом в некоторых сценариях

Недостатки пользовательской реализации

  1. Производительность — обычно медленнее, чем реализация в ядре
  2. Дополнительные накладные расходы — копирование данных между ядром и пользовательским пространством
  3. Потребление памяти — больше использования памяти для хранения состояния

Ссылки