所有文章
本文暂仅提供 英语 版本,其他语言翻译将陆续上线。

Split-tunneling on Android: three approaches, two failures

Мобильная команда· 发布于 2026/3/25· 10 分钟

From VpnService to VpnService extensions to userspace packet filtering — what actually works in 2026.

What we want

User: "I want my bank direct, Netflix via VPN." Sounds trivial — it has worked on desktops for years. On Android, it's hard.

Attempt #1: VpnService.Builder.addAllowedApplication()

Android's API has addAllowedApplication(packageName) and the inverse addDisallowedApplication. Looks like the solution.

Problem: for multiple apps the API is whitelist-only or blacklist-only, not both at once. And users can post-hoc edit VPN-exclusion lists only on Android 10+.

Works, but the limitations are awkward for users. Failure #1.

Attempt #2: extended VpnService with two interfaces

We tried to bring up two virtual interfaces — one for VPN traffic, one for direct — and route between them via our own packet filter.

Problem: Android doesn't allow two simultaneous VpnServices. One app = one tunnel. Failure #2.

Attempt #3: VpnService + userspace iptables-style packet filter

The only approach that works. We intercept packets from VpnService, inspect the source process UID, and — per rules (app-based or domain-based) — decide whether to send through VPN or NAT into the local network.

Gotchas:

  • Performance: a userspace packet filter in Java/Kotlin is slow. We moved it to native (Rust via JNI).
  • Domains via DNS — we also intercept, build a DNS cache, and route off that.
  • Battery: the filter runs constantly. We tuned a fast path for uncacheable categories.

Net: ~3–5% battery overhead and 0.5 ms latency. Invisible to users.

Code

We open-sourced the packet filter as a standalone library: github.com/logrus/android-pktfilter. Apache 2.0. Used by our client and outside contributors.

分享