Ко всем статьям

Как мы делали split-tunneling на Android: 3 подхода, 2 провала

Мобильная команда· Опубликовано 25.03.2026· 10 мин

От VpnService до расширений VpnService до перехвата через iptables — что работает в 2026.

Что хочется

Пользователь: «я хочу, чтобы Сбер был напрямую, а Netflix через VPN». Кажется тривиально — на десктопах это работает годами. На Android оказалось сложно.

Попытка #1: VpnService.Builder.addAllowedApplication()

Android API предлагает addAllowedApplication(packageName) и обратный addDisallowedApplication. Казалось бы, вот решение.

Проблема: для multiple apps API работает только в whitelist или blacklist режиме, не в обоих одновременно. Плюс пользователь может добавлять VPN-exclusion списки post-factum только для Android 10+.

Работает, но ограничения неудобны пользователю. Провал #1.

Попытка #2: Extended VpnService с двумя интерфейсами

Попробовали создать два виртуальных интерфейса — один для VPN-трафика, один для direct. Маршрутизация между ними через наш собственный packet filter.

Проблема: Android не даёт держать два одновременных VpnService. Один app = один tunnel. Провал #2.

Попытка #3: VpnService + iptables-based packet filter на userspace

Единственный рабочий подход. Мы перехватываем пакеты от VpnService, смотрим на UID процесса-источника, и по правилам (app-based или domain-based) решаем — отправить в VPN или сделать NAT в local network.

Сложности:

  • Performance: userspace packet filter на Java/Kotlin — медленный. Мы вынесли в native (Rust через JNI).
  • Домены по DNS — тоже перехватываем, строим DNS-cache, роутим по нему.
  • Battery: packet filter постоянно работает. Оптимизировали fast-path для uncacheable категорий.

Итог: работает с overhead'ом ~3-5% battery и 0.5ms latency. Для пользователя незаметно.

Код

Мы открыли packet-filter отдельной библиотекой: github.com/logrus/android-pktfilter. Apache 2.0. Используется и нашим клиентом и внешними контрибьюторами.

Поделиться

X / TwitterTelegram