АйТи Фреш
Главная / Статьи / Сети
Сети

Почему DNS-запросы обходят VPN и как работают route-only-домены

Автор: Семёнов Евгений Сергеевич, директор ООО «АйТи-Фреш» · · ~19 мин чтения
Почему DNS-запросы обходят VPN и как работают route-only-домены
Иллюстрация к статье «Почему DNS-запросы обходят VPN и как работают route-only-домены».

VPN поднялся, внутренний DNS виден, прямой запрос к нему отвечает, а `crm.corp.example` всё равно получает NXDOMAIN от публичного резолвера. Я, Семёнов Евгений Сергеевич, директор ООО «АйТи-Фреш», разбирал эту картину не раз. Причина обычно не в WireGuard и не в самом DNS: администратор указал адрес сервера, но не объяснил `systemd-resolved`, какие имена надо отправлять на этот сервер. Покажу логику выбора DNS-канала, разницу между `~corp.example` и обычным поисковым суффиксом, постоянную настройку через NetworkManager и проверку результата на практическом стенде.

Адрес DNS-сервера ещё не задаёт маршрут запросов

Главная ошибка — воспринимать список DNS как /etc/resolv.conf старой школы: первый сервер основной, второй резервный, третий опрашивается после тайм-аута. У systemd-resolved другая модель. DNS-серверы, домены и признак DefaultRoute привязаны к сетевым интерфейсам. Ethernet может получить DNS от DHCP, а VPN — корпоративный DNS. Затем resolved для каждого запрашиваемого имени выбирает подходящий DNS-канал. Поэтому наличие 10.77.0.53 в выводе resolvectl status ещё ничего не гарантирует.

Сначала resolved ищет среди routing- и search-доменов всех каналов и глобальной конфигурации наиболее подходящий: совпадающий с именем и содержащий больше всего меток. Для api.corp.example домен corp.example специфичнее, чем ~.. Если одинаковый лучший домен настроен на нескольких каналах, запрос уходит на их серверы параллельно, и возвращается первый успешный ответ. Если совпадений нет, запрос получают DNS-серверы каналов с признаком DefaultRoute и глобальные серверы из DNS= в resolved.conf. Вот почему внутреннее имя уходит на публичный DNS: VPN-сервер объявлен, но домена для него нет, а сам VPN помечен как не используемый для остальных DNS-запросов.

Не путайте DNS-маршрутизацию с таблицей маршрутов ядра. DefaultRoute=no в выводе resolvectl относится к выбору DNS-канала. ipv4.never-default yes в NetworkManager запрещает обычный IP-маршрут по умолчанию через соединение. Это связанные в типичной конфигурации, но разные механизмы. Даже идеально выбранный DNS не ответит, если до 10.77.0.53 нет IP-маршрута через туннель.

Отдельно запомните правило неявного значения. Если DefaultRoute не задан явно ни через resolvectl default-route, ни через DNSDefaultRoute= в .network-файле, resolved вычисляет его сам: при наличии на канале route-only-домена, отличного от ~., признак считается no, иначе yes. Поэтому VPN-канал с DNS-серверами, но без доменов, по умолчанию может начать получать вообще все запросы наравне с Ethernet, а канал с ~corp.example автоматически перестаёт быть маршрутом по умолчанию. NetworkManager, в свою очередь, сам добавляет ~. соединению, у которого есть IP-маршрут по умолчанию.

Я начинаю диагностику не с перезапуска служб, а с двух проверок: какой канал выбрал resolved и существует ли IP-маршрут до выбранного DNS. Это экономит больше времени, чем правка `/etc/resolv.conf` наугад.

Что означает тильда и почему я выбираю route-only

Запись ~corp.example — route-only-домен. Тильда не является частью DNS-имени и никуда не передаётся. Она говорит resolved: запросы к самому corp.example и любым его поддоменам направляй на DNS-серверы этого канала, но не используй суффикс для дополнения коротких имён. Запрос crm.corp.example попадёт в VPN. Запрос crm не превратится автоматически в crm.corp.example.

Запись corp.example без тильды — поисковый домен. Для уже полного имени она выполняет ту же маршрутизирующую роль. Разница проявляется на однокомпонентных именах: приложение запросило crm, а resolved попробовал имя с поисковым суффиксом. Это удобно для старых сценариев, но создаёт неявное поведение. Пользователь думает, что обращается к локальному printer, а после подключения VPN получает корпоративный printer.corp.example. Риск не надо раздувать до катастрофы, однако путаница, лишние запросы и трудно воспроизводимые ошибки вполне реальны.

