Summary
Я собрал VPN, подключился - рукопожатие прошло, счётчики трафика растут, а браузер упорно пишет
ERR_NAME_NOT_RESOLVED. Дальше была неделя отладки, где каждая починка ломала что-то соседнее. Разбираю по слоям, что именно происходило, и почему главной находкой стал обычныйtcpdump.
Симптом
Собранная схема выглядела так: клиенты приходят по 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. Любой адрес, который важен, нужно закреплять явно.
Чек-лист на следующий раз
- Подключиться и посмотреть, куда реально уходит запрос:
tcpdump -ni any port 53. - Проверить, достижим ли выданный клиенту DNS-адрес из его сегмента.
- Проверить
fake-ipдля собственных доменов. - Посмотреть, каким маршрутом уходит сам DNS при включённом
respect-rules. - Не латать дыру в нескольких местах одновременно: один слой, одна проверка.
Вывод
Проблемы с DNS почти никогда не живут в одном месте. Они наслаиваются, и каждое исправление открывает следующую проблему. Единственное, что реально работает, - смотреть на пакеты, а не на конфиг. Конфиг говорит, что должно быть. tcpdump - что происходит на самом деле.