From 0af450008b021592d3a1241891f593ddfbd57b15 Mon Sep 17 00:00:00 2001 From: Mygod Date: Thu, 8 Mar 2018 22:47:58 -0800 Subject: [PATCH 1/2] 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", From 74e2935f1e562810ac7f9d5c9ad9f671742d67dc Mon Sep 17 00:00:00 2001 From: Mygod Date: Fri, 23 Mar 2018 00:17:25 -0700 Subject: [PATCH 2/2] Actually use underlying network --- .../com/github/shadowsocks/bg/VpnService.kt | 49 +++++++++++-------- 1 file changed, 29 insertions(+), 20 deletions(-) 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 1cbdd618..4e3e0989 100644 --- a/mobile/src/main/java/com/github/shadowsocks/bg/VpnService.kt +++ b/mobile/src/main/java/com/github/shadowsocks/bg/VpnService.kt @@ -20,7 +20,6 @@ package com.github.shadowsocks.bg -import android.annotation.TargetApi import android.app.Service import android.content.Context import android.content.Intent @@ -29,7 +28,6 @@ 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 @@ -56,7 +54,8 @@ class VpnService : BaseVpnService(), LocalDnsService.Interface { 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 + * Unfortunately registerDefaultNetworkCallback is going to return VPN interface since Android P DP1: + * 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 @@ -77,13 +76,22 @@ class VpnService : BaseVpnService(), LocalDnsService.Interface { var success = false try { socket.inputStream.read() - val fds = socket.ancillaryFileDescriptors - if (fds.isEmpty()) return - val fd = getInt.invoke(fds.first()) as Int - success = protect(fd) - JniHelper.close(fd) // Trick to close file decriptor + val fd = socket.ancillaryFileDescriptors!!.single()!! + val fdInt = getInt.invoke(fd) as Int + try { + val network = underlyingNetwork + success = if (network != null && Build.VERSION.SDK_INT >= 23) { + network.bindSocket(fd) + true + } else protect(fdInt) + } catch (e: Exception) { + Log.e(tag, "Error when protect socket", e) + app.track(e) + } finally { + JniHelper.close(fdInt) // Trick to close file decriptor + } } catch (e: Exception) { - Log.e(tag, "Error when protect socket", e) + Log.e(tag, "Error when receiving ancillary fd", e) app.track(e) } try { @@ -107,19 +115,23 @@ class VpnService : BaseVpnService(), LocalDnsService.Interface { private var conn: ParcelFileDescriptor? = null private var worker: ProtectWorker? = null private var tun2socksProcess: GuardedProcess? = null + private var underlyingNetwork: Network? = null + set(value) { + if (Build.VERSION.SDK_INT >= 22) setUnderlyingNetworks(if (value == null) null else arrayOf(value)) + field = value + } 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)) + underlyingNetwork = network } override fun onCapabilitiesChanged(network: Network, networkCapabilities: NetworkCapabilities?) { // it's a good idea to refresh capabilities - setUnderlyingNetworks(arrayOf(network)) + underlyingNetwork = network } override fun onLost(network: Network) { - setUnderlyingNetworks(null) + underlyingNetwork = null } } private var listeningForDefaultNetwork = false @@ -132,7 +144,7 @@ class VpnService : BaseVpnService(), LocalDnsService.Interface { override fun onRevoke() = stopRunner(true) override fun killProcesses() { - if (BuildCompat.isAtLeastP() && listeningForDefaultNetwork) { + if (listeningForDefaultNetwork) { connectivity.unregisterNetworkCallback(defaultNetworkCallback) listeningForDefaultNetwork = false } @@ -217,12 +229,9 @@ 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 - } + // 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"),