Я выбираю ~corp.example и требую от приложений FQDN. Обычный search-домен оставляю только там, где короткие имена являются документированным требованием старого ПО. Одновременно указывать corp.example и ~corp.example бессмысленно: обычный search-домен уже участвует в маршрутизации. Специальный домен ~. означает DNS-корень и совпадает с любым именем. Это правильная настройка для VPN, который должен забирать весь DNS-трафик, но плохой выбор для корпоративного split tunnel.

Не ставьте `~.` только ради того, чтобы «DNS VPN точно победил». Интернет-имена тоже пойдут к корпоративному серверу. Если нужен доступ лишь к внутренней зоне, задайте конкретные route-only-домены и `DefaultRoute=no`.
Почему DNS-запросы обходят VPN и как работают route-only-домены — схема
Схема к статье. Открыть схему в полном размере

Как увидеть реальную картину до внесения изменений

Сначала убеждаюсь, что работает именно systemd-resolved, а приложения обращаются к его stub-резолверу. На распространённой схеме /etc/resolv.conf ссылается на /run/systemd/resolve/stub-resolv.conf, где указан 127.0.0.53. Сам по себе этот адрес нормален: список реальных upstream-серверов хранится по каналам и показывается через resolvectl. Если /etc/resolv.conf является обычным статическим файлом, split DNS resolved может вообще не участвовать в запросах приложения.

Минимальный снимок состояния снимаю одной серией команд. Название интерфейса смотрю в общем статусе, а не угадываю по имени профиля NetworkManager.

systemctl is-active systemd-resolved
readlink -f /etc/resolv.conf
resolvectl status
resolvectl dns
resolvectl domain
nmcli connection show --active
ip route get 10.77.0.53

В нужном блоке должны одновременно присутствовать корпоративные DNS-серверы, ~corp.example и Default Route: no (в разных версиях строка называется DefaultRoute setting). Последняя команда должна показать VPN-интерфейс, например wg-polis. Если пакет до DNS идёт через обычный шлюз, исправлять домены пока рано — сначала добавьте DNS-адрес в AllowedIPs или обычные маршруты VPN.

Проверка dig @10.77.0.53 api.corp.example полезна, но доказывает только работоспособность конкретного сервера. Она намеренно обходит алгоритм выбора resolved. Для проверки пользовательского пути нужен resolvectl query api.corp.example: в ответе он показывает использованные сервер, протокол и link. После неудачных опытов я очищаю кэш, иначе отрицательный ответ NXDOMAIN может создать впечатление, что новая настройка не сработала.

sudo resolvectl flush-caches
resolvectl query api.corp.example
dig @10.77.0.53 api.corp.example

Наконец, браузер с принудительным DNS over HTTPS, локальный контейнерный резолвер или приложение с собственным DNS-клиентом способны обойти системную схему. Это уже отдельный тракт, и перезапуск resolved его не исправит.

Не редактируйте автоматически созданный `/etc/resolv.conf` как постоянное решение. После DHCP, перезагрузки или переподключения NetworkManager вернёт управляемое состояние, а проблема маршрутизации доменов останется.
Памятка: Как увидеть реальную картину до внесения изменений — схема
Памятка: Как увидеть реальную картину до внесения изменений. Открыть схему в полном размере

Кейс «Полиса и партнёров»: DNS был указан, но не использовался

Разберу проект страхового брокера «Полис и партнёры»: 29 рабочих мест, один офис, половина сотрудников регулярно работает из дома и на встречах с клиентами. Технический сценарий собран из моей практики. Девять ноутбуков менеджеров по урегулированию убытков и аналитиков работали на Ubuntu 24.04 LTS с systemd 255.4 и NetworkManager 1.46.0, остальные станции — Windows. Удалённые сотрудники подключались к офису через WireGuard. Внутренние сервисы — CRM по договорам, портал заявок и файловый архив полисов — жили в зоне corp.example, DNS — 10.77.0.53 и 10.77.0.54, прикладной сегмент — 10.77.0.0/16. Профиль VPN назывался polis-vpn, интерфейс — wg-polis.

Симптом выглядел убедительно: VPN подключён, ping 10.77.12.20 проходит, dig @10.77.0.53 api.corp.example возвращает внутренний адрес, но обычный getent hosts api.corp.example периодически даёт NXDOMAIN. В профиле действительно были оба корпоративных DNS. Однако ipv4.dns-search оставался пустым, а split-tunnel-профиль имел ipv4.never-default yes. В состоянии resolved это выражалось так:

Link 7 (wg-polis)
    Current Scopes: DNS
    DefaultRoute setting: no
    DNS Servers: 10.77.0.53 10.77.0.54

Серверы были зарегистрированы и DNS-scope существовал, но у канала не было ни подходящего домена, ни права обслуживать запросы по умолчанию. Запрос уходил через Wi-Fi-канал на DNS, полученный от домашнего роутера, а тот пересылал его публичному резолверу провайдера.

Дополнительную неразбериху создали две ручные попытки ремонта. На двух ноутбуках добавили search corp.example прямо в /etc/resolv.conf; настройка исчезла после переподключения. Ещё на одном в конфигурацию WireGuard вписали DNS = 10.77.0.53, и внутренние имена заработали, зато весь публичный DNS начал ходить через офисный канал: wg-quick регистрирует такие серверы через resolvconf -x, а совместимый режим resolvectl превращает -x в домен ~.. Утечкой это не назовёшь, но задержка внешних ответов выросла с 10–20 до 40–70 мс, а на время обслуживания офисного DNS сотрудник терял разрешение интернет-имён. Я вернул модель к простой политике: корпоративная зона — через VPN, всё остальное — через текущий канал сотрудника.

Ключевой диагностический признак кейса: на VPN-канале есть `Current Scopes: DNS` и оба сервера, но строка `DNS Domain` пуста, а `DefaultRoute` выключен. Сервер доступен — resolved просто не видит оснований посылать ему запросы.

Постоянная настройка через NetworkManager

На рабочих станциях я храню DNS-политику в профиле NetworkManager. Так она применяется вместе с VPN и удаляется при его отключении. Ручной resolvectl оставляю для теста или для VPN-клиента, который сам не интегрируется с resolved. На Ubuntu связка обычно уже настроена, но это надо проверить. Если NetworkManager не передаёт per-link DNS в resolved, задаётся его DNS-плагин:

# /etc/NetworkManager/conf.d/10-dns-resolved.conf
[main]
dns=systemd-resolved

После изменения конфигурации NetworkManager нужно перечитать настройки. Сам файл создавайте только после проверки существующих drop-in-файлов: глобальная настройка DNS влияет на все профили.

sudo systemctl reload NetworkManager

Для polis-vpn я задал два прямых доменных маршрута: внутреннюю прямую зону и reverse-зону. Автоматический DNS от самого VPN нам не требовался, а обычный маршрут по умолчанию должен был оставаться у основного канала сотрудника.

sudo nmcli connection modify "polis-vpn" ipv4.dns "10.77.0.53,10.77.0.54" ipv4.dns-search "~corp.example,~77.10.in-addr.arpa" ipv4.ignore-auto-dns yes ipv4.never-default yes
sudo nmcli connection down "polis-vpn"
sudo nmcli connection up "polis-vpn"

ipv4.ignore-auto-dns yes нужен, если профиль способен получить лишние DNS или домены автоматически. Он не запрещает два адреса, заданных вручную. Отрицательный dns-priority я здесь не использую: он может исключить DNS других активных соединений целиком, тогда как нам достаточно более специфичного доменного маршрута.

После переподключения resolvectl status wg-polis должен показать DNS Servers: 10.77.0.53 10.77.0.54, DNS Domain: ~corp.example ~77.10.in-addr.arpa и выключенный DefaultRoute. Если короткие имена действительно обязательны, замените ~corp.example на corp.example. Именно замените, а не добавляйте второй вариант. Я сначала выясняю, какое старое приложение требует crm вместо FQDN, и только затем разрешаю search-поведение.

Для интерфейса, которым NetworkManager не управляет, ту же политику можно проверить командами resolved. Но она живёт только до удаления интерфейса, перезапуска сетевого менеджера или перезагрузки:

sudo resolvectl domain wg-polis '~corp.example' '~77.10.in-addr.arpa'
sudo resolvectl default-route wg-polis no
sudo resolvectl dns wg-polis 10.77.0.53 10.77.0.54

VPN-клиент должен применять эти параметры при поднятии канала и вызывать resolvectl revert wg-polis при отключении. Иначе после аварийного завершения останется устаревшее per-link-состояние.

Параметры, введённые через `resolvectl`, не являются постоянной конфигурацией. Для парка машин закрепляйте их в NetworkManager, systemd-networkd либо штатных up/down-хуках VPN-клиента.
Памятка: Постоянная настройка через NetworkManager — схема
Памятка: Постоянная настройка через NetworkManager. Открыть схему в полном размере

WireGuard, OpenVPN и systemd-networkd: кто на самом деле пишет настройки

