Настройка WireGuard VPN на собственном сервере

Опубликовано 14 мин. чтения

От пустого сервера до работающего туннеля WireGuard: пары ключей, NAT на nftables, wg-quick как служба, QR-код для телефона и три типичные проблемы, на которых всё действительно застревает.

Что во всех четырёх дистрибутивах одинаково, а что нет

WireGuard прочно сидит в ядре Linux начиная с версии 5.6. Поэтому в Debian 13, Debian 12, Ubuntu 24.04 и Ubuntu 22.04 вам больше не нужно ни собирать модуль DKMS, ни подключать репозиторий backports, ни импортировать чужой ключ. Кроме того, все четыре системы поставляют одну и ту же версию пользовательских утилит (upstream-версия 1.0.20210914), то есть команды wg и wg-quick ведут себя везде одинаково.

Различия касаются исключительно окружения, и именно на нём спотыкается большинство инструкций:

  • Пакетный фильтр: wireguard-tools рекомендует nftables или iptables. Так как apt берёт первую доступную альтернативу, на минимальной установке Debian оказывается nftables, а не iptables. Переписанные из инструкции в инструкцию строки iptables -t nat -A POSTROUTING там просто не сработают, пока вы не доустановите нужный пакет.
  • Передача DNS: для строки DNS = утилита wg-quick упрямо вызывает программу resolvconf. В Ubuntu пакет systemd-resolved приносит с собой слой совместимости, а на минимальной установке Debian никакого resolvconf часто нет вообще. При доустановке нужный пакет в Debian и в Ubuntu 22.04 называется openresolv, а в Ubuntu 24.04 такого пакета больше нет. Это касается только Linux-клиентов, но не телефонов.
  • Надстройка над файрволом: в образах Ubuntu часто уже активен ufw, в Debian, как правило, нет. ufw по умолчанию блокирует пересылку пакетов, даже если net.ipv4.ip_forward установлен в 1.

Сначала проверьте, с чем вы имеете дело:

apt-get update
apt-get install -y wireguard wireguard-tools nftables qrencode
wg --version
apt-cache policy wireguard-tools

Если wg --version выводит номер версии, пользовательские утилиты на месте. А поддерживает ли WireGuard само ядро, станет понятно только при первом wg-quick up. Если при этом остаётся сообщение RTNETLINK answers: Operation not supported или Unable to access interface: Protocol not supported, значит работает ядро без поддержки WireGuard: обычно это либо очень старое, либо сильно урезанное ядро контейнера.

Создание пар ключей без будущих проблем

WireGuard не знает ни имён пользователей, ни сертификатов. На каждого участника приходится ровно одна пара ключей, к ней при желании добавляется общий preshared key как дополнительный симметричный слой. Создавайте и то и другое с заданной umask, иначе закрытые ключи будут лежать доступными на чтение всем подряд:

umask 077
wg genkey | tee /etc/wireguard/server.key | wg pubkey > /etc/wireguard/server.pub
wg genkey | tee /etc/wireguard/telefon.key | wg pubkey > /etc/wireguard/telefon.pub
wg genpsk > /etc/wireguard/telefon.psk
ls -l /etc/wireguard

Каталог /etc/wireguard создавать вручную не нужно, пакет wireguard-tools уже приносит его с правами 0700. Важнее особенность umask: значение действует только в том сеансе оболочки, в котором вы его задали. Если ключи создаются в два захода, например потому что соединение оборвалось и вы зашли на сервер заново, дальше работает уже маска по умолчанию, и файлы server.key и telefon.psk окажутся на диске с правами 0644. Поэтому в конце задайте права ещё раз явно:

