О WireGuard

2020-11-25

Полку VPN прибыло. WireGuard наконец стал достаточно стабильным, чтобы его можно было использовать обычным пользователям Ubuntu 20.04, или Debian 10 (подключив buster-backports), или OpenWrt 19.07 без необходимости компилировать и патчить ядро.

WireGuard logo

WireGuard написан с нуля. WireGuard является модулем ядра и работает в ядре, поэтому, для VPN, он чудовищно быстр, если кому-то это важно. WireGuard работает только через UDP, что для VPN, в общем-то, более правильно. Ибо нефиг весь этот хлам, что мы через VPN обычно передаём, пытаться впихнуть в последовательный и надёжный TCP. WireGuard работает только на сетевом уровне модели OSI, а значит, прокинуть Ethernet с его помощью не получится.

WireGuard весьма отличается от «привычных» VPN вроде OpenVPN. По духу и форме он, скорее, ближе к Tinc. Здесь тоже не соединяются две точки (point-to-point), а соединяются сети, и настраивается маршрутизация между сетями. Только, в отличие от Tinc, WireGuard не умеет самоорганизовывать mesh сеть. Поэтому, если вам нужно соединить несколько сетей/датацентров через WireGuard, все возможные линки, кто к кому подключается, все короткие маршруты нужно будет прописать в конфиге WireGuard явно.

Устанавливается WireGuard в Debian/Ubuntu очевидно просто:

# apt install wireguard

Дальше нужно нарисовать конфиг. В отличие от Tinc, где каждый узел сети описывается своим отдельным файлом, что на первый взгляд кажется излишеством, но на деле оказывается очень удобным и SOLIDным, в WireGuard вся сеть описывается в одном файле. И конфигурация локального узла, и параметры всех других узлов, всё в одном файле. Соответственно, при добавлении новых узлов в сеть, этот файл придётся править на всех узлах.

Файл конфига должен лежать в /etc/wireguard. Имя файла будет именем создаваемого интерфейса. Например, /etc/wireguard/wg0.conf.

Конфигурация локального узла описывается в секции [Interface]. Как-то так:

[Interface]
Address = 10.123.0.1/24
ListenPort = 51820
PrivateKey = EOYQ+i2ASAzYt4FnBtP9uLcxcnlCMR1QzTJRnCLRFVc=

На самом деле этот файл будет читаться сервисом wg-quick, который уже запускает wg. Описания параметров конфигурации размазаны между ман-страницами этих двух программок и не блещут подробностями. И это вся официальная документация.

Address — это адрес и маска в CIDR нотации, которые будут назначены локальному VPN интерфейсу. По сути это адреса, доступные в этом сегменте VPN сети. Можно вписать сюда IP адрес, совпадающий с адресом на другом интерфейсе на этом узле, но с другой маской, как мы делали с Tinc. Если же мы делаем «классический» point-to-point VPN, то нам нужно выделить свою отдельную сеть для VPN, и назначить на наш интерфейс уникальный адрес в этой сети.

ListenPort — это UDP порт, который будет слушать WireGuard на этом узле. Если к этому узлу будут подключаться другие узлы, порт необходимо указать, чтобы другие узлы знали, куда подключаться. Если же узел работает исключительно как «клиент», то есть не ждёт подключений от других узлов, а только сам подключается, то порт можно не указывать, тогда он будет выбран случайно. Как правило, WireGuard вешают на порт 51820 или порты с соседствующими номерами. Но стандарта тут нет.

PrivateKey — приватный ключ этого узла. Все узлы в WireGuard аутентифицируются публичными ключами. А вот приватный ключ каждый узел хранит только у себя. В точности как в ssh. Вот приватный ключ прописывается здесь. Это не RSA, а эллиптические кривые, поэтому ключ выглядит таким коротким. Тут должна быть ссылка на описание протокола.

Пара ключей генерируется заранее с помощью wg. Из приватного ключа извлекается публичный ключ (как в ssh). В официальной документации применяется такое комбо для создания пары ключей и сохранения приватного и публичного ключа в разные файлы:

$ wg genkey | tee privatekey | wg pubkey > publickey

Эти файлы не читаются WireGuard. Это лишь, чтобы вы могли скопипастить ключи в конфиги.

Никаких скриптов по настройке интерфейса писать не надо, но можно. Для этого есть параметры PreUp, PostUp, PreDown, PostDown, куда можно вписать любые шелловые команды, которые будут выполняться при поднятии и гашении интерфейса. Например, включать NAT можно здесь.

В секции [Interface] вроде всё. wg-quick сделает всё сам: создаст интерфейс, навесит на него адрес. Что-то подобное будет делать и интерфейс с протоколом "WireGuard VPN" в OpenWRT или VPN подключение в Network Manager. Везде нужен приватный ключ, опциально порт, и адрес для интерфейса.

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

Вот описание пира для «сервера»:

[Peer]
PublicKey = t9C2mFsp4KYNaYLzkwaV6OOtJ+8PsTax7tNb302xWyo=
AllowedIPs = 10.123.0.2/32

PublicKey — публичный ключ пира. У пира есть свой приватный ключ. Но его он никому не расскажет. А вот его публичный ключ нужно вписать сюда. Как authorized_keys в ssh.

