Список разрешённых в Android VpnService: почему разрешённые страницы всё ещё зависают
Ваш список разрешённых, скорее всего, в порядке. Страницы зависают из-за ответа DNS, таблицы маршрутов, сокета резолвера или MTU. Вот порядок проверки.
Вы добавили домен в список разрешённых, запрос доходит до приложения, сопоставитель возвращает «разрешено», а страница так и не загружается. В этот момент большинство начинает переписывать сопоставитель и добавлять сравнения с шаблонами. По моему опыту сопоставитель обычно корректен, а сбой находится на уровень ниже: в ответе DNS, который вы синтезировали, в маршруте, который вы установили, или в сокете, которым пересылали запрос. Список — простая часть; ломаются списки разрешённых именно на пути пакета.
Сопоставитель редко бывает виноват
Сначала нужно разделить заблокированное и сломанное. Заблокированное имя должно отказывать быстро и заметно, потому что браузер получает ответ, что имени нет, и останавливается. Сломанное имя зависает, повторяет попытки через IPv6, переключается на другой транспорт, и вы остаётесь с крутящимся индикатором. Если вы видите крутящийся индикатор, список разрешённых, скорее всего, отвечает чем-то, с чем клиент не может ничего сделать.
Пустая запись A — классическая ошибка. Ответ NOERROR с нулём записей выглядит допустимым, поэтому stub-резолвер его принимает, браузер пробует снова, пробует AAAA, пробует другой резолвер и сдаётся гораздо позже. Код ответа 3, то есть NXDOMAIN, с повторённой секцией вопроса и скопированным идентификатором транзакции завершает разговор немедленно. Используйте его для каждого имени, которое вы намеренно отклоняете.
IPv6 заслуживает того же решения, а не значения по умолчанию. Если вы отвечаете NXDOMAIN на A и пропускаете AAAA, клиент может добраться до заблокированного имени по IPv6, и фильтр протекает. Некоторые реализации отвечают на заблокированные запросы AAAA адресом loopback, чтобы соединение падало сразу, а не переходило на запасной путь. Выберите одну политику, примените её к обоим типам записей и логируйте оба.
Маршрут, который перехватывает всё, владеет всем
Схема только с DNS никогда не добавляет addRoute("0.0.0.0", 0), поэтому в интерфейс TUN попадают только запросы, а всё остальное продолжает использовать нижележащую сеть. Это безопасная форма, и она же объясняет, почему некоторые списки разрешённых работают в Wi-Fi и отказывают в сотовой сети.
В тот момент, когда существует маршрут по умолчанию, каждый поток ваш. Пакет TCP, который приходит на интерфейс и не пересылается и не получает ответа, стоит в очереди, пока у клиента не истечёт время, и ошибки для журнала нет, потому что ничего не сломалось. Я видел, как человек потратил полдня на сломанный список разрешённых, который на деле был отсутствующим пересыльщиком для порта 443. Логируйте протокол и назначение каждого пакета, который решите отбросить.
Сокет резолвера должен покинуть туннель
Пересылка — место следующей ловушки. Резолверы оператора часто живут на частном адресе, до которого туннель не достаёт, поэтому пересылаемый запрос уходит в никуда, а приложение сообщает об истечении времени чтения. Отправляйте одобренные запросы резолверу, о котором знаете, что он доступен, и сначала передайте этот сокет в VpnService.protect(). Незащищённый сокет возвращает запрос в ваш собственный туннель, где он снова доходит до приложения и крутится, пока не сработает тайм-аут.
Сбой выглядит идентично сбою сети, и поэтому так долго живёт в отчётах об ошибках. Держите счётчики отправленных запросов и полученных ответов: если первое число растёт, а второе остаётся нулём, вы смотрите на сокет, а не на список.
Размер и контрольные суммы отказывают без сообщения
Тихие отбрасывания бывают и из-за размера. Ответ DNS с большим набором записей или рукопожатие TCP с щедрыми опциями может превысить MTU пути, который вы никогда явно не задавали; фрагмент исчезает выше по потоку, и клиент либо повторяет с меньшим размером, либо перестаёт пытаться. Установка MTU интерфейса в 1280 — консервативный выбор, который переживает большинство сотовых путей, и явность лучше унаследованного значения по умолчанию, которое никто не измерял.
Если вы строите пакеты сами, контрольная сумма — второй тихий убийца. Контрольная сумма UDP, вычисленная по псевдозаголовку, в котором пропущена длина UDP или поле протокола, даёт пакет, который у вас выглядит правильно сформированным, а получатель отбрасывает его без следа. Сравните свой псевдозаголовок со спецификацией поле за полем и печатайте вычисленную контрольную сумму для одного известного потока во время отладки.
Чего VpnService за вас не сделает
Два ограничения стоит знать до того, как проектировать вокруг них. Никакое обычное приложение не может установить системный прокси или файл PAC вне Wi-Fi, и эта часть не изменилась. Путь для отдельных приложений изменился: до API 29 VpnService.Builder вообще не мог нести прокси, а начиная с API 29 setHttpProxy(ProxyInfo.buildPacProxy(uri)) передаёт платформе скрипт PAC для вычисления для сети VPN. Вычисляйте правила в собственном коде, когда ориентируетесь на старые API или когда модель PAC не может выразить правило. UDP тоже не бесплатен: браузеры отправляют через прокси SOCKS только TCP, а большинство клиентов SOCKS5 никогда не реализуют UDP ASSOCIATE, поэтому схема на основе прокси отбрасывает или пропускает UDP, пока TCP выглядит здоровым.
Это второе ограничение — причина, по которой протокол на основе UDP может работать в обычной сети и отказать в момент подключения вашего VPN. Если туннель должен нести UDP, пересыльщик обязан реализовать путь пересылки и для него, а не только путь TCP с другим типом сокета.
Проверяйте в этом порядке
Пройдите список по порядку и остановитесь на первом этапе, который откажет, потому что каждая следующая проверка — шум, пока не держится предыдущая.
- Перечислите установленные маршруты и подтвердите, попадают ли пакеты, не являющиеся DNS, в интерфейс TUN вообще.
- Залогируйте имя запроса, решение и код ответа для одного заблокированного и одного одобренного имени.
- Подтвердите, что сокет резолвера защищён и выбранный резолвер отвечает изнутри туннеля.
- Подтвердите MTU интерфейса и контрольную сумму одного ответа, собранного вручную.
- Повторите ту же страницу в Wi-Fi и в сотовой сети, потому что резолверы оператора и MTU пути различаются.
Если все пять держатся, а страница всё ещё висит, следующий подозреваемый — сам пересыльщик: сравните перехваченный запрос с перехваченным ответом и поищите недостающий счётчик байтов или полузакрытый сокет. Справочник VpnService.Builder описывает контракты маршрута, DNS и MTU, а protect() — отдельный вызов VpnService, а не опция builder; подключённый интерфейс означает только то, что первый контракт выполнен.