Половина проблем со split DNS возникает не в resolved, а в том, кто и как передаёт ему параметры. У wg-quick директива DNS = в секции [Interface] принимает и адреса, и имена: всё, что похоже на IP, становится сервером, остальное — поисковым доменом. Регистрирует их wg-quick командой resolvconf -a <интерфейс> -m 0 -x. Если в системе resolvconf — это ссылка на resolvectl, ключ -m молча игнорируется, а -x превращается в домен ~.. Итог: любой туннель wg-quick с DNS = забирает весь DNS-трафик, кроме имён, для которых на других каналах есть более специфичный домен. Для split DNS я убираю DNS = и задаю политику в хуках:

[Interface]
PostUp = resolvectl dns %i 10.77.0.53 10.77.0.54; resolvectl domain %i '~corp.example' '~77.10.in-addr.arpa'; resolvectl default-route %i no
PreDown = resolvectl revert %i

Если WireGuard импортирован в NetworkManager (nmcli connection import type wireguard file wg-polis.conf), хуки не нужны: DNS и домены задаются свойствами профиля, как в предыдущем разделе.

У OpenVPN штатной интеграции с resolved нет, для этого используют скрипт update-systemd-resolved. Он читает переданные сервером опции dhcp-option и вызывает resolved через D-Bus. Важная деталь: для route-only-доменов у него отдельный ключ DOMAIN-ROUTE, а DOMAIN и DOMAIN-SEARCH создают обычные поисковые домены. Клиентский конфиг по README проекта выглядит так (путь зависит от способа установки, в пакетах дистрибутивов он другой):

script-security 2
up /usr/local/libexec/openvpn/update-systemd-resolved
up-restart
down /usr/local/libexec/openvpn/update-systemd-resolved
down-pre

На стороне сервера отдаю push "dhcp-option DNS 10.77.0.53" и push "dhcp-option DOMAIN-ROUTE corp.example". dhcp-option DOMAIN-ROUTE . означает ~. — ровно то, чего в split tunnel надо избегать. Без up-restart скрипт не отработает при переподключении с сохранённым tun-устройством, и настройки канала могут потеряться.

На серверах и станциях без NetworkManager туннель описываю в systemd-networkd. В .network-файле интерфейса DNS= задаёт серверы, Domains= — домены (с тильдой — route-only), а DNSDefaultRoute= — участие канала в запросах, не совпавших ни с одним доменом. Без явного значения действует автоматический режим: канал с route-only-доменами маршрутом по умолчанию не считается.

# /etc/systemd/network/50-wg-polis.network
[Match]
Name=wg-polis

[Network]
DNS=10.77.0.53 10.77.0.54
Domains=~corp.example ~77.10.in-addr.arpa
DNSDefaultRoute=false

И последний источник сюрпризов — /etc/systemd/resolved.conf. Глобальные DNS= участвуют в каждом запросе, не совпавшем с доменами, параллельно с каналами DefaultRoute, а Domains= там действуют на всю систему. Прописанный туда корпоративный сервер или публичный резолвер «на всякий случай» ломает всю модель per-link-маршрутизации.

Прежде чем чинить split DNS на ноутбуке, выясните, какой компонент управляет VPN-интерфейсом: NetworkManager, wg-quick, скрипт OpenVPN или networkd. Если два из них одновременно пишут настройки в resolved, последний победивший перетрёт политику, и проблема будет «плавать» от подключения к подключению.

Приёмка, результат и ошибки, которые стоит ловить

Приёмку я провожу минимум в трёх состояниях: VPN выключен, VPN включён через офисный Wi-Fi и VPN включён через домашний роутер или мобильную точку доступа. После очистки кэша проверяю полное внутреннее имя, внешний домен, короткое имя и PTR. Короткое имя в выбранной мною политике не обязано разрешаться — это ожидаемый результат, а не дефект.

sudo resolvectl flush-caches
resolvectl query api.corp.example
resolvectl query kernel.org
resolvectl query crm
resolvectl query 10.77.12.20

Для внутреннего FQDN и PTR вывод должен ссылаться на wg-polis и корпоративный DNS. kernel.org должен обслуживаться обычным каналом. crm не должен незаметно превращаться в crm.corp.example.

Если результат спорный, смотрю пакеты. На VPN-интерфейсе должны появляться только запросы корпоративной зоны и нужных reverse-зон. Параллельно полезно проверить обычный интерфейс, чтобы убедиться, что внутренний FQDN туда не попадает.

sudo tcpdump -ni wg-polis '(host 10.77.0.53 or host 10.77.0.54) and port 53'

