# 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.

## Установка

На обоих серверах:

```bash
apt install wireguard
```

Пакет ставит утилиты `wg` и `wg-quick`. Модуль ядра в современных Debian и Ubuntu уже есть.

## Ключи

На каждом сервере свой ключ. Приватный ключ никуда не копируется, между серверами передаются только публичные.

```bash
cd /etc/wireguard
umask 077
wg genkey | tee private.key | wg pubkey > public.key
chmod 600 private.key
cat public.key
```

`umask 077` делает так, что файлы сразу создаются с правами только для root. Именно так рекомендует `man wg`. Публичный ключ из `public.key` каждого сервера понадобится в конфиге другого.

## Конфиги

### Сервер A

```ini {name="/etc/wireguard/wg0.conf"}
[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

```ini {name="/etc/wireguard/wg0.conf"}
[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:51820
```

```bash
chmod 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`.

> [!CAUTION]
> Не пиши `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](/gaidy/nftables-minimalnyy-fayrvol/) в цепочку `input` сервера A:

```bash {name="/etc/nftables.conf"}
    # 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:

```bash
ufw allow from 198.51.100.20 to any port 51820 proto udp
```

> [!WARNING]
> Вторая строка про `wg0` не для красоты. С `policy drop` пакеты, пришедшие из туннеля, режутся так же, как из интернета. Туннель поднимется, handshake будет, а база отвечать не станет.

Не забудь, что сервис должен слушать адрес туннеля: база на `127.0.0.1` из `wg0` не видна. Слушай на `10.99.0.1` или на всех интерфейсах, но тогда доступ снаружи режь файрволом.

## Запуск

Проверочный запуск руками:

```bash
wg-quick up wg0
ping -c 3 10.99.0.2
wg show
```

Если всё работает, передаём управление systemd, чтобы туннель поднимался после перезагрузки:

```bash
wg-quick down wg0
systemctl enable --now wg-quick@wg0
```

`wg-quick down` перед этим обязателен: если интерфейс уже поднят руками, юнит упадёт с ошибкой `already exists`.

Добавил пира или поменял `AllowedIPs`? Не нужно ронять туннель: юнит умеет перечитывать конфиг через `wg syncconf`.

```bash
systemctl reload wg-quick@wg0
```

## Проверка: wg show

```bash
wg show wg0
```

Смотри на строки у пира:

- `latest handshake: 1 minute, 5 seconds ago`: ключи согласованы, туннель живой.
- `transfer: ... received, ... sent`: счётчики трафика.

Если строки `latest handshake` нет совсем, handshake не было ни разу. Учти, что WireGuard молчалив: без трафика он ничего не шлёт. Сделай `ping` на адрес пира и посмотри ещё раз.

Для скриптов есть машиночитаемый вывод: время последнего handshake в Unix-времени, `0`, если его не было.

```bash
wg show wg0 latest-handshakes
```

Его удобно проверять в самописном healthcheck из гайда [про мониторинг](/gaidy/monitoring-na-kolenke/).

## Нет handshake: что проверить

1. **UDP-порт закрыт.** Файрвол на сервере, security group у хостера, или порт слушает не тот. `ss -ulpn | grep 51820` покажет, слушает ли сервер.
2. **Перепутаны ключи.** В `[Peer]` должен стоять *публичный* ключ *другого* сервера. Классика: вставить свой публичный или чужой приватный. Сравни `wg show wg0 public-key` на B со строкой `PublicKey` в конфиге A.
3. **Неверный `Endpoint`.** Опечатка в IP или порте, либо `Endpoint` нет на обеих сторонах, и никто не начинает первым.
4. **Handshake есть, но пинг не идёт.** Смотри `AllowedIPs`: адрес пира должен в них входить. Затем файрвол на `wg0`.

## MTU

По умолчанию `wg-quick` сам вычисляет MTU: берёт MTU маршрута до `Endpoint` (или маршрута по умолчанию) и вычитает 80 байт. На обычном интерфейсе с MTU 1500 получается **1420**.

Руками задавать нужно, только если путь между серверами уже, чем MTU интерфейса (туннель у хостера, PPPoE). Проверить, проходит ли пакет полного размера без фрагментации, можно с DF-флагом: 1472 байта данных плюс 28 байт заголовков ICMP и IP дают 1500.

```bash
ping -M do -s 1472 -c 3 198.51.100.20
```

Если не проходит, уменьшай `-s`, пока не пройдёт. Найденный размер плюс 28 минус 80 и есть MTU туннеля. Задаётся в `[Interface]` на обеих сторонах:

```ini {name="/etc/wireguard/wg0.conf"}
[Interface]
Address = 10.99.0.1/24
ListenPort = 51820
PrivateKey = <...>
MTU = 1380
```

## WireGuard и Россия

> [!IMPORTANT]
> Всё выше про туннель между серверами за рубежом, и там WireGuard работает без сюрпризов. Как канал из России он не годится: по опыту сообщества классический WireGuard в РФ блокируют легко, зато между нодами за границей его не трогают. Выжимки и ссылки на обсуждения в базе: [ТСПУ: серверная часть](/baza/set/tspu-servernoe/), [приватные сети между серверами](/baza/set/ssh-dostup/). Что сейчас работает для входа из РФ, смотри в [методах обхода](/baza/tspu/metody-obhoda/) и [хронологии блокировок](/baza/tspu/hronologiya/).

Если туннель нужен между сервером в РФ и зарубежным, в той же [базе](/baza/set/tspu-servernoe/) есть отзывы про AmneziaWG и про схему с WARP. Проверяй на своём маршруте, прежде чем строить на этом прод.

## Источники

- [WireGuard: Quick Start](https://www.wireguard.com/quickstart/)
- [WireGuard: Installation](https://www.wireguard.com/install/)
- [WireGuard: Cryptokey Routing](https://www.wireguard.com/#cryptokey-routing)
- [wg(8)](https://man7.org/linux/man-pages/man8/wg.8.html)
- [wg-quick(8)](https://man7.org/linux/man-pages/man8/wg-quick.8.html)
- [wireguard-tools: wg-quick/linux.bash (расчёт MTU)](https://github.com/WireGuard/wireguard-tools/blob/master/src/wg-quick/linux.bash)
- [wireguard-tools: wg-quick@.service](https://github.com/WireGuard/wireguard-tools/blob/master/src/systemd/wg-quick%40.service)
- [ping(8)](https://man7.org/linux/man-pages/man8/ping.8.html)

---
Источник: https://host.pink/gaidy/wireguard-mezhdu-serverami/
Лицензия: CC BY-SA 4.0
