В один момент ноутбук в офисном Wi-Fi, в следующий он подключён к телефону, и индикатор туннеля снова зелёный, прежде чем вы закончите переключать окна. Зелёный свет сообщает только о том, что туннель переподключился; что делала политика маршрутизации и блокировки в этот промежуток — другой вопрос. Эта политика представляет собой набор условий сопоставления, скомпилированных при последней установке, и называет интерфейс, адрес и семейство адресов, которые переход только что удалил. Поэтому один и тот же переход даёт три результата, выглядящие одинаково: блокировка продолжается, политика блокирует собственное восстановление, или открывается путь, который ничто не покрывает.

Ничто на экране не различает эти три состояния, поэтому спрашивайте, к каким идентификаторам были привязаны правила, а не какого цвета индикатор. В Linux правило может называть интерфейс или ipset, заполненный из него; в Android службе VpnService можно указать через setUnderlyingNetworks, за какими нижележащими сетями следовать. Каждая привязка указывает на канал, который вот-вот будет заменён, и каждую нужно переоценить после этого.

Что переход действительно переписывает

Когда старый канал исчезает, ядро удаляет его адреса, отзывает маршруты, которые шли через него, и заново строит выбор адреса источника из того, что осталось. Маршрут по умолчанию переезжает на новый шлюз, а индекс интерфейса, на который ссылались старые правила, исчез или переиспользован для другого устройства. Резолверы на канал тоже обновляются, хотя и не в тот же миг, что маршруты. Всё это происходит ниже клиента VPN, поэтому клиент, который следит только за состоянием собственного туннеля, ничего этого не видит.

Беда в том, что правило кодирует конкретный идентификатор, а не стоящее за ним намерение. Правило, отбрасывающее всё, что не уходит через интерфейс туннеля, кодирует имя интерфейса; правило, разрешающее конечную точку, кодирует адрес, который новая сеть может направить в другое место. Когда идентификатор исчезает, правило не становится видимо неверным: оно либо перестаёт совпадать с чем-либо, либо начинает совпадать с неправильным.

Перестроить внешний туннель, не пробивая дыру

Внешний транспорт был привязан к старому адресу источника, поэтому ему нужно открыть новый сокет с нового. Когда конечная точка — имя хоста, а не буквальный адрес, поиск должен успеть, пока блокировка ещё действует. Это и есть ловушка порядка: политика, достаточно строгая, чтобы отбросить всё, кроме конечной точки, отбросит и поиск, нужный для её нахождения, если только разрешение не произойдёт до снятия блокировки или не будет пронесено через ещё живой туннель. Оба порядка могут работать, но только если клиент знает, в каком он находится.

Клиенты различаются здесь так, что это важно при диагностике. Одни сначала разрешают, а потом устанавливают правила, поэтому переход стоит видимого перерыва и затем восстанавливается сам; другие держат блокировку и полагаются на выживший туннель, чтобы пронести поиск, а это полностью проваливается, когда туннель умер вместе с каналом. Добавление широкого прямого маршрута по умолчанию для восстановления связи всегда пропускает тест и всегда оставляет ту утечку, которую вы предотвращали.

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

ip route get 1.1.1.1
ip -6 route get 2606:4700:4700::1111

Когда новый канал приносит IPv6, а правила нет

Мобильные и гостевые сети часто выдают глобальный префикс IPv6, даже если проводная LAN офиса никогда этого не делала. Политика, написанная только как правила IPv4, обычная форма большинства самодельных рецептов iptables и nftables, не покрывает ничего из этого. Туннель остаётся подключённым, приложения с доступностью IPv6 выходят мимо него, а индикатор не может показать разницу, потому что туннель действительно поднят. Зеркальный случай столь же обычен: набор правил только для IPv6 ничего не делает в сети, которая не предлагает v6.

Сравните, что предлагает канал, с тем, что упоминает политика, по обе стороны перехода. Эти команды печатают те же факты, с которыми работает клиент:

ip -4 route show default
ip -6 route show default
sudo nft list ruleset | grep -c 'ip6 '

Если вторая команда печатает маршрут по умолчанию, а третья возвращает ноль, новый канал может вывести машину наружу через IPv6, и ни одно правило за этим не смотрит. Запустите все три снова после переключения, потому что интересное число — разница между двумя запусками. Если вторая ничего не печатает, действует обратный случай и любая политика, тяжёлая по v6, там лишь украшение.

Резолвер меняет руки посреди сессии

В современных системах резолверы привязаны к каналам, поэтому новый канал приходит с новым набором резолверов и, возможно, с другим представлением split-DNS. systemd-resolved держит по одной области на канал и выбирает между ними по маршруту, а macOS читает области видимости резолверов из динамического хранилища и уважает порядок интерфейсов. Клиент, устанавливающий собственный резолвер, конкурирует с резолвером канала, и правила приоритета, за которыми никто не следит, решают, кто победит. Тогда разрешение имён может сломаться, хотя фильтрация пакетов делает именно то, для чего была написана.

Читайте активный резолвер, а не настроенный. На хосте systemd подкоманда status показывает, с какого канала пришла каждая область и действует ли имя private-DNS:

resolvectl status --no-pager
resolvectl query intranet.example.com

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

Отработайте переход и сохраните запись

Воспроизведите изменение намеренно, а не ждите его. Поднимите туннель на первом канале, запустите длительную передачу и повторный поиск, затем смените канал и посмотрите, что выживет. Запишите интервал между исчезновением старого канала и сообщением туннеля о подключении, потому что именно в этом окне политика либо удержалась, либо нет. Прогоните последовательность и в обратную сторону, чтобы результат не был односторонним.

Исключения требуют собственного прохода, потому что обход, оправданный в старой сети, не оправдан автоматически в новой. Принтер, NAS или captive portal в домашней LAN не имеют аналога в мобильной сети, и исключение, которое до них дотягивалось, становится правилом без аналога.

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

Похожие статьи