Типичная заявка: «подняли WireGuard на VPS, рукопожатие есть, сайты открываются, а в офис до внутренней админки как не ходили, так и не ходят». Или наоборот: «весь интернет офиса теперь через виртуалку, YouTube тормозит, трафик на тарифе хостера улетел в космос, а до NAS в соседней комнате пакеты почему-то идут через Франкфурт».

Смотрю конфиг — почти всегда одно и то же. На пире стоит AllowedIPs = 0.0.0.0/0 «чтобы точно работало», DNS прописан 8.8.8.8, на VPS ip_forward выключен, а проверку сделали командой ping 10.66.66.1. Пинг до адреса туннеля проходит. Это доказывает только то, что два пира видят друг друга. Это не доказывает, что рабочий трафик идёт в туннель, что DNS не утекает мимо, и что обратный путь из офисной сети существует.

Ниже — как я собираю офис ↔ VPS так, чтобы маршруты, DNS и split-tunnel не врали, и чем проверяю, что пакет реально ушёл в wg0, а не красиво показал handshake в wg show. Без «поставьте GUI» и без схемы «все в 0.0.0.0/0 и будет счастье».

Два разных туннеля, которые путают в одном конфиге

Мне на стол приносят три задачи, и их пытаются решить одним [Peer]:

  • С ноутбука в офисе ходить на сервисы на VPS: SSH, панель, приватный git, админка, которая слушает только внутренний адрес.
  • С VPS ходить в офисную сеть: NAS, принтер, контроллер, камера, которой «интернет не нужен».
  • Выгнать весь офисный интернет через VPS, «чтобы как на работе».

Это не один туннель с разными галочками. Это разный AllowedIPs, разный форвардинг и разный DNS. Первые две задачи я почти всегда делаю split-tunnel: в туннель уезжает только нужная подсеть. Третья — полный туннель, и за неё отдельно платят CPU, трафиком и сломанным Zoom, если MTU не поправили.

Адреса в примерах ниже документационные, не боевые. Публичный VPS — 203.0.113.10, белый офиса — 198.51.100.20, туннель — 10.66.66.0/24, LAN офиса — 192.168.40.0/24. Свои цифры подставьте, чужие из интернета не копируйте вместе с ключами.

AllowedIPs — это маршрут, а не «список друзей»

В WireGuard AllowedIPs делает две вещи сразу. Снаружи это криптоключ: пакет с таким источником примут только от этого пира. Изнутри wg-quick по этому списку ставит маршруты. Написали 0.0.0.0/0 — получите дефолт через туннель (у wg-quick ещё и отдельная таблица, обычно 51820). Написали 10.66.66.0/24 — в туннель пойдёт только эта сеть.

Поэтому «handshake есть, а сайт на VPS открывается по белому IP» — нормально для split-tunnel и баг, если вы хотели полный туннель. Браузер пошёл на 203.0.113.10 напрямую, UDP 51820 при этом живой. Туннель тут ни при чём.

Смотрю, что система думает про маршрут, не что написано в комментарии к конфигу:

wg show
ip -4 addr show dev wg0
ip -4 route show
ip -4 route show table all | grep -E 'wg0|51820'
ip -4 route get 10.66.66.1
ip -4 route get 203.0.113.10
ip -4 route get 8.8.8.8

ip route get врёт меньше, чем ощущения. Если до внутреннего адреса VPS маршрут не через wg0, дальше можно не гадать — трафик в туннель не собирался.

Split-tunnel: офис ходит на VPS, интернет офиса не трогаем

Это тот вариант, который я ставлю по умолчанию. Офисный пир (ноутбук или маленький шлюз) держит туннель, в AllowedIPs только сеть туннеля и, если надо, приватные сети на стороне VPS. Исходящий веб офиса идёт как раньше, через местного провайдера.

VPS:

[Interface]
Address = 10.66.66.1/24
ListenPort = 51820
PrivateKey = <ключ VPS>

[Peer]
PublicKey = <ключ офиса>
AllowedIPs = 10.66.66.2/32

Офис (за NAT, поэтому keepalive):

[Interface]
Address = 10.66.66.2/24
PrivateKey = <ключ офиса>

[Peer]
PublicKey = <ключ VPS>
Endpoint = 203.0.113.10:51820
AllowedIPs = 10.66.66.0/24
PersistentKeepalive = 25

Если на VPS сервисы торчат в docker-сети или во втором внутреннем интерфейсе, в AllowedIPs офиса добавляю эту сеть, не «весь мир». И на VPS тогда уже нужен форвардинг и фильтр, иначе пакет дойдёт до ядра и умрёт в FORWARD.

На VPS для split-tunnel NAT наружу обычно не нужен. Нужен, когда офис должен ходить не на сам VPS, а через него дальше. Пока задача «SSH на 10.66.66.1» — без маскарада.