Не забывайте, что tcpdump -i any покажет ещё локальный обмен со stub 127.0.0.53 и может запутать картину. А dig @8.8.8.8 всегда обходит системную политику — это не подходящий тест отсутствия DNS-утечки.

В «Полисе и партнёрах» профиль развернули на девяти Ubuntu-ноутбуках. Провели 80 циклов подключения через домашние сети сотрудников, мобильный интернет и гостевой Wi-Fi офиса. Внутренний FQDN во всех циклах выбирал wg-polis; публичные запросы оставались на текущем канале. Задержка внутренних ответов укладывалась в 20–35 мс, внешняя вернулась к привычным 10–20 мс. За следующие 30 дней не было ни одного обращения «CRM не открывается после VPN»; до изменения таких обращений набиралось в среднем три-четыре в неделю.

Из повторяющихся ошибок я бы в первую очередь ловил ~., забытый reverse-домен, отсутствие IP-маршрута до DNS и браузерный DoH. На .local для корпоративной unicast-зоны лучше вообще не рассчитывать: этот суффикс зарезервирован для MulticastDNS и требует особого обращения. Приоритеты dns-priority NetworkManager трогайте только при конфликте одинаковых доменов на разных соединениях. Самое важное — явно описать зоны и проверить фактический link каждого контрольного запроса.

Получение публичным DNS имени вроде `api.corp.example` раскрывает часть внутренней схемы именования, но само по себе не означает компрометацию сети. Отнеситесь к этому как к утечке метаданных и ошибке архитектуры, без паники — она исправляется явной маршрутизацией зон.
Памятка: Приёмка, результат и ошибки, которые стоит ловить — схема
Памятка: Приёмка, результат и ошибки, которые стоит ловить. Открыть схему в полном размере

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

Почему запрос уходит на публичный DNS, хотя DNS VPN виден в `resolvectl status`?

Потому что адрес сервера не определяет обслуживаемые зоны. Если у VPN нет подходящего routing/search-домена и `DefaultRoute` выключен, resolved отправит запрос серверам каналов с `DefaultRoute` и глобальным `DNS=` из `resolved.conf`.

Чем `~corp.example` отличается от `corp.example`?

Оба варианта маршрутизируют полные имена зоны через связанный канал. Вариант без тильды дополнительно является поисковым суффиксом и может превратить короткое `crm` в `crm.corp.example`; route-only-вариант с тильдой этого не делает.

Нужно ли одновременно указывать `corp.example` и `~corp.example`?

Нет. Search-домен без тильды уже участвует в маршрутизации. Выберите его, если нужны короткие имена, либо route-only-домен, если приложения используют FQDN.

Что делает `~.`?

Это маршрут для DNS-корня, то есть подходящий маршрут практически для любого имени. Его используют, когда VPN должен принимать весь DNS-трафик. Более специфичные доменные маршруты всё равно имеют приоритет.

Почему после `DNS =` в конфиге WireGuard весь DNS пошёл через офис?

wg-quick регистрирует серверы командой `resolvconf -x`, а совместимый режим `resolvectl` превращает `-x` в домен `~.`. Для split DNS уберите `DNS =` и задайте домены через `PostUp` с `resolvectl` или через профиль NetworkManager.

Почему после правильной настройки имя всё ещё не открывается?

Очистите отрицательный кэш через `resolvectl flush-caches`, проверьте `ip route get` до DNS-сервера и убедитесь, что приложение не использует собственный DoH или контейнерный resolver.

Нужно ли править `/etc/resolv.conf`?

Обычно нет. При работе через systemd-resolved он должен вести к управляемому stub-файлу. Постоянные per-link-настройки задаются в NetworkManager, systemd-networkd или VPN-клиенте.

Столкнулись с похожей задачей? Обращайтесь — решим

Если у вас происходит что-то из описанного в этой статье — или любая другая проблема с ИТ-инфраструктурой, — обращайтесь в любое время. Мои специалисты и я лично разберём ситуацию, найдём настоящую причину и доведём до решения.

Возьмёмся и за разовую задачу, и за постоянное обслуживание. Первичная консультация — бесплатно и без обязательств.

📞 +7 903 729-62-41 💬 MAX: +7 903 729-62-41 ✈ Telegram @ITfresh_Boss

С уважением, Семёнов Евгений Сергеевич, директор «АйТи Фреш» — IT-аутсорсинг для компаний до 50 рабочих мест, 15+ лет практики

Источники

© ООО «АйТи-Фреш» · Москва · Все статьи