WireGuard между серверами: приватная сеть на два конфига
Туннель точка-точка, чтобы база, панель и ноды общались по приватным адресам, а не торчали портами в интернет.
Типичная картина: панель на одном сервере, база или бот на другом, и порт PostgreSQL открыт всему интернету «временно, до выходных». WireGuard решает это двумя файлами по десять строк. Сложного здесь ничего нет, зато есть пара мест, где легко ошибиться и потом час смотреть на пустой wg show.
Схема
Два сервера, у обоих публичный IP:
| Сервер A (панель) | Сервер B (нода, база) | |
|---|---|---|
| Публичный IP | 203.0.113.10 | 198.51.100.20 |
| Адрес в туннеле | 10.99.0.1 | 10.99.0.2 |
| UDP-порт | 51820 | 51820 |
Подсеть туннеля выбери такую, которая не пересекается с сетями хостера и Docker.
Установка
На обоих серверах:
apt install wireguardПакет ставит утилиты wg и wg-quick. Модуль ядра в современных Debian и Ubuntu уже есть.
Ключи
На каждом сервере свой ключ. Приватный ключ никуда не копируется, между серверами передаются только публичные.
cd /etc/wireguard
umask 077
wg genkey | tee private.key | wg pubkey > public.key
chmod 600 private.key
cat public.keyumask 077 делает так, что файлы сразу создаются с правами только для root. Именно так рекомендует man wg. Публичный ключ из public.key каждого сервера понадобится в конфиге другого.
Конфиги
Сервер A
[Interface]
Address = 10.99.0.1/24
ListenPort = 51820
PrivateKey = <содержимое private.key сервера A>
[Peer]
PublicKey = <содержимое public.key сервера B>
AllowedIPs = 10.99.0.2/32
Endpoint = 198.51.100.20:51820Сервер B
[Interface]
Address = 10.99.0.2/24
ListenPort = 51820
PrivateKey = <содержимое private.key сервера B>
[Peer]
PublicKey = <содержимое public.key сервера A>
AllowedIPs = 10.99.0.1/32
Endpoint = 203.0.113.10:51820chmod 600 /etc/wireguard/wg0.confВ конфиге лежит приватный ключ. Если права шире, wg-quick при запуске предупредит: is world accessible.
Что значит каждая строка
[Interface] описывает этот сервер:
Address: адрес интерфейсаwg0в туннеле. Это ключwg-quick, а неwg: его назначает скрипт черезip.ListenPort: UDP-порт, на котором ждём пакеты. Если не указать, порт выбирается случайно, и второй стороне будет некуда стучаться.PrivateKey: приватный ключ этого сервера. Обязателен.MTU: необязательный, про него ниже.
[Peer] описывает другую сторону:
PublicKey: публичный ключ пира. По нему WireGuard понимает, кто прислал пакет. Обязателен.AllowedIPs: какие адреса «принадлежат» этому пиру. Подробно ниже, это главная грабля.Endpoint: публичный IP и порт пира. Необязателен: WireGuard сам обновляет его на адрес, с которого последний раз пришёл корректный пакет от пира. Но сторона безEndpointне может начать соединение первой и ждёт, пока пир постучится сам. Между двумя серверами с постоянными IP проще прописатьEndpointна обоих.PersistentKeepalive: раз в сколько секунд слать пустой пакет, чтобы NAT или stateful-файрвол не забыл соединение. По умолчанию выключен, иman wgпрямо пишет, что большинству он не нужен. СтавьPersistentKeepalive = 25, только если одна из сторон за NAT (домашний сервер, некоторые облака) и к ней должны приходить соединения в тишине.
AllowedIPs: это таблица маршрутизации
AllowedIPs работает в обе стороны. На отправку это маршрут: пакет на адрес из списка шифруется ключом этого пира и уходит ему. На приём это фильтр: пакет от пира с исходным адресом не из списка выбрасывается. В документации WireGuard это называется Cryptokey Routing.
Кроме того, wg-quick сам добавляет в системную таблицу маршрутов всё, что перечислено в AllowedIPs.
Не делай так
Не пиши AllowedIPs = 0.0.0.0/0 в туннеле между серверами. Для wg-quick это сигнал сделать туннель маршрутом по умолчанию, и весь трафик сервера, включая ответы твоей SSH-сессии, уйдёт в туннель. Для точка-точка нужен только адрес пира: /32.
Если за сервером B есть целая сеть, которую надо видеть с A (например, подсеть Docker), добавь её через запятую: AllowedIPs = 10.99.0.2/32, 172.20.0.0/16. На B тогда понадобится ещё и форвардинг, но это уже другая история.
Файрвол
WireGuard слушает UDP. На каждом сервере открой порт, лучше только для IP второй стороны. Для конфига из
гайда по nftables в цепочку input сервера A:
# WireGuard только от сервера B
ip saddr 198.51.100.20 udp dport 51820 accept
# трафик внутри туннеля: например, PostgreSQL
iifname "wg0" tcp dport 5432 acceptНа сервере B то же самое, но с ip saddr 203.0.113.10. Если используешь ufw:
ufw allow from 198.51.100.20 to any port 51820 proto udpГрабли
Вторая строка про wg0 не для красоты. С policy drop пакеты, пришедшие из туннеля, режутся так же, как из интернета. Туннель поднимется, handshake будет, а база отвечать не станет.
Не забудь, что сервис должен слушать адрес туннеля: база на 127.0.0.1 из wg0 не видна. Слушай на 10.99.0.1 или на всех интерфейсах, но тогда доступ снаружи режь файрволом.
Запуск
Проверочный запуск руками:
wg-quick up wg0
ping -c 3 10.99.0.2
wg showЕсли всё работает, передаём управление systemd, чтобы туннель поднимался после перезагрузки:
wg-quick down wg0
systemctl enable --now wg-quick@wg0wg-quick down перед этим обязателен: если интерфейс уже поднят руками, юнит упадёт с ошибкой already exists.
Добавил пира или поменял AllowedIPs? Не нужно ронять туннель: юнит умеет перечитывать конфиг через wg syncconf.
systemctl reload wg-quick@wg0Проверка: wg show
wg show wg0Смотри на строки у пира:
latest handshake: 1 minute, 5 seconds ago: ключи согласованы, туннель живой.transfer: ... received, ... sent: счётчики трафика.
Если строки latest handshake нет совсем, handshake не было ни разу. Учти, что WireGuard молчалив: без трафика он ничего не шлёт. Сделай ping на адрес пира и посмотри ещё раз.
Для скриптов есть машиночитаемый вывод: время последнего handshake в Unix-времени, 0, если его не было.
wg show wg0 latest-handshakesЕго удобно проверять в самописном healthcheck из гайда про мониторинг.
Нет handshake: что проверить
- UDP-порт закрыт. Файрвол на сервере, security group у хостера, или порт слушает не тот.
ss -ulpn | grep 51820покажет, слушает ли сервер. - Перепутаны ключи. В
[Peer]должен стоять публичный ключ другого сервера. Классика: вставить свой публичный или чужой приватный. Сравниwg show wg0 public-keyна B со строкойPublicKeyв конфиге A. - Неверный
Endpoint. Опечатка в IP или порте, либоEndpointнет на обеих сторонах, и никто не начинает первым. - Handshake есть, но пинг не идёт. Смотри
AllowedIPs: адрес пира должен в них входить. Затем файрвол наwg0.
MTU
По умолчанию wg-quick сам вычисляет MTU: берёт MTU маршрута до Endpoint (или маршрута по умолчанию) и вычитает 80 байт. На обычном интерфейсе с MTU 1500 получается 1420.
Руками задавать нужно, только если путь между серверами уже, чем MTU интерфейса (туннель у хостера, PPPoE). Проверить, проходит ли пакет полного размера без фрагментации, можно с DF-флагом: 1472 байта данных плюс 28 байт заголовков ICMP и IP дают 1500.
ping -M do -s 1472 -c 3 198.51.100.20Если не проходит, уменьшай -s, пока не пройдёт. Найденный размер плюс 28 минус 80 и есть MTU туннеля. Задаётся в [Interface] на обеих сторонах:
[Interface]
Address = 10.99.0.1/24
ListenPort = 51820
PrivateKey = <...>
MTU = 1380WireGuard и Россия
Важно
Всё выше про туннель между серверами за рубежом, и там WireGuard работает без сюрпризов. Как канал из России он не годится: по опыту сообщества классический WireGuard в РФ блокируют легко, зато между нодами за границей его не трогают. Выжимки и ссылки на обсуждения в базе: ТСПУ: серверная часть, приватные сети между серверами. Что сейчас работает для входа из РФ, смотри в методах обхода и хронологии блокировок.
Если туннель нужен между сервером в РФ и зарубежным, в той же базе есть отзывы про AmneziaWG и про схему с WARP. Проверяй на своём маршруте, прежде чем строить на этом прод.