Как мы делали split-tunneling на Android: 3 подхода, 2 провала
От 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. Используется и нашим клиентом и внешними контрибьюторами.