Summary

Я собрал VPN, подключился - рукопожатие прошло, счётчики трафика растут, а браузер упорно пишет ERR_NAME_NOT_RESOLVED. Дальше была неделя отладки, где каждая починка ломала что-то соседнее. Разбираю по слоям, что именно происходило, и почему главной находкой стал обычный tcpdump.

Пять слоёв DNS-проблемы: симптом, причина и фикс

Симптом

Собранная схема выглядела так: клиенты приходят по VPN на сервер, а внутри сервера их трафик подхватывает Mihomo - диспетчер маршрутов. Я про него рассказывал в статье про каскадный VPN.

Проверка: подключаюсь с телефона. Туннель поднялся. В панели видно, что пакеты идут. Открываю любой сайт - тишина.

Если вы когда-нибудь отлаживали сеть, то знаете: когда «трафик идёт, а сайтов нет», первым делом надо подозревать DNS. Но оказалось, что DNS тут сломан не в одном месте, а в нескольких, и они маскировали друг друга.

flowchart LR
    C[Клиент VPN] -->|запрос DNS| R[iptables REDIRECT]
    R --> M[Mihomo :1053]
    M -->|respect-rules| P[Прокси-группа]
    M --> U[Локальный резолвер<br/>AdGuard Home]
    U --> I[(Интернет)]
    P -.->|не достаёт до моста| U

Слой 1. DNS, которого клиент не видит

Первая ошибка была банальной. Клиентам в конфиге я выдал DNS-адрес, который жил в Docker-мосту. Проблема в том, что из VPN-сегмента до этого моста маршрута нет.

Фикс: выдавать клиентам адрес из их собственного сегмента и принудительно перенаправлять любой DNS-трафик с VPN-сегментов на порт Mihomo.

iptables -t nat -A PREROUTING -i <vpn-сегмент> -p udp --dport 53 -j REDIRECT --to-ports 1053

Побочный эффект: все запросы теперь идут через Mihomo, и AdGuard Home, который стоял как DNS-фильтр, оказался обойдён. Я решил, что раз так, то AdGuard не нужен совсем, и убрал его. Забегая вперёд: он ещё вернётся.

Слой 2. fake-ip против собственных доменов

Теперь внешние сайты открывались, но собственные домены проекта, которые я поднял за обратным прокси, внутри VPN выдавали ту же ошибку ERR_NAME_NOT_RESOLVED. Снаружи при этом всё работало.

Причина: Mihomo в режиме fake-ip подменяет настоящие IP-адреса на фиктивные, чтобы потом понимать, к какому домену относится соединение. Для обычных сайтов это идеально. Для собственных внутренних доменов - катастрофа: клиент получает «поддельный» адрес, который не ведёт никуда.

Фикс: сначала исключил из подмены один домен, потом весь суффикс (fake-ip-filter) и добавил nameserver-policy: для своих доменов спрашивать локальный резолвер.

Слой 3. Запрос, который ехал через собственный прокси

Теперь самое интересное. Внешние сайты работают, внутренние домены в fake-ip-фильтре, а спорадические таймауты остались. Я запустил tcpdump и наконец увидел то, чего не хватало.

Так устроено: Mihomo работает в network_mode: host, то есть живёт в сети самого сервера. А его upstream-резолвер - это контейнер в Docker-мосту. Настройка respect-rules: true означает, что DNS-запросы тоже подчиняются правилам маршрутизации. Следовательно, запрос к резолверу уходил в группу RU-DIRECT, а она жёстко привязана к внешнему интерфейсу сервера. До адреса моста с внешнего интерфейса не добраться.

Проще говоря, DNS-запрос выходил из дома через парадную дверь, чтобы зайти в соседнюю комнату.

Фикс: резолвер стал слушать ещё и на 127.0.0.1, а Mihomo ходит к нему по localhost. Локальный адрес в правилах не нужен - он просто достижим.

Tip

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

Слой 4. Искушение заткнуть дыру в firewall

На этом этапе возникла идея: «раз DNS к моим собственным адресам не надо перехватывать, добавлю исключения в iptables». Я даже добавил. Всё заработало. А потом я понял, что сломал главную идею: все DNS-запросы клиентов должны идти через Mihomo, иначе он теряет понимание, что куда ведёт.

Правила откатил. Принцип, который я себе записал: маршрутизация решается политикой в Mihomo, а не хитростями в firewall. Как только политика расползается по двум местам, вы перестаёте понимать, кто и что решил.

Слой 5. AdGuard Home возвращается

Помните, я убрал AdGuard? Через несколько дней выяснилось, что по нему скучаю: блокировка рекламы, статистика и вообще удобный интерфейс. Я вернул его, но уже правильно:

  • списки блокировки взяты из рабочего образца;
  • upstream-серверы скопированы с домашнего роутера;
  • bootstrap - российский DNS по IP, а fallback - его же DoH/DoT.

Цена компромисса: в логах AdGuard теперь нет настоящего IP клиента, потому что запросы приходят не напрямую, а через Mihomo. Для меня это приемлемо.

Мелочи, о которых тоже стоит знать

  • Панель MetaCubeXD не подключалась. Сначала грешил на CORS. Оказалось, что preflight-запрос ломал forward-auth на соседнем домене. Лечится настройкой external-controller-cors с allow-private-network: true.
  • Контейнер без фиксированного IP занял адрес, зарезервированный под DNS, и получил Address already in use. Любой адрес, который важен, нужно закреплять явно.

Чек-лист на следующий раз

  1. Подключиться и посмотреть, куда реально уходит запрос: tcpdump -ni any port 53.
  2. Проверить, достижим ли выданный клиенту DNS-адрес из его сегмента.
  3. Проверить fake-ip для собственных доменов.
  4. Посмотреть, каким маршрутом уходит сам DNS при включённом respect-rules.
  5. Не латать дыру в нескольких местах одновременно: один слой, одна проверка.

Вывод

Проблемы с DNS почти никогда не живут в одном месте. Они наслаиваются, и каждое исправление открывает следующую проблему. Единственное, что реально работает, - смотреть на пакеты, а не на конфиг. Конфиг говорит, что должно быть. tcpdump - что происходит на самом деле.