chmod 600 /etc/wireguard/*.key /etc/wireguard/*.psk

Три ошибки повторяются здесь снова и снова:

  1. Перепутаны открытый и закрытый ключи. Оба представляют собой строки Base64 длиной 44 символа и выглядят совершенно одинаково. Если в [Interface] PrivateKey случайно попал открытый ключ, туннель всё равно поднимется, но handshake не состоится никогда. Проверить это можно в любой момент: wg pubkey < /etc/wireguard/server.key должен дать в точности содержимое server.pub.
  2. Скопирован лишний перевод строки. Ключи, скопированные мышью из терминала, охотно тянут за собой пробелы. wg отвечает на это сообщением Key is not the correct length or format.
  3. Права на файлы. Если забыть про umask 077, при запуске wg-quick предупредит: Warning: `/etc/wireguard/wg0.conf' is world accessible. Это не косметика, в этом файле закрытый ключ лежит открытым текстом.

Конфигурация сервера

Сначала выясните имя внешнего сетевого интерфейса. На виртуальных машинах он в зависимости от образа называется eth0, ens3 или enp1s0:

ip route show default

Затем создайте файл /etc/wireguard/wg0.conf. Сеть туннеля стоит выбрать такую, которая не встретится вам в дороге в гостиничном Wi-Fi, то есть лучше не 192.168.0.0/24 и не 192.168.1.0/24:

[Interface]
Address = 10.8.0.1/24
ListenPort = 51820
PrivateKey = СОДЕРЖИМОЕ_SERVER_KEY

PostUp = nft add table ip wgnat
PostUp = nft add chain ip wgnat postrouting '{ type nat hook postrouting priority srcnat; policy accept; }'
PostUp = nft add rule ip wgnat postrouting ip saddr 10.8.0.0/24 oifname "eth0" masquerade
PostDown = nft delete table ip wgnat

[Peer]
# телефон
PublicKey = СОДЕРЖИМОЕ_TELEFON_PUB
PresharedKey = СОДЕРЖИМОЕ_TELEFON_PSK
AllowedIPs = 10.8.0.2/32

Замените eth0 на ваш реальный интерфейс. Если вы предпочитаете остаться на iptables, доустановите пакет iptables и используйте вместо этого:

PostUp = iptables -t nat -A POSTROUTING -s 10.8.0.0/24 -o eth0 -j MASQUERADE
PostDown = iptables -t nat -D POSTROUTING -s 10.8.0.0/24 -o eth0 -j MASQUERADE

Две детали, о которых упоминают редко. Первая: каждая строка PostUp выполняется через оболочку, и если хотя бы одна из них завершается ошибкой, wg-quick откатывает весь запуск целиком. Опечатка в правиле nft приводит поэтому не к наполовину работающему туннелю, а к его полному отсутствию. Вторая: на стороне сервера AllowedIPs означает совсем не то, что на стороне клиента. Здесь это таблица соответствия, какой адрес отправителя относится к какому peer. Если у двух peer указать один и тот же адрес, победит загруженный последним, а второй будет молчать. Каждый клиент получает ровно одну /32.

Перед запуском выставьте права и проверьте синтаксис:

chmod 600 /etc/wireguard/wg0.conf
wg-quick strip wg0

wg-quick strip выводит конфигурацию без строк, которые понимает только сам wg-quick. Если команда отработала до конца, формально с файлом всё в порядке. Если она сообщает wg-quick: Line unrecognized, чаще всего какая-то опция попала не в тот раздел, например DNS в [Peer].

Постоянное включение пересылки IP-пакетов

Без пересылки каждый пакет заканчивает свой путь на сервере. Включается она сразу и навсегда:

echo 'net.ipv4.ip_forward=1' > /etc/sysctl.d/99-wireguard.conf
sysctl --system
sysctl net.ipv4.ip_forward

Последняя команда должна вывести net.ipv4.ip_forward = 1. Если вы хотите пропускать через туннель ещё и IPv6, в тот же файл добавляется строка net.ipv6.conf.all.forwarding=1.

Если работает ufw, этого ещё недостаточно. ufw задаёт собственную политику пересылки, которая действует независимо от переключателя в ядре. Откройте порт и разрешите прохождение трафика явно:

ufw allow 51820/udp
ufw route allow in on wg0 out on eth0

Типичная картина при забытой пересылке особенно коварна: handshake проходит, ping до 10.8.0.1 проходит, но всё, что находится за сервером, недоступно. Тот, кто смотрит только на handshake, часами ищет причину не в том месте.

Настройка wg-quick как службы

Пакет приносит с собой шаблонный юнит, имя после @ совпадает с именем файла конфигурации без расширения:

systemctl enable wg-quick@wg0
systemctl start wg-quick@wg0
systemctl status wg-quick@wg0
journalctl -u wg-quick@wg0 -n 50 --no-pager

Ловушка, которую многие замечают лишь спустя недели: SaveConfig = true. Эта опция при остановке службы записывает текущее состояние обратно в файл. При этом теряются все комментарии, исходный порядок строк PostUp и вся структура, расставленная руками. Для сервера, конфигурацию которого вы ведёте вручную, эту опцию лучше не задавать.

После запуска проверяйте состояние не по коду возврата, а по самому интерфейсу:

wg show
ip -brief address show wg0
ss -ulpn

ss -ulpn должен показать слушающий сокет на UDP-порту 51820. wg show перечисляет peer, пока ещё без handshake.

Конфигурация клиента и QR-код для телефона

Файл клиента удобнее всего создать прямо на сервере, потому что все ключи и так лежат там. Внимание: на стороне клиента AllowedIPs означает другое, а именно какие адреса назначения должны идти через туннель. Запись 0.0.0.0/0, ::/0 означает: всё.

mkdir -p /etc/wireguard/clients

Содержимое файла /etc/wireguard/clients/telefon.conf:

[Interface]
PrivateKey = СОДЕРЖИМОЕ_TELEFON_KEY
Address = 10.8.0.2/32
DNS = 9.9.9.9, 149.112.112.112

[Peer]
PublicKey = СОДЕРЖИМОЕ_SERVER_PUB
PresharedKey = СОДЕРЖИМОЕ_TELEFON_PSK
AllowedIPs = 0.0.0.0/0, ::/0
Endpoint = ВАШ_АДРЕС_СЕРВЕРА:51820
PersistentKeepalive = 25

PersistentKeepalive = 25 для телефонов не роскошь. NAT в мобильных сетях часто забывает UDP-привязки уже через 30-60 секунд, и без keepalive сервер больше не может сам обратиться к установленному соединению.

QR-код создаётся прямо в терминале:

qrencode -t ansiutf8 < /etc/wireguard/clients/telefon.conf

В приложении WireGuard нажмите на плюс, выберите импорт по QR-коду и наведите камеру на терминал. Два практических замечания: уменьшите шрифт в терминале, прежде чем выводить код, иначе он не поместится в кадр. И не удаляйте файл сразу после этого, он понадобится вам при смене устройства. Если файл всё же удалён, придётся создавать новую пару ключей, потому что закрытый ключ из открытого обратно не вычисляется.

Как понять, что всё действительно работает

То, что systemctl start завершился без вывода, говорит лишь о существовании интерфейса. Настоящая проверка состоит из трёх ступеней, и проходить их стоит именно в таком порядке:

wg show wg0 latest-handshakes
wg show wg0 transfer

Ступень первая, handshake. latest-handshakes выводит по одной метке времени Unix на каждый peer. Если там стоит 0, соединение не устанавливалось ещё ни разу. В подробном выводе wg show вместо этого читается строка latest handshake: 42 seconds ago.

Ступень вторая, поток данных. В выводе transfer указаны принятые и отправленные байты. Только отправленные байты без принятых означают: ваши пакеты уходят, но обратно ничего не приходит. Это почти всегда файрвол или неверный endpoint, но не проблема с ключами.

Ступень третья, фактический маршрут. На клиенте проверьте, попадает ли трафик в туннель вообще:

ip route get 1.1.1.1

Если там указано dev wg0, маршрутизация в порядке. Только после этого имеет смысл заглянуть на страницу, которая показывает ваш публичный IP-адрес. Если она показывает адрес вашего сервера, работа закончена.

Диагностика: нет handshake

Самая частая проблема из всех. Сначала включите журналирование модуля ядра, ответ оно обычно даёт за десять секунд:

echo 'module wireguard +p' > /sys/kernel/debug/dynamic_debug/control
dmesg -w

Теперь запустите на клиенте попытку подключения и читайте вывод. Три сообщения, которые здесь важны:

  • wireguard: wg0: Handshake for peer 1 (...) did not complete after 5 seconds, retrying (try 2) и больше ничего. До сервера не доходит ни один пакет. Проверьте встречно командой tcpdump -n -i eth0 udp port 51820. Если и там пусто, дело в файрволе перед сервером, в порте или в том, что клиент сидит в сети, которая фильтрует исходящий UDP. Проверка из мобильной сети вместо корпоративного Wi-Fi чётко разделяет эти случаи.
  • wireguard: wg0: Invalid handshake initiation from .... Пакеты приходят, но не подходят. Почти всегда в файле клиента указан неверный открытый ключ сервера либо preshared key прописан только на одной стороне. Preshared key должен быть либо одинаковым на обеих сторонах, либо отсутствовать на обеих.
  • Никаких сообщений вообще, хотя tcpdump показывает пакеты. Значит, WireGuard слушает другой порт или другой адрес. Проверка через ss -ulpn.

Редко описываемый особый случай — системное время. WireGuard вкладывает в первое сообщение handshake метку времени, а сервер запоминает по каждому peer наибольшее из когда-либо виденных значений. Более старые метки он отбрасывает, это защита от повторного воспроизведения. Если устройство однажды подключилось с часами, переведёнными далеко вперёд, после исправления времени оно больше не пройдёт. Состояние сервер держит в памяти, поэтому помогает следующее: выставить правильное время, затем на сервере выполнить wg-quick down wg0 и wg-quick up wg0. После пересоздания интерфейса блокировка снимается.

После этого выключите журналирование обратно, оно весьма многословно:

echo 'module wireguard -p' > /sys/kernel/debug/dynamic_debug/control

Диагностика: не работает DNS

Симптом: туннель поднят, ping 1.1.1.1 проходит, но ни одно имя не разрешается. Причин ровно три.

Первая: резолвер недоступен. Если вы указываете DNS = 10.8.0.1, на сервере по этому адресу действительно должен слушать сервер имён. В чистом Debian или Ubuntu его нет. Либо вы указываете публичный резолвер, доступный через туннель, либо поднимаете собственный. Для второго пути достаточно dnsmasq и небольшого файла /etc/dnsmasq.d/wireguard.conf:

interface=wg0
bind-dynamic
no-resolv
server=9.9.9.9
server=1.1.1.1
cache-size=1000

bind-dynamic важен потому, что на момент запуска dnsmasq интерфейс wg0 может ещё не существовать. no-resolv в Ubuntu обязателен: без этой строки dnsmasq читает /etc/resolv.conf, находит там заглушку 127.0.0.53 от systemd-resolved и замыкает запросы в петлю. И то, что в Ubuntu кусается чаще всего: dnsmasq занимает порт 53. В системе с активным systemd-resolved этот порт уже занят, обе службы конфликтуют, и dnsmasq не запускается. Поэтому строки interface=wg0 и bind-dynamic нужны не для красоты, они не дают dnsmasq привязаться ко всем адресам сразу. После этого проверьте через ss -ulpn, что dnsmasq и systemd-resolved не спорят за порт 53.

Вторая: резолвер лежит вне AllowedIPs. Если вместо 0.0.0.0/0 через туннель отправляются только отдельные сети, а в качестве DNS-сервера указан адрес, которого в этом списке нет, запросы уходят мимо туннеля в локальную сеть.

Третья, только на Linux-клиентах: отсутствует resolvconf. Строку DNS = утилита wg-quick передаёт программе с именем resolvconf. Если её нет, запуск прерывается сообщением /usr/bin/wg-quick: line 32: resolvconf: command not found. Сначала проверьте, есть ли эта программа вообще:

command -v resolvconf

При доустановке дистрибутивы расходятся, и именно на этом ломаются переписанные откуда-то инструкции в Ubuntu 24.04. Там пакета openresolv нет ни в main, ни в universe, и вызов заканчивается сообщением E: Package 'openresolv' has no installation candidate:

СистемаПодходящий пакет
Debian 11, Debian 12, Debian 13apt-get install -y openresolv
Ubuntu 22.04apt-get install -y openresolv
Ubuntu 24.04apt-get install -y resolvconf

В Ubuntu 24.04 resolvconf является виртуальным пакетом, который однозначно разрешается в systemd-resolved и при этом создаёт /usr/sbin/resolvconf, то есть ровно тот исполняемый файл, который wg-quick вызывает для строки DNS. Там вы с тем же успехом можете установить systemd-resolved напрямую. Если нужна одна строка, работающая на всех перечисленных системах, подойдёт такая:

apt-get install -y openresolv || apt-get install -y resolvconf

В Ubuntu 24.04 встречается разновидность этой проблемы, которая всплывает после обновления с 22.04: Failed to resolve interface "tun.wg0": No such device. Причина в оставшемся от 22.04 старом пакете resolvconf вместе с файлом /etc/resolvconf/interface-order, то есть не в виртуальном пакете с тем же именем из 24.04. Опираясь на этот файл, wg-quick добавляет к имени интерфейса префикс tun., с которым слой совместимости от systemd-resolved уже ничего сделать не может. Решение: удалить старый пакет, чтобы остался только слой совместимости от systemd-resolved.

Диагностика: проблемы с MTU

Самая неприятная картина, потому что внешне всё выглядит рабочим. Handshake есть, ping идёт, SSH работает, но сайты открываются наполовину и зависают, крупные загрузки обрываются, и страдает как раз HTTPS. Причина: маленькие пакеты проходят, большие нет.

Если туннель идёт поверх IPv4, WireGuard добавляет к каждому пакету 60 байт (20 байт IP, 8 байт UDP, 32 байта WireGuard), а поверх IPv6 — 80 байт. Поэтому wg-quick просто вычитает 80 байт из определённой им MTU пути и на обычном маршруте с 1500 получает 1420. Это сознательно консервативно и в большинстве случаев верно.

Неверно это тогда, когда маршрут уже, чем 1500: например, при DSL с PPPoE (1492), за ещё одним туннелем или в некоторых мобильных сетях. Измерьте фактическую MTU пути с клиента до публичного адреса сервера, с установленным битом Don't Fragment и без туннеля:

ping -M do -s 1472 -c 3 АДРЕС_НАЗНАЧЕНИЯ

1472 плюс 28 байт заголовков дают 1500. Если в ответ приходит ping: local error: message too long, mtu=... или Frag needed and DF set, понижайте значение шаг за шагом, пока пакет не пройдёт: 1464, 1444, 1414, 1372. К найденному значению прибавьте 28 и вычтите 80. Для 1464 это даёт MTU пути 1492 и MTU туннеля 1412.

Записывается это в разделе [Interface] на той стороне, у которой возникает проблема:

MTU = 1412

Быстрая встречная проверка, прежде чем долго считать: задайте для пробы MTU = 1280. Это наименьшая MTU, которую гарантирует IPv6, и работает она практически везде. Если страницы с ней загружаются нормально, дело было в MTU, и оптимальное значение вы спокойно подберёте позже. Если проблема осталась, причина в другом.

На самом сервере неверная MTU встречается реже, но тоже возможна: если там задано значение больше, чем позволяет маршрут, последствия проявятся только для отдельных адресов назначения. Текущее заданное значение показывают команды ip -brief address show wg0 и ip link show wg0.

Изменение peer на работающем сервере без разрыва всех соединений

Рефлекс набирать после каждого изменения systemctl restart wg-quick@wg0 обрывает все существующие соединения и заново строит правила NAT. Для сервера с несколькими пользователями это излишне грубо. WireGuard умеет сверять конфигурацию на ходу:

wg syncconf wg0 <(wg-quick strip wg0)

Команда сравнивает файл с текущим состоянием и меняет только различия. Уже подключённые peer сохраняют свою сессию. Учтите, что подстановка процесса через <(...) требует bash или zsh, в чистом sh она не сработает. Отдельный peer можно добавить и напрямую:

wg set wg0 peer ОТКРЫТЫЙ_КЛЮЧ allowed-ips 10.8.0.3/32

Это изменение живёт только в памяти. Внесите его дополнительно в файл конфигурации, иначе после следующей перезагрузки peer исчезнет. Кстати, это самая частая причина фразы «вчера же работало».

Если вы хотите держать точку подключения WireGuard постоянно и на стабильном адресе, собственный сервер будет для этого естественной основой. В KernelHost KVM root-серверы и выделенные серверы работают в дата-центре maincubes во Франкфурте-на-Майне (TÜV TIER3+) в нашей собственной сети, по модели PrePaid и без минимального срока. В продолжение темы могут быть полезны наши статьи о защите SSH и о настройке ufw.

Частые вопросы

Какая версия WireGuard в Debian 13, Debian 12, Ubuntu 24.04 и Ubuntu 22.04?
Все четыре системы поставляют одну и ту же upstream-версию утилит, 1.0.20210914, каждая со своей ревизией дистрибутива. Сам модуль WireGuard входит в ядро начиная с версии 5.6, и собирать его отдельно не нужно. Команды wg и wg-quick ведут себя на всех четырёх системах одинаково, различия касаются пакетного фильтра (nftables или iptables), resolvconf и ufw. При доустановке resolvconf нужный пакет в Debian и в Ubuntu 22.04 называется openresolv, а в Ubuntu 24.04 пакета openresolv больше нет, и вы устанавливаете resolvconf.
Почему строки с iptables из многих инструкций не работают в Debian?
Пакет wireguard-tools рекомендует nftables или iptables как альтернативу. apt устанавливает первую доступную альтернативу, то есть nftables. На минимальной установке Debian iptables при этом вообще отсутствует, строка PostUp завершается ошибкой, а это прерывает весь запуск wg-quick. Либо используйте вариант с nft, либо явно доустановите iptables.
Handshake проходит, но выйти в интернет не получается. В чём причина?
Проверяйте в таком порядке: net.ipv4.ip_forward должен быть равен 1, правило NAT должно указывать правильный исходящий интерфейс (его показывает ip route show default), а при активном ufw дополнительно нужна команда ufw route allow in on wg0 out on eth0. Одного переключателя в ядре при ufw недостаточно, потому что ufw задаёт собственную политику пересылки.
Как распознать проблему с MTU?
Типичная картина такая: туннель поднят, ping и SSH работают, но сайты открываются лишь наполовину, а крупные загрузки обрываются. Задайте для пробы MTU = 1280 в разделе [Interface] на клиенте. Если проблема исчезла, дело было в MTU. Оптимальное значение определяется командой ping -M do до адреса сервера: понижайте размер полезной нагрузки, пока пакет не пройдёт, затем прибавьте 28 и вычтите 80.
Нужен ли собственный DNS-сервер, чтобы DNS работал в туннеле?
Нет. В конфигурации клиента достаточно указать публичный резолвер, который будет доступен через туннель. Собственный резолвер на сервере (например, dnsmasq на wg0) имеет смысл, если вы хотите разрешать внутренние имена или кешировать запросы. Важно только одно: если вы указываете DNS = 10.8.0.1, по этому адресу действительно должен слушать сервер имён.
Почему устройство перестаёт подключаться после перевода часов?
WireGuard защищается от повторного воспроизведения меткой времени в первом сообщении handshake. Сервер запоминает по каждому peer наибольшее увиденное значение и отбрасывает более старые. Если устройство однажды подключилось с часами из будущего, после исправления времени его отклоняют. Состояние лежит в памяти, поэтому помогает wg-quick down wg0 и wg-quick up wg0 на сервере.

WireGuard VPN Debian Ubuntu nftables Сеть Руководство