From 0af450008b021592d3a1241891f593ddfbd57b15 Mon Sep 17 00:00:00 2001 From: Mygod Date: Thu, 8 Mar 2018 22:47:58 -0800 Subject: [PATCH] Implement network capabilities reporting This commit should fix the problem where VPN connection is viewed as metered connections on Android P DP1. However as I only have an emulator to test, I can't tell if it really works but it should. An interesting observation is that the developer can call setUnderlyingNetworks(arrayOfNulls(1)) to make the VPN connection always unmetered. This is more of a quick hack instead of a real fix. Source: https://android.googlesource.com/platform/frameworks/base/+/adbf1d0/services/core/java/com/android/server/connectivity/Vpn.java#312 One may ask whether it's possible to fix this issue for Android 8.1. The short answer is not that I know of. After some intensive source code reading, I found out that this is the commit that "breaks" whether VPN is metered for Android 8.1: https://android.googlesource.com/platform/frameworks/base/+/43d2a1700b6eb1d804924c6a1e5e0161a13a5348%5E%21/ The key is that isActiveNetworkMetered() used to return getActiveNetworkInfo().isMetered() and now it uses the more up-to-date API getActiveNetwork(). However, getActiveNetworkInfo() ignores VPN connections while getActiveNetwork() prefers VPN connection. And since NetworkCapabilities for VPN Network is always metered by default, this ultimately causes the bug. One might be able to get around this bug by somehow calling NetworkAgent.sendNetworkCapabilities in the correct context. Fix #1666. --- mobile/src/main/AndroidManifest.xml | 1 + .../com/github/shadowsocks/bg/VpnService.kt | 47 ++++++++++++++++++- 2 files changed, 47 insertions(+), 1 deletion(-) diff --git a/mobile/src/main/AndroidManifest.xml b/mobile/src/main/AndroidManifest.xml index 23bf4e9b..08691c77 100644 --- a/mobile/src/main/AndroidManifest.xml +++ b/mobile/src/main/AndroidManifest.xml @@ -3,6 +3,7 @@ package="com.github.shadowsocks" android:installLocation="internalOnly"> + diff --git a/mobile/src/main/java/com/github/shadowsocks/bg/VpnService.kt b/mobile/src/main/java/com/github/shadowsocks/bg/VpnService.kt index cfba6660..1cbdd618 100644 --- a/mobile/src/main/java/com/github/shadowsocks/bg/VpnService.kt +++ b/mobile/src/main/java/com/github/shadowsocks/bg/VpnService.kt @@ -20,12 +20,16 @@ package com.github.shadowsocks.bg +import android.annotation.TargetApi import android.app.Service +import android.content.Context import android.content.Intent import android.content.pm.PackageManager -import android.net.LocalSocket +import android.net.* +import android.os.Build import android.os.IBinder import android.os.ParcelFileDescriptor +import android.support.v4.os.BuildCompat import android.util.Log import com.github.shadowsocks.App.Companion.app import com.github.shadowsocks.JniHelper @@ -50,6 +54,20 @@ class VpnService : BaseVpnService(), LocalDnsService.Interface { private const val PRIVATE_VLAN6 = "fdfe:dcba:9876::%s" private val getInt: Method = FileDescriptor::class.java.getDeclaredMethod("getInt$") + + /** + * Unfortunately registerDefaultNetworkCallback is going to return our VPN interface: https://android.googlesource.com/platform/frameworks/base/+/dda156ab0c5d66ad82bdcf76cda07cbc0a9c8a2e + * + * This makes doing a requestNetwork with REQUEST necessary so that we don't get ALL possible networks that + * satisfies default network capabilities but only THE default network. Unfortunately we need to have + * android.permission.CHANGE_NETWORK_STATE to be able to call requestNetwork. + * + * Source: https://android.googlesource.com/platform/frameworks/base/+/2df4c7d/services/core/java/com/android/server/ConnectivityService.java#887 + */ + private val defaultNetworkRequest = NetworkRequest.Builder() + .addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) + .addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_RESTRICTED) + .build() } private inner class ProtectWorker : LocalSocketListener("ShadowsocksVpnThread") { @@ -90,6 +108,22 @@ class VpnService : BaseVpnService(), LocalDnsService.Interface { private var worker: ProtectWorker? = null private var tun2socksProcess: GuardedProcess? = null + private val connectivity by lazy { getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager } + @TargetApi(Build.VERSION_CODES.P) + private val defaultNetworkCallback = object : ConnectivityManager.NetworkCallback() { + override fun onAvailable(network: Network) { + setUnderlyingNetworks(arrayOf(network)) + } + override fun onCapabilitiesChanged(network: Network, networkCapabilities: NetworkCapabilities?) { + // it's a good idea to refresh capabilities + setUnderlyingNetworks(arrayOf(network)) + } + override fun onLost(network: Network) { + setUnderlyingNetworks(null) + } + } + private var listeningForDefaultNetwork = false + override fun onBind(intent: Intent): IBinder? = when (intent.action) { SERVICE_INTERFACE -> super.onBind(intent) else -> super.onBind(intent) @@ -98,6 +132,10 @@ class VpnService : BaseVpnService(), LocalDnsService.Interface { override fun onRevoke() = stopRunner(true) override fun killProcesses() { + if (BuildCompat.isAtLeastP() && listeningForDefaultNetwork) { + connectivity.unregisterNetworkCallback(defaultNetworkCallback) + listeningForDefaultNetwork = false + } worker?.stopThread() worker = null super.killProcesses() @@ -179,6 +217,13 @@ class VpnService : BaseVpnService(), LocalDnsService.Interface { this.conn = conn val fd = conn.fd + // We only need to update since Android P: https://android.googlesource.com/platform/frameworks/base/+/72f9c42b9e59761a28d6b32c42f65de57c98daed + if (BuildCompat.isAtLeastP()) { + // we want REQUEST here instead of LISTEN + connectivity.requestNetwork(defaultNetworkRequest, defaultNetworkCallback) + listeningForDefaultNetwork = true + } + val cmd = arrayListOf(File(applicationInfo.nativeLibraryDir, Executable.TUN2SOCKS).absolutePath, "--netif-ipaddr", PRIVATE_VLAN.format(Locale.ENGLISH, "2"), "--netif-netmask", "255.255.255.0",