AllowedIPs — это сети, которые находятся за этим пиром. Это правила маршрутизации для WireGuard. В случае простого point-to-point «клиента» тут будет единственный (маска /32) адрес того узла в нашей VPN сети. В случае линка между датацентрами тут нужно будет прописать сеть того датацентра.

Для «клиентского» подключения на стороне «сервера» достаточно знать публичный ключ и сеть. И этот «серверный» узел будет посылать пакеты с адресом назначения, попадающем в указанную сеть, на этот пир.

На стороне «клиента» описание «серверного» пира будет несколько длиннее:

[Peer]
PublicKey = 4GdvenN3YqecE69U6p3L0KsFSxjnURTnJvxKrpaf2Bs=
Endpoint = my-vpn.example.net:51820
AllowedIPs = 0.0.0.0/0

Здесь PublicKey — это публичный ключ нашего «сервера».

Endpoint — это адрес нашего «сервера», как он виден в Интернетах. И порт, который мы явно указали в конфиге «сервера». «Клиент» будет пытаться «подключиться» именно к этому адресу и порту.

«Клиент» хочет направлять весь свой трафик, то есть ходить в Интернеты, через VPN канал. Поэтому в AllowedIPs стоит сеть, покрывающая все IPv4 адреса.

Я пишу «клиент» и «сервер» в кавычках, потому что в WireGuard все узлы равнозначны. Разница только в том, какой узел к какому для установления сессии подключается. У одного из узлов должен быть постоянный IP адрес (или доменное имя), и фиксированный порт для подключения, прописанный в его конфиге. Это и будет «сервер». И в том, куда маршрутизируется трафик. Если нужен выход из VPN сети в Интернеты, какой-то из узлов может быть объявлен «выходным», и весь трафик будет направляться туда.

Клиент-сервер в исполнении WireGuard

Когда конфиг /etc/wireguard/wg0.conf у нас готов, можно запустить wg-quick как-то так:

# systemctl enable wg-quick@wg0
# systemctl start wg-quick@wg0

Проверить, как оно работает, можно командой wg без аргументов:

# wg
interface: wg0
  public key: 4GdvenN3YqecE69U6p3L0KsFSxjnURTnJvxKrpaf2Bs=
  private key: (hidden)
  listening port: 51820

peer: t9C2mFsp4KYNaYLzkwaV6OOtJ+8PsTax7tNb302xWyo=
  endpoint: 188.232.130.66:56594
  allowed ips: 10.123.0.2/32
  latest handshake: 13 seconds ago
  transfer: 360.42 KiB received, 1.04 MiB sent

Как такового «подключения» в WireGuard нет, UDP ведь. Хотя предварительный handshake, для создания сессионных ключей, существует. Если вы запускаете WireGuard из Network Manager, он радостно будет рапортовать об успешном подключении, если вы не совсем уж сильно накосячили в настройках. А вот пакеты могут не ходить, UDP ведь. Так что будьте готовы отлаживать это дело с помощью tcpdump.

Для пущей секретности и стойкости криптографии в постквантовую эпоху помимо пар приватных/публичных ключей можно добавить общий ключ. Он генерируется командой wg genpsk и добавляется в виде параметра PresharedKey в секции [Peer] конфига. Соответственно, для каждой пары узлов этот общий ключ может быть свой.

На «сервере» наверняка нужно добавить NAT для пакетов, выходящих в Интернеты из VPN сети. Можно натить по диапазону адресов, как оно обычно и делается.

# iptables -t nat -L POSTROUTING -v
Chain POSTROUTING (policy ACCEPT 0 packets, 0 bytes)
 pkts  bytes  target      prot  opt  in   out   source      destination
 4212   242K  MASQUERADE  all   --   any  ens3  10.0.0.0/8  anywhere

В более сложных случаях может помочь параметр FwMark, который задаётся в секции [Interface]. Он задаёт метку пакетов (32-битное число) для исходящих пакетов WireGuard. По этой метке к пакетам можно применить дополнительные правила фильтрации или даже применить к ним другую таблицу маршрутизации (да, в Линуксе таблиц маршрутизации может быть много).

Ну и не забудьте включить пересылку пакетов.

«Клиент» может находиться за NAT. Чтобы ему (этому NATу) лучше жилось, можно добавить параметр PersistentKeepalive в секцию [Peer]. Тогда, при отсутствии сетевой активности в течение указанного количества секунд, будет передан пустой пакет. И NAT не выкинет наше VPN подключение из своей таблицы трансляции по таймауту.

WireGuard существует и для Android. И он работает. Интерфейс и пиры можно задать руками, импортировать из написанного файла конфигурации или отсканировать QR код. Этот QR код можно нарисовать в консоли из того же самого файла конфигурации вот такой командой: qrencode -t ansiutf8 -r client.conf.

Получается, что файл конфигурации — это самое универсальное представление конфигурации каждого узла. Это полезно, если конфигурацию нужно хранить где-нибудь в системе контроля версий или заготавливать и передавать множеству клиентов.

WireGuard мне нравится. Он достаточно просто настраивается. Уж точно проще, чем авторизация по ключам в OpenVPN. Он достаточно мощный, чтобы и датацентры связывать, и одиночных клиентов подключать. Он весьма быстр. Он вполне универсален. Буду пользовать.