Привет. Меня зовут Алексей, и я System Infrastructure Engineer в inDriver. В этой статье на конкретных кейсах объясню, почему WireGuard — отличная VPN-система для работы, в чем разница использования ее утилит, и что надо помнить, когда с ними работаешь. Прошу под кат!
С чего все началось
Исторически сложилось, что часть сервисов в inDriver находятся на арендованных серверах. Для защиты трафика между серверами в одном дата-центре мы решили зашифровать его. Изначально рассматривали следующие решения:
IPsec (strongSwan).
Tinc.
Yggdrasil на тот момент не внушал доверия.
Вот сравнительный анализ, где N — количество серверов:
Сервис |
Снижение скорости |
Количество соединений в полносвязной топологии |
Порог вхождения |
IPsec |
на 10-20% |
N-1 |
Очень высокий |
Tinc |
на 70-90% |
1 |
Средний |
WireGuard |
на 10-20% |
1 |
Низкий |
В итоге мы остановились на 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 <WG_IFACE> 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.
# ip link add wg0 type wireguard && ip link set wg0 up
# ip address add 10.255.255.1/24 dev wg0
Примечательно, что имя интерфейса можно задать любое, исходя из практики именования сетевых интерфейсов. Выходит, к этому интерфейсу применимы все действия, которые можно проделывать с любым интерфейсом (например, менять MTU). Также можно вывести только WireGuard-интерфейсы командой:
$ ip link show type wireguard
2. Не требуется использование конфигурации. Все можно настроить вручную, используя команду wg set. Параметры можно указывать по очереди или вместе.
Коротко о параметрах этой команды:
1. listen-port <port>
UDP-порт, на котором будет работать сервер WireGuard. Если не указывать, при каждом новом запуске сервиса будет выбран случайный порт. Это усложняет настройку сервера.
Важно помнить, что одновременно на одном порту может работать только один сервис! Если поднимать несколько WireGuard-подключений, каждому следует выделять отдельный порт.
2. fwmark <mark>
Маркировка исходящего трафика. Мне было лень искать в nftables правила маркировки, но для iptables правил не создается. Экспериментально выяснил, что помечаются пакеты в таблице FILTER цепочки OUTPUT. Следует помнить, что если приложение лезет своими битами в маркировку пакета (например, k8s), это может поломать нормальную работу WireGuard. Тогда следует задуматься о необходимости использования этого параметра.
3. private-key <file path>
Приватный ключ. Немного поэкспериментировав, я выяснил, что это просто массив из 32 байт, закодированный в base64. По сути, выполнение скрипта на python3:
from base64 import b64encode
from secrets import token_bytes
b64encode(token_bytes(32)).decode('utf-8')
Эквивалентно выполнению команды:
$ wg genkey
Убедиться в этом можно, передав полученный результат в команду wg pubkey (будет сформирован публичный ключ). Возможны и другие варианты: например, используя echo $RANDOM | md5sum | head -c 32 | base64. Но следует помнить, что эти варианты не всегда могут быть рабочими и достаточно безопасными.
Для того, чтобы впихнуть ключ в WireGuard и избежать создание файла, можно применить следующее:
# wg set $WG_IFACE private-key <(echo $PRIVATE_KEY)
4. peer <base64 public key>
Работа с пирами. Дополнительные параметры:
remove — удаление пира из списка. Полностью прекращает работу с данным пиром.
preshared-key <file path> — ключ для дополнительного шифрования трафика. Да, требуется наличие файла.
endpoint <ip>:<port> — показывает, с какого адреса и порта ждать подключение.
persistent-keepalive <interval seconds> — об этом чуть попозже.
allowed-ips <ip1>/<cidr1>[,<ip2>/<cidr2>]... — список сетей, которым предназначается трафик. Если в WireGuard-интерфейс попадает пакет, то пир, которому перенаправляют пакет, берется из этого параметра. Если в интерфейс попадет пакет, а ответственного пира нет, пакет никуда не пойдет. Для icmp ловил отлуп вида “Required key not available”.
Хорошо! А как сделать так, чтобы не пришлось вручную вводить информацию о пирах? Ответ: сформировать файл конфигурации. Следуя инструкциям, в нем должна быть одна секция [INTERFACE] со следующими полями:
PrivateKey — приватный ключ. Не путь к ключу, а именно его содержимое. Обязательно должно присутствовать.
ListenPort — порт, на котором работает WireGuard. Если нет, выбирается случайно каждый раз.
FwMark — маркировка исходящего трафика.
Каждый пир должен быть представлен отдельной секцией [Peer] со следующими полями (совпадающими с параметрами запуска wg set <WG_IFACE> peer...):
PublicKey — публичный ключ. Обязательно должен присутствовать.
PresharedKey — дополнительный ключ шифрования. Если нет, дополнительное шифрование не используется.
AllowedIPs — список разрешенных подсетей. Можно указывать каждую сеть через запятую, а можно несколько раз указать это поле. Если нет, никакая сеть не закреплена за пиром.
Endpoint — IP-адрес или DNS имя пира с обязательным указанием порта. Можно не указывать.
PersistentKeepalive — терпение, чуть ниже все расскажу.
Чтобы не марать руки, можно сгенерировать файл автоматически, используя метод showconf (выводит текущую конфигурацию):
# wg showconf wg0 >> /etc/wireguard/wg0.conf
Также есть команды setconf, addconf и syncconf. Об этом поподробнее:
setconf — применяет конфигурационный файл.
addconf — добавляет конфигурацию к текущей. В файле конфигурации не всегда должна присутствовать секция [Interface]. Будет выполняться wg set для каждого пира в файле.
syncconf — синхронизирует текущую конфигурацию и файл конфигурации. Применяет разницу. Если в текущей конфигурации ListenPort = 51820, а в файле он не указан, после применения получится случайный порт, так как изменилось значение. Это касается и приватного ключа! Будет выполняться wg set peer только для тех пиров, которые изменили значение.
Для просмотра информации о соединениях можно воспользоваться командой:
# wg show $WG_IFACE
Мы получаем много информации, поэтому я чаще пользуюсь командой:
# wg show $WG_IFACE dump
Чтобы после перезапуска сервиса все изменения сохранились, придется сохранять изменившуюся конфигурацию:
# wg-quick save $WG_IFACE
Заметил, что после изменений, сделанных утилитой wg служба wg-quick@$WG_IFACE перестает работать: остановка службы не происходит, а попытка запуска завершается ошибкой «Данный интерфейс существует».
Выходит, что:
Для восстановления конфигурации используется wg-quick, для изменения — wg.
В списках пиров все серверы дата-центра.
Для шлюзов дата-центра добавить в пиры другие шлюзы дата-центра и передать свою подсеть им в allowed_ips.
Все получилось — интерфейс поднялся, данные передаются. Ура!
Проблема 3. Маршрутизация и утилита wg
Все новые пиры на месте, но доступа к ним нет. Что же случилось? Все просто: утилита wg не следит за маршрутами, за ними надо следить самостоятельно!
Все надо делать самому: следить за маркировкой пакетов, маршрутами, правилами файрвола, заворачивать трафик. Для того, чтобы добавить пир через утилиту wg, нужно сделать это вручную и убедиться, что он есть в таблице маршрутизации. И после этого все заработает.
Выход был найден! Выяснилось, что скрипт:
wg show $WG_IFACE allowed-ips | awk '{ for (i=2; i<=NF; i++) print $i }' | while read ip_a;
do
[[ $ip_a != 0.0.0.0/0 ]] && ip route add $ip_a dev $WG_IFACE 2> /dev/null;
done;
Выполняет всю пыльную работу за инженера!
Значит, системе не обязательно знать, куда уходит пакет. Главное, что он падает в интерфейс WireGuard, а что, как, кому и куда — решает сам VPN! Вот она — магия WireGuard!
Выходит, что:
Есть отдельный скрипт для добавления маршрутов.
При изменении конфигурации нужно запускать скрипт добавления маршрутов.
Почти что без проблем
Ключевое слово — почти. Первая проблема возникает, если маршрут по умолчанию идет через WireGuard, а обращение — на внешний интерфейс. Объясню на пальцах:
Клиент обращается к серверу по внешнему адресу exIP.
На сервер приходит пакет на адрес exIP.
Сервер формирует ответный пакет.
Согласно таблице маршрутизации пакет отправляется… в интерфейс WireGuard.
Шлюз WireGuard пересылает пакет, используя wgIP-адрес.
Клиент получает ответ от сервера с IP-адреса wgIP и… отбрасывает его, так как он не ждет от него ответа. Ответ ожидается от exIP.
Клиент уходит в закат, не дождавшись ответа.
Ленивый инженер скажет всем клиентам отключить rp_filter, но эта история не про нас. Можно маркировать пакеты с помощью файрвола, а потом маркированные пакеты заворачивать в отдельную таблицу маршрутизации. Но я слишком ленив, чтобы ковырять еще и его.
Выход: только хардкор! Только iproute2! Достаточно заворачивать исходящий трафик с определенного адреса в отдельную таблицу маршрутизации! Для адреса 172.100.0.2 и маршрутизатора 172.100.0.1 нужно выполнить 2 действия:
# ip route add default via 172.100.0.1 table 10000;
# ip rule add from 172.100.0.2 lookup 10000;
Эти команды создадут дополнительную таблицу маршрутизации с номером 10000 и завернут весь исходящий трафик с IP-адреса 172.100.0.2 через маршрутизатор 172.100.0.1, что нам и нужно.
Это нужно добавить в скрипт маршрутизаци, чтобы правила применялись при поднятии интерфейса WireGuard.
Еще одна проблема возникает при мониторинге состояния WireGuard. Оказалось, что если сервер A не обращается какое-то время к серверу B, handshake не происходит! Как быть?
Можно повесить задачу на пинги всех пиров раз в минуту. Но это не самое лучшее решение. На помощь приходит вышеупомянутый параметр PersistentKeepalive. Примечательно, что в документации указано, что он нужен для сохранения состояния в файрволе (имеются в виду таймауты соединений) при работе через NAT:
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 будет поставляться из коробки со всеми ОС, включая мобильными. Буду изучать, как менее проблемно добавить к этой топологии пользовательские подключения.
Спасибо, что дочитали статью до конца. С радостью отвечу на ваши вопросы в комментариях.
Комментарии (14)
NikaLapka
02.11.2021 16:36+6AllowedIPs - это не "список разрешенных подсетей", а набор IP-адресов, которые локальный хост должен направлять удаленному узлу через туннель WireGuard. Этот параметр сообщает локальному хосту, что входит в туннель.
ZakharovAV Автор
02.11.2021 17:33Спасибо, вы совершенно точно сформулировали то, что у меня не вышло
XBOCTOB
02.11.2021 20:32Нет, это таки список разрешенных src к приему из wg-туннеля. Если из туннеля прилетит что-то с src не в списке - оно будет отброшено. Вполне может быть поднято несколько разных туннелей, с абсолютно идентичными списками AllowedIPs (даже состоящими только из default-а), а что в какой туннель отправлять разруливать уже на уровне таблиц маршрутизации.
vainkop
03.11.2021 00:02+2Вы разделяете утилиту wg-quick и wg, но wg-quick это, условно говоря, bash обёртка вокруг wg. Если вам чего-то не хватает в wg-quick то дописать это прямо в него достаточно просто.
https://github.com/WireGuard/wireguard-tools/blob/master/src/wg-quick/linux.bash
ZakharovAV Автор
03.11.2021 12:30Определённо это так, но могут возникнуть проблемы при обновлении или поведение приложения станет неочевидным для других специалистов.
vainkop
03.11.2021 13:15Ваша текущая связка из wg-quick для одной части задачи и wg для другой выглядит сложнее и не такой "очевидной для других специалистов". По-моему лучше дописать вторую часть в свой новый форкнутый bash и хорошенько прокомментировать его + создать хорошую документацию.
Если это будут какие-то универсальные дополнения, то можно послать PR в репозиторий самого wg-quick, тогда у всех будет ваша версия. The power of open source :)
max_im_ka
03.11.2021 10:03+1Хорошая статья для новичков, но я думаю кому нужно было то те нашли документацию, которая есть и хорошо написана
ZakharovAV Автор
04.11.2021 02:57Спасибо! Надеюсь, эта статья сможет помочь кому-нибудь разобраться с Wireguard!
ajijiadduh
а он разве ещё тогда не?
ZakharovAV Автор
Спасибо за ответ. Я имею в виду момент, когда wireguard не придётся дополнительно устанавливать.
vainkop
А в чём проблема поставить мобильный клиент? Достаточно давно пользуюсь https://play.google.com/store/apps/details?id=com.wireguard.android
Для популярности OpenVPN необходимость установки клиентов не стала препятствием.
ZakharovAV Автор
Конечно, можно установить ПО самостоятельно. Но если все основные ОС включат Wireguard в свой состав, то проблема интеграции пользовательского трафика в существующую сетевую схему станет ещё более актуальной.