Android VPN 应用流量处理及绕过特定 IP (127.0.0.1)
2025-03-16 06:23:25
Android VPN 应用自身流量处理及绕过特定 IP
碰到的问题
在开发一款 VPN 应用时,遇到了一个挺棘手的问题。为了保证应用的自身流量也通过 VPN 隧道,我需要特别处理。简单说,如果不把应用的包名加到 addDisallowedApplication
里,应用就连不上服务器了。
问题在于,如果不用 addDisallowedApplication
,应用启动 VPN 后会立刻尝试连接服务器。但这个时候 VPN 连接还没完全建立起来,导致连接总是失败。应用会通过 ping 127.0.0.1:8086
来检测网络是否通畅,只有 ping 通了才会继续连接。
我想知道,有没有办法能直接让流量发到 127.0.0.1:8086
,同时又不用 addDisallowedApplication
呢?
问题根源
这个问题的核心在于 VPN 连接建立过程和应用连接逻辑之间的时序冲突。
-
VPN 连接异步性: VPNService 的
establish()
方法返回一个ParcelFileDescriptor
,这并不代表 VPN 隧道立刻就绪。VPN 连接的建立,包括路由配置、网络接口设置等,是异步进行的。 -
应用启动过快: 应用启动 VPN 后立即进行连接尝试。由于 VPN 隧道还没准备好,发往外网的请求自然无法成功,即使目标地址是本地环回地址(
127.0.0.1
)的特定端口(8086
)。 -
127.0.0.1
的特殊性: 即使 VPN 隧道就绪,127.0.0.1
也默认会被包含在 VPN 路由中。发送到127.0.0.1:8086
的数据包会被导向 VPN 接口,而不是直接到达应用自身的服务。
解决方案
解决这个问题的关键是让应用在 VPN 隧道完全建立好之后再进行连接,以及让 127.0.0.1:8086
的流量不经过 VPN 隧道。有几种方法可以实现:
1. 延迟连接 + protect()
方法
这是比较推荐的一种方法,可以同时处理 VPN 连接的异步性和特定 IP/端口的绕过。
-
原理:
VpnService.protect(socket)
: 这个方法可以保护一个 socket,使其流量不经过 VPN。- 延迟连接: 通过在 VPNService 启动后增加延时,等待 VPN 隧道完全建立。
-
步骤:
- 在 VpnService 的
onStartCommand()
方法中,启动 VPN 隧道,并开启一个后台线程/Handler。 - 在后台线程/Handler 中,创建用于连接
127.0.0.1:8086
的 Socket。 - 调用
VpnService.protect(socket)
保护该 Socket。 - 设置一个适当的延时 (比如 2-3 秒),确保 VPN 隧道建立。
- 通过该 Socket 连接到
127.0.0.1:8086
。 - 在连接到远端服务时,创建 Socket 时也进行 protect.
- 在 VpnService 的
-
代码示例 (Kotlin):
class MyVpnService : VpnService() { private val vpnInterface: ParcelFileDescriptor? = null override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { // ... 建立 VPN 隧道 ... vpnInterface = Builder() .addAddress("10.0.0.2", 32) .addRoute("0.0.0.0", 0) // ... 其他设置 ... .establish() Thread { try { // 1. 创建 Socket val socket = Socket() // 2. 保护 Socket protect(socket) // 3. 延迟 Thread.sleep(3000) // 3 秒延时 // 4. 连接 socket.connect(InetSocketAddress("127.0.0.1", 8086)) // ... 处理连接 ... val outputStream = socket.getOutputStream() outputStream.write("Hello from VPN!".toByteArray()) //在连接到外部网络时保护该socket,防止递归 val serverSocket = Socket() protect(serverSocket) serverSocket.connect(InetSocketAddress("your.server.ip", 12345)) } catch (e: Exception) { e.printStackTrace() } }.start() return START_STICKY } // ... }
-
安全建议:
protect()
方法只对通过socket()
系统调用创建的 socket 有效。务必确保使用的 Socket 对象是这样创建的。 -
进阶使用技巧 : 可以建立一个 socket pool, 通过
protect
方法来跳过 VPN 限制,达到灵活控制哪些 socket 需要走 VPN 哪些不需要。
2. 使用 bindSocket()
这个方法也可以在不用 addDisallowedApplication
的前提下解决问题。
-
原理:
Network.bindSocket(socket)
: 这个方法可以将一个 socket 绑定到特定的网络接口。可以用来强制 socket 使用特定的网络,比如绕过 VPN。
-
步骤:
- 在VPNService里获取系统的默认网络。
- 在创建连接到
127.0.0.1:8086
的 Socket 前,调用bindSocket()
方法。
-
代码示例(Kotlin):
```kotlin
class MyVpnService : VpnService() {
private var defaultNetwork: Network? = null
override fun onCreate() {
super.onCreate()
val connectivityManager = getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
defaultNetwork = connectivityManager.activeNetwork
}
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
vpnInterface = Builder()
.addAddress("10.0.0.2", 32)
.addRoute("0.0.0.0", 0)
// ... 其他设置 ...
.establish()
// ...建立 VPN...
Thread {
try {
//延迟, 等待 VPN 服务启动
Thread.sleep(3000)
val socket = Socket()
defaultNetwork?.bindSocket(socket) // 绑定到默认网络
socket.connect(InetSocketAddress("127.0.0.1", 8086))
// ... 连接到 127.0.0.1:8086 ...
} catch (e:Exception) {
//...
}
}.start()
return START_STICKY;
}
}
```
- 进阶使用技巧
将bindSocket()
方法封装成为工具方法,方便随时调用
3. 显式路由配置
这种方法需要更精细地控制路由表,操作更复杂,适合对网络底层有深入了解的开发者。不推荐初学者使用
-
原理: 通过在
VpnService.Builder
中精确设置路由,排除127.0.0.1/32
(或者更具体的127.0.0.1/8
),使其不经过 VPN 隧道。 -
步骤:
- 在
VpnService.Builder
中,不要 添加包含127.0.0.1
的路由 (比如0.0.0.0/0
)。 - 手动添加需要的路由,确保不包含
127.0.0.1
。 例如把0.0.0.0/0
拆分成0.0.0.0/1
和128.0.0.0/1
. - 如果除了
127.0.0.1
外, 有其他的ip 也想排除, 也可以在这里进行拆分. - 适当延迟应用中的连接逻辑,等待路由配置完成。
- 在
-
代码示例 (Kotlin):
class MyVpnService : VpnService() { override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { val vpnInterface = Builder() .addAddress("10.8.0.2", 32) //vpn interface ip .addRoute("0.0.0.0", 1) //CIDR notation, 0.0.0.0~127.255.255.255 .addRoute("128.0.0.0", 1)// 128.0.0.0~255.255.255.255, 避开 127.0.0.1 // .addRoute("192.168.1.0", 24) // specific network // ... .establish() //连接127.0.0.1:8086的socket Thread{ Thread.sleep(3000) //等待路由配置生效. //这里无需做特殊处理.直接创建和连接 socket val socket = Socket() socket.connect(InetSocketAddress("127.0.0.1",8086)) //... }.start() return START_STICKY } }
注意: 在拆分0.0.0.0/0
的时候,一定要将其完整拆分为 0.0.0.0/1
and 128.0.0.0/1
, 这样才能保证除了127.0.0.1
外所有流量都进入 VPN 隧道。
- 安全建议: 错误的网络路由设置可能会导致整个 VPN 连接失败或流量泄露, 需仔细配置和测试。
4. 轮询 + protect()
这是一个更稳妥但稍微复杂一点的方法。
-
原理: 启动一个轮询线程,不断尝试创建 Socket 并连接到
127.0.0.1:8086
,一旦连接成功,立即调用protect()
保护该 Socket。 -
步骤:
- 在
onStartCommand
方法中建立好 VPN 隧道. - 在主线程之外启动一个不断循环的线程。
- 在循环内:
- 创建 Socket。
- 尝试连接
127.0.0.1:8086
。 - 如果连接成功, 则调用
protect()
方法,然后跳出循环. - 如果连接失败,短暂休眠 (例如 100 毫秒),然后继续循环。
- 在
-
代码示例(Kotlin):
class MyVpnService : VpnService() { override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { val vpnInterface = Builder() .addAddress("192.168.0.1", 24) .addRoute("0.0.0.0", 0) .establish() //建立vpn tunnel Thread{ var connected = false; while(!connected) { try{ val socket = Socket() socket.connect(InetSocketAddress("127.0.0.1", 8086), 2000) //timeout protect(socket) connected = true }catch(e:IOException) { Thread.sleep(100); } } }.start() return START_STICKY; } }
选择哪个方案?
- 方案1(延迟连接 +
protect()
) 是最常用的,实现起来也最简单。适用于大部分情况。 - 方案2 (使用
bindSocket()
) 在不修改路由的情况下也可以达到预期效果, 建议尝试. - 方案3(显式路由配置) 适用于对网络路由有深入了解的开发者,可以更精细地控制流量走向,但是也更容易出错。
- 方案4 (轮询 +
protect()
) 最为稳健, 避免了竞态条件,但略微增加实现复杂性.
最终,选择哪个方案,需要根据应用的具体需求和开发者的技术水平来决定。