Почему Android VpnService перехватывает часть трафика, а не весь
Маршруты, записи DNS и область действия по приложениям решают, какие пакеты доходят до устройства tun, почему локальные устройства исчезают и от чего отказывается конструктор.
Ничто в VpnService не перехватывает трафик само по себе. Он просит у системы интерфейс tun, передаёт этому интерфейсу набор адресов и маршрутов, и с этого момента ядро копирует каждый пакет, соответствующий этим маршрутам, в файловый дескриптор, который приложение может читать. Код приложения никогда не видит пакеты, которые не выбрали маршруты, поэтому охват туннеля — это вопрос таблицы маршрутизации раньше, чем вопрос программирования. Поэтому одна и та же реализация службы может проглотить весь поток на одном телефоне и лишь часть на другом, и поэтому добавление маршрута иногда чинит туннель, который выглядел сломанным. Вы получаете то, что объявили, минус всё, от чего конструктор тихо отказался.
Маршруты решают, какие пакеты приходят
Builder.addRoute(prefix, prefixLength) устанавливает префикс в таблицу маршрутизации сети VPN. Система считает эту сеть сетью по умолчанию для приложений в области действия, поэтому её записи просматриваются раньше маршрута по умолчанию физической сети, и подходящего префикса достаточно, чтобы отклонить поток. Поэтому маршрут для 0.0.0.0/0 забирает IPv4 целиком, включая принтер, цель cast и страницу администрирования роутера.
Клиенты, которым нужен такой охват без удаления физической записи по умолчанию, используют вместо неё пару 0.0.0.0/1 и 128.0.0.0/1. Два более конкретных маршрута побеждают маршрут по умолчанию, вместе они покрывают всё адресное пространство, а собственная запись телефона по умолчанию остаётся в таблице для сокетов, всё ещё привязанных к физической сети.
Адреса и записи DNS приходят от того же конструктора и отвечают на другой вопрос. addAddress даёт интерфейсу адрес с длиной префикса, а addDnsServer устанавливает резолвер, который приложения в области действия будут запрашивать через интерфейс. Оба семейства могут находиться на одном интерфейсе, поэтому набор маршрутов только для IPv4 рядом с адресом IPv6 — это конфигурация, которая читается правильно и всё равно протекает. Если вы маршрутизируете семейство, в котором у интерфейса нет адреса, у ядра нет адреса источника для этих пакетов, и они никогда не появятся у дескриптора.
Что возвращает establish()
establish() возвращает ParcelFileDescriptor или null, когда приложение не авторизовано. Null — это состояние согласия, а не ошибка сети: VpnService.prepare() уже должен был вернуть null, либо пользователь должен был подтвердить системный диалог до вызова. Согласие можно отозвать во время работы службы, и интерфейс закрывается у вас под ногами без обратного вызова в ваш цикл чтения.
Дескриптор не блокирующий, если не указано иное, поэтому чтение возвращается сразу без данных, а не паркует поток. Цикл, написанный для блокирующего сокета, сжигает одно ядро, а цикл, считающий пустое чтение фатальным, разрывает туннель за пустую секунду. Builder.setBlocking(true) меняет это поведение, и цена — поток, который ждёт внутри чтения.
Аргументы маршрута и адреса проверяются как значения LinkAddress, а multicast-назначения вроде 224.0.0.0/4 отвергаются сразу. Интерфейс tun точка-точка тоже не имеет домена широковещания, поэтому обнаружение multicast через него требует другого проекта, а не ещё одного маршрута. Справочник VpnService.Builder — это список, который стоит проверить, прежде чем заключить, что вызов проигнорирован.
Область по приложениям сужает набор, а не туннель
Начиная с API 21 конструктор принимает addAllowedApplication и addDisallowedApplication, и эти два списка взаимоисключающи в пределах одного конструктора. Список разрешённых делает туннель существующим для этих пакетов, пока всё остальное сохраняет физическую сеть; список запрещённых перехватывает всё, кроме названных пакетов, что клиент предлагает как раздельное туннелирование. До API 21 платформенного механизма нет вовсе, а поведение по приложениям приходило от сторонних клиентов, которые фильтровали по UID внутри собственного пересыльщика. Разница важна при отладке, потому что туннель по приложениям со списком разрешённых из одного пакета почти не даёт трафика, и это правильное поведение, а не сломанный маршрут.
Каким бы ни был охват, сокет, который ваше приложение открывает к серверу VPN, не должен проходить через туннель, который оно строит. VpnService.protect(socket) привязывает этот сокет к нижележащей сети, чтобы его пакеты обходили устройство tun. Пропустите вызов, и рукопожатие поглотит тот интерфейс, который должен был его поднять.
VpnService.setUnderlyingNetworks замыкает цикл, сообщая системе, какая физическая сеть несёт туннель. Это значение определяет, как VPN сообщает о связности другим приложениям, и null или устаревшее значение — обычная причина подключённого туннеля, который остальной телефон считает офлайн. Обновляйте его при смене нижележащей сети, а не только при запуске.
Контрольные суммы и MTU: где правильный маршрут всё равно не работает
Когда пакеты приходят, две детали на уровне пакета решают, работает ли что-то от начала до конца. Первая — адрес источника: при нескольких адресах интерфейса ядро может отправлять пакеты, несущие первый, поэтому пересыльщик, переписывающий заголовки, должен исправить адрес и контрольную сумму, которая его покрывает.
Вторая — охват контрольной суммы. Когда приложение само строит пакеты TCP или UDP, контрольная сумма покрывает псевдозаголовок с адресом источника, адресом назначения, номером протокола и длиной транспорта, и пропуск любого из них даёт пакеты, которые приходят и отбрасываются. Этот сбой выглядит как тайм-аут после ответа, который никогда не приходит, и это возвращает большинство людей к маршрутам. setMtu фиксирует MTU интерфейса, и каждое чтение возвращает ровно один полный пакет IP, поэтому буфер, написанный для потока байтов, фрагментирует ваш разбор, а не пакет.
Проверочный проход, который разделяет уровни
Чтобы найти, где охват пошёл не так, проверяйте уровни по порядку. Спросите устройство, какие маршруты сеть VPN действительно держит, затем проверьте, появляется ли поток к известному адресу как чтение у дескриптора, затем проверьте, правильно ли сформирован пакет, который отправляет ваш пересыльщик. Проблема маршрута ничего не показывает у дескриптора, тогда как проблема контрольной суммы показывает пакет, который приходит и исчезает.
Если до дескриптора ничего не доходит, подозреваемый — набор маршрутов, а не читатель: подтвердите префиксы, подтвердите наличие адреса в том же семействе и подтвердите, что приложение внутри заданной вами области. Если пакеты доходят до дескриптора, но ничего не возвращается, проверьте protect() и значение нижележащей сети, прежде чем снова трогать маршрут. Если дальняя сторона сообщает о повреждённых пакетах, контрольная сумма и MTU — это места, куда смотреть. Одна проверка на уровень экономит полдня догадок.