PersistentKeepalive = 25 ставлю на том конце, который за NAT или с динамическим белым. Без него UDP-маппинг у провайдера офиса схлопывается, handshake в wg show становится старше пары минут, и «туннель встал сам» ровно до первой попытки зайти с другой стороны.

Когда VPS должен видеть LAN офиса

Вторая заявка: бэкап с VPS на NAS, мониторинг контроллера, SSH на станок, который «в интернет не смотрит». Тогда на стороне VPS в пире офиса мало /32 туннельного адреса. Нужна офисная подсеть:

# на VPS, peer = офисный шлюз
[Peer]
PublicKey = <ключ офисного шлюза>
AllowedIPs = 10.66.66.2/32, 192.168.40.0/24

Офисный шлюз должен форвардить. Это уже не «WireGuard на ноутбуке бухгалтера». Ноутбук не маршрутизатор: пакет с VPS на 192.168.40.15 дойдёт до ноутбука и останется там, NAS его не увидит, а ответ NAS пойдёт в локальный шлюз офиса и умрёт, потому что шлюз про 10.66.66.0/24 ничего не знает.

На шлюзе офиса:

sysctl -w net.ipv4.ip_forward=1
# постоянное место — /etc/sysctl.d/, не «в сессии до ребута»

iptables -C FORWARD -i wg0 -o eth0 -s 10.66.66.0/24 -d 192.168.40.0/24 -j ACCEPT 2>/dev/null || \
iptables -A FORWARD -i wg0 -o eth0 -s 10.66.66.0/24 -d 192.168.40.0/24 -j ACCEPT
iptables -C FORWARD -i eth0 -o wg0 -s 192.168.40.0/24 -d 10.66.66.0/24 -j ACCEPT 2>/dev/null || \
iptables -A FORWARD -i eth0 -o wg0 -s 192.168.40.0/24 -d 10.66.66.0/24 -j ACCEPT

Имена интерфейсов свои. eth0 в статье — заглушка. На машине смотрю ip -br link, не копирую с чужого скриншота.

Обратный маршрут в LAN часто забывают. NAS отвечает на запрос с 10.66.66.1 через дефолтный шлюз офиса. Шлюз этот пакет не ждал — в лучшем случае дроп, в худшем NAT в интернет. Варианты, которые работают:

  • Статический маршрут на роутере офиса: 10.66.66.0/24 via 192.168.40.2, где .2 — шлюз с WireGuard.
  • Или маскарад на офисном WG-хосте в сторону LAN, если роутер трогать нельзя. Работает, в логах NAS все запросы с одного адреса шлюза. Для бэкапа терпимо, для разбора «кто стучится» — нет.

Пересечение подсетей ловлю сразу. Офис на 192.168.1.0/24, docker на VPS тоже 192.168.1.0/24 — туннель поднимется, маршруты будут врать по очереди. Одну из сетей надо сменить. Спорить, «чья 192.168.1 важнее», бесполезно: важнее та, которую проще перенумеровать.

Полный туннель: 0.0.0.0/0, таблица 51820 и NAT на VPS

AllowedIPs = 0.0.0.0/0 на офисном пире означает: весь IPv4, кроме того что wg-quick сам вычтет для Endpoint, должен идти в туннель. wg-quick для этого поднимает policy routing, не просто ip route add default. Поэтому «я убрал default из main, а туннель живой» — ещё не победа. Смотреть надо ip rule и таблицу, которую создал WireGuard:

ip rule
ip route show table 51820
ip route get 8.8.8.8
ip route get 203.0.113.10

До Endpoint маршрут обязан остаться в физический интерфейс. Иначе туннель пытается нести сам себя и тихо умирает после первого же пересчёта маршрутов.

На VPS для чужого интернета нужны три вещи, не одна:

  • net.ipv4.ip_forward=1
  • NAT с туннеля в внешний интерфейс, иначе провайдер VPS не пропустит чужие RFC1918 как source
  • FORWARD в файрволе в обе стороны, не только INPUT на 51820/udp
# на VPS, внешний интерфейс уточнить: ip -br addr
sysctl -w net.ipv4.ip_forward=1

iptables -t nat -C POSTROUTING -s 10.66.66.0/24 -o ens3 -j MASQUERADE 2>/dev/null || \
iptables -t nat -A POSTROUTING -s 10.66.66.0/24 -o ens3 -j MASQUERADE

iptables -C FORWARD -i wg0 -o ens3 -j ACCEPT 2>/dev/null || iptables -A FORWARD -i wg0 -o ens3 -j ACCEPT
iptables -C FORWARD -i ens3 -o wg0 -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT 2>/dev/null || \
iptables -A FORWARD -i ens3 -o wg0 -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT

Правила в статье — эскиз под iptables. На машине с nftables я пишу в ту таблицу, которая уже живая, а не добавляю второй файрвол «рядом». ufw route allow и iptables -A FORWARD одновременно — классический способ получить «у меня же разрешено», когда пакет режет другая цепочка.

Полный туннель я не ставлю «на всякий случай». Он нужен, когда офисный канал дырявый, нужен выход с фиксированного белого VPS, или политика «весь трафик только через нас». Для доступа к одной админке на VPS это дорогой способ сломать DNS, MTU и чужой Zoom.

DNS: туннель живой, а имена резолвятся мимо

Самая частая ложь после wg show. Туннель есть, curl https://10.66.66.1 работает, а curl https://panel.internal идёт неизвестно куда или не идёт вовсе. Причина почти всегда в резолвере, не в маршрутах.

DNS = в wg-quick — не магия. На systemd-resolved это попытка прикрутить домены к интерфейсу. На машине без resolved это может записаться в /etc/resolv.conf и затереть офисный DNS. На Windows-клиенте это отдельная история с «использовать DNS этого адаптера». Я не верю строке в конфиге, пока не увижу, куда реально уходит запрос.

Проверка с офисного пира:

resolvectl status
resolvectl query panel.internal
getent hosts panel.internal

# куда уходит UDP/53, пока стучите в имя
tcpdump -ni wg0 port 53
tcpdump -ni eth0 port 53

Если дамп на физическом интерфейсе показывает запросы на 8.8.8.8, а на wg0 тихо — split-tunnel сделал ровно то, что вы написали: маршрут до публичного DNS прямой. Внутреннее имя при этом либо не резолвится, либо резолвится в публичный адрес, и вы снова обходите туннель.

Рабочая схема для split-tunnel, которой я пользуюсь:

  • На VPS крутится DNS, который знает внутренние имена. Слушает 10.66.66.1, не 0.0.0.0 в интернет.
  • У офисного пира DNS = 10.66.66.1, и этот адрес уже входит в AllowedIPs.
  • Если нужен только суффикс internal, в resolved указываю домен на интерфейс туннеля, а не подменяю весь резолвер офиса.

Для полного туннеля публичный DNS тоже должен быть достижим через туннель. Иначе получаете курицу и яйцо: чтобы поднять туннель, нужен DNS Endpoint, а DNS вы уже отправили в туннель, которого нет. Поэтому Endpoint я пишу IP, не именем. Имя в Endpoint оставляю только там, где белый VPS прыгает и без DNS туннель не собрать — тогда DNS офиса должен резолвить этот хост до подъёма wg0.

IPv6 утекает отдельно, MTU ломает «сайты открываются через раз»

Настроили IPv4, про IPv6 забыли. Браузер берёт AAAA, пакеты идут мимо туннеля в родной канал. curl -4 при этом красивый. Если полный туннель — в AllowedIPs нужен и ::/0, и на VPS форвардинг/NAT6, либо IPv6 на офисном пире надо выключить осознанно, не «само как-нибудь».

Для split-tunnel я проверяю оба стека:

ip -6 route get 2001:db8::1
curl -4 -s https://ifconfig.me
curl -6 -s https://ifconfig.me

Документационный 2001:db8::1 в route get нужен, чтобы увидеть, в какой интерфейс ядро отправит «чужой» v6. Если полного туннеля нет, прямой маршрут — ожидаемо. Если полный туннель заявлен, а v6 уходит в eth0 — дыра.

MTU. UDP + WireGuard + внешний Ethernet обычно просит 1420, на части каналов — ниже. Симптом не «туннель мёртв», а «SSH живой, HTTPS висит, крупные POST отваливаются, мессенджеры отваливаются на картинках». Ловлю так:

ip link show wg0
ping -M do -s 1372 10.66.66.1
ping -M do -s 1200 10.66.66.1

Если 1372 с DF не проходит, опускаю MTU на wg0, пока не начнёт. На VPS с jumbo или с PPPoE за NAT цифры другие — подбираю, не копирую 1280 «потому что все так пишут», если канал нормально держит больше.

Проверка, что рабочий трафик в туннеле, а не только handshake

Чеклист, которым я пользуюсь после wg-quick up. По порядку, не выборочно.

# 1. пир живой
wg show
# handshake секунды-минуты, не часы; transfer растёт, когда вы сами генерируете трафик

# 2. адрес и маршрут
ip route get 10.66.66.1
# ожидаю dev wg0

# 3. ICMP внутри туннеля — необходимое, но не достаточное
ping -c 3 10.66.66.1

# 4. TCP на сервис, который слушает только внутренний адрес
curl -v --connect-timeout 5 http://10.66.66.1:8080/
ss -tlnp | grep 8080   # это уже на VPS: сервис не на 0.0.0.0:80 случайно?

Дальше — дамп. Это единственный способ перестать спорить. На офисном пире в одном окне генерирую трафик, в двух других:

tcpdump -ni wg0 host 10.66.66.1
tcpdump -ni eth0 udp port 51820
tcpdump -ni eth0 host 203.0.113.10 and not udp port 51820

Нормальная картина split-tunnel, когда я хожу на внутренний сервис: на wg0 видны пакеты до 10.66.66.1, на физическом интерфейсе до VPS — только UDP 51820. Если на физическом интерфейсе всплыл обычный TCP на белый 203.0.113.10:443, вы открыли публичный nginx, не туннель.

Для полного туннеля добавляю:

# с офисного пира
curl -4 -s https://ifconfig.me
# должен вернуться белый VPS, не белый офиса

ip route get 1.1.1.1
# dev wg0 или таблица 51820, не eth0

tcpdump -ni eth0 tcp port 443
# во время curl на внешний сайт здесь не должно быть голого TCP
# только UDP 51820

Если ifconfig.me показал белый офиса — трафик мимо. Если показал белый VPS, но на eth0 всё равно виден TCP 443 — смотрю второй интерфейс, IPv6, браузерный DoH, «умный» антивирус со своим туннелем. Бывает, что curl в туннеле, а Firefox живёт своей жизнью.

На самой VPS в это время:

tcpdump -ni wg0
tcpdump -ni ens3 udp port 51820
# conntrack, если ping есть, а TCP нет
conntrack -L | grep 10.66.66

Ещё одна ловушка: сервис на VPS слушает 127.0.0.1 или только публичный адрес. Туннель доставляет пакет на 10.66.66.1, nginx его не принимает. Это не «WireGuard сломался». Это ss -tlnp до настройки туннеля, не после.

Что я проверяю, когда «подключилось, но не работает»

Короткий разбор по симптомам, без перебора всего man.

  • Handshake старше нескольких минут, transfer ноль — NAT, файрвол хостера на UDP 51820, неверный Endpoint, часы сильно разъехались реже, но бывает. С офиса: nc -u -v 203.0.113.10 51820 ничего не доказывает (UDP без ответа), нужен дамп на VPS в момент wg-quick up.
  • Handshake живой, ping до 10.66.66.x нет — AllowedIPs не покрывает адрес, или адреса туннеля пересеклись у двух пиров. Два клиента с 10.66.66.2/24 одновременно — классика копипаста.
  • Ping есть, TCP нет — MTU, или сервис не слушает адрес туннеля, или FORWARD/фильтр режет не ICMP, а tcp.
  • До VPS по внутреннему IP есть, до NAS в офисе нет — на VPS в AllowedIPs нет LAN, на офисе нет forward, на роутере офиса нет обратного маршрута.
  • Браузер открывает сайт по домену «не туда» — DNS мимо туннеля или A-запись смотрит на белый, а вы думали, что ходите внутрь.
  • После сна ноутбука туннель «есть», трафика нет — keepalive, stale UDP mapping, помогает wg-quick down; wg-quick up, а постоянно — keepalive и иногда NetworkManager dispatcher, не вечная сессия с 2023 года.

Ключи и чужие IP в тикет не кладу. В логах для разбора достаточно последних байт публичного ключа из wg show и документационных адресов. Боевой Endpoint заказчика в статью и в переписку с подрядчиком не тащу.

Что оставляю в конфиге, когда уже заработало

Минимальный набор, который я не выкидываю «потом допишем»:

  • Endpoint только IP, порт явно 51820 или тот, что реально открыт у хостера. У части облаков UDP на дефолтном порту режут, тогда слушаю другой и это же пишу в Endpoint.
  • AllowedIPs по задаче, не максимальный. Split-tunnel — сети, которые правда надо унести. Полный туннель — осознанный 0.0.0.0/0 плюс IPv6-решение.
  • PersistentKeepalive на NAT-стороне.
  • DNS только тот, который достижим через получившиеся маршруты.
  • Проверка дампом один раз после подъёма, не «пинг прошёл — закрываем задачу».

Конфиг без этой проверки — это VPN на скриншоте. Скриншот wg show с handshake я в отчёт тоже прикладываю, но рядом кладу ip route get до рабочего адреса и кусок tcpdump, где видно UDP 51820 снаружи и целевой трафик внутри. Если второго нет, туннеля для работы тоже нет — есть только красивый интерфейс wg0.

Нужна профессиональная удалённая помощь с сервером, сайтом, компьютером или ноутбуком?

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

Обсудить задачу

Помогла статья? Поблагодари автора!

Остались вопросы, или есть что добавить? Добро пожаловать в комментарии.

Угостить автора чашечкой кофе