Commit 89988ec3 authored by Mygod's avatar Mygod

Improve IPv4/IPv6 detection

parent d1e3e612
...@@ -20,17 +20,28 @@ ...@@ -20,17 +20,28 @@
package com.github.shadowsocks.bg package com.github.shadowsocks.bg
import android.annotation.SuppressLint
import android.annotation.TargetApi import android.annotation.TargetApi
import android.app.ActivityManager import android.app.ActivityManager
import android.net.DnsResolver import android.net.DnsResolver
import android.net.Network import android.net.Network
import android.os.Build import android.os.Build
import android.os.CancellationSignal import android.os.CancellationSignal
import android.system.ErrnoException
import android.system.Os
import android.system.OsConstants
import androidx.core.content.getSystemService import androidx.core.content.getSystemService
import com.github.shadowsocks.Core import com.github.shadowsocks.Core
import com.github.shadowsocks.Core.app import com.github.shadowsocks.Core.app
import com.github.shadowsocks.utils.int
import com.github.shadowsocks.utils.parseNumericAddress
import com.github.shadowsocks.utils.printLog
import com.github.shadowsocks.utils.use
import kotlinx.coroutines.* import kotlinx.coroutines.*
import java.io.FileDescriptor
import java.io.IOException import java.io.IOException
import java.net.Inet4Address
import java.net.Inet6Address
import java.net.InetAddress import java.net.InetAddress
import java.util.concurrent.Executor import java.util.concurrent.Executor
import java.util.concurrent.Executors import java.util.concurrent.Executors
...@@ -39,16 +50,69 @@ import kotlin.coroutines.resumeWithException ...@@ -39,16 +50,69 @@ import kotlin.coroutines.resumeWithException
sealed class DnsResolverCompat { sealed class DnsResolverCompat {
companion object : DnsResolverCompat() { companion object : DnsResolverCompat() {
private val instance by lazy { if (Build.VERSION.SDK_INT >= 29) DnsResolverCompat29 else DnsResolverCompat21 } private val instance by lazy {
when (Build.VERSION.SDK_INT) {
in 29..Int.MAX_VALUE -> DnsResolverCompat29
in 23 until 29 -> DnsResolverCompat23
in 21 until 23 -> DnsResolverCompat21()
else -> error("Unsupported API level")
}
}
/**
* Based on: https://android.googlesource.com/platform/frameworks/base/+/9f97f97/core/java/android/net/util/DnsUtils.java#341
*/
private val address4 = "8.8.8.8".parseNumericAddress()!!
private val address6 = "2000::".parseNumericAddress()!!
fun haveIpv4(network: Network) = checkConnectivity(network, OsConstants.AF_INET, address4)
fun haveIpv6(network: Network) = checkConnectivity(network, OsConstants.AF_INET6, address6)
private fun checkConnectivity(network: Network, domain: Int, addr: InetAddress) = try {
Os.socket(domain, OsConstants.SOCK_DGRAM, OsConstants.IPPROTO_UDP).use { socket ->
instance.bindSocket(network, socket)
Os.connect(socket, addr, 0)
}
true
} catch (_: IOException) {
false
} catch (_: ErrnoException) {
false
} catch (e: ReflectiveOperationException) {
check(Build.VERSION.SDK_INT < 23)
printLog(e)
val addresses = Core.connectivity.getLinkProperties(network)?.linkAddresses
true == when (addr) {
is Inet4Address -> addresses?.any { it.address is Inet4Address }
is Inet6Address -> addresses?.any {
it.address.run { this is Inet6Address && !isLinkLocalAddress && !isIPv4CompatibleAddress }
}
else -> error("Unknown address type")
}
}
override fun bindSocket(network: Network, socket: FileDescriptor) = instance.bindSocket(network, socket)
override suspend fun resolve(network: Network, host: String) = instance.resolve(network, host) override suspend fun resolve(network: Network, host: String) = instance.resolve(network, host)
override suspend fun resolveOnActiveNetwork(host: String) = instance.resolveOnActiveNetwork(host) override suspend fun resolveOnActiveNetwork(host: String) = instance.resolveOnActiveNetwork(host)
} }
@Throws(IOException::class)
abstract fun bindSocket(network: Network, socket: FileDescriptor)
abstract suspend fun resolve(network: Network, host: String): Array<InetAddress> abstract suspend fun resolve(network: Network, host: String): Array<InetAddress>
abstract suspend fun resolveOnActiveNetwork(host: String): Array<InetAddress> abstract suspend fun resolveOnActiveNetwork(host: String): Array<InetAddress>
private object DnsResolverCompat21 : DnsResolverCompat() { @SuppressLint("PrivateApi")
private open class DnsResolverCompat21 : DnsResolverCompat() {
private val bindSocketToNetwork by lazy {
Class.forName("android.net.NetworkUtils").getDeclaredMethod("bindSocketToNetwork")
}
private val netId by lazy { Network::class.java.getDeclaredField("netId") }
override fun bindSocket(network: Network, socket: FileDescriptor) {
val netId = netId.get(network)!!
val err = bindSocketToNetwork.invoke(null, socket.int, netId) as Int
if (err == 0) return
val message = "Binding socket to network $netId"
throw IOException(message, ErrnoException(message, -err))
}
/** /**
* This dispatcher is used for noncancellable possibly-forever-blocking operations in network IO. * This dispatcher is used for noncancellable possibly-forever-blocking operations in network IO.
* *
...@@ -65,6 +129,11 @@ sealed class DnsResolverCompat { ...@@ -65,6 +129,11 @@ sealed class DnsResolverCompat {
GlobalScope.async(unboundedIO) { InetAddress.getAllByName(host) }.await() GlobalScope.async(unboundedIO) { InetAddress.getAllByName(host) }.await()
} }
@TargetApi(23)
private object DnsResolverCompat23 : DnsResolverCompat21() {
override fun bindSocket(network: Network, socket: FileDescriptor) = network.bindSocket(socket)
}
@TargetApi(29) @TargetApi(29)
private object DnsResolverCompat29 : DnsResolverCompat(), Executor { private object DnsResolverCompat29 : DnsResolverCompat(), Executor {
/** /**
...@@ -72,6 +141,8 @@ sealed class DnsResolverCompat { ...@@ -72,6 +141,8 @@ sealed class DnsResolverCompat {
*/ */
override fun execute(command: Runnable) = command.run() override fun execute(command: Runnable) = command.run()
override fun bindSocket(network: Network, socket: FileDescriptor) = network.bindSocket(socket)
override suspend fun resolve(network: Network, host: String): Array<InetAddress> { override suspend fun resolve(network: Network, host: String): Array<InetAddress> {
return suspendCancellableCoroutine { cont -> return suspendCancellableCoroutine { cont ->
val signal = CancellationSignal() val signal = CancellationSignal()
......
...@@ -89,9 +89,11 @@ class ProxyInstance(val profile: Profile, private val route: String = profile.ro ...@@ -89,9 +89,11 @@ class ProxyInstance(val profile: Profile, private val route: String = profile.ro
// it's hard to resolve DNS on a specific interface so we'll do it here // it's hard to resolve DNS on a specific interface so we'll do it here
if (profile.host.parseNumericAddress() == null) { if (profile.host.parseNumericAddress() == null) {
// if fails/null, use IPv4 only, otherwise pick a random IPv4/IPv6 address // if fails/null, use IPv4 only, otherwise pick a random IPv4/IPv6 address
val hasIpv6 = service.getActiveNetwork()?.let { Core.connectivity.getLinkProperties(it) }?.linkAddresses val network = service.getActiveNetwork() ?: throw UnknownHostException()
?.any { it.address.run { this is Inet6Address && !isLinkLocalAddress && !isIPv4CompatibleAddress } } val hasIpv4 = DnsResolverCompat.haveIpv4(network)
profile.host = (hosts.resolve(profile.host, if (hasIpv6 == true) null else false).firstOrNull() ?: try { val hasIpv6 = DnsResolverCompat.haveIpv6(network)
if (!hasIpv4 && !hasIpv6) throw UnknownHostException()
profile.host = (hosts.resolve(profile.host, hasIpv6).firstOrNull() ?: try {
service.resolver(profile.host).firstOrNull() service.resolver(profile.host).firstOrNull()
} catch (_: IOException) { } catch (_: IOException) {
null null
......
...@@ -29,7 +29,6 @@ import android.net.Network ...@@ -29,7 +29,6 @@ import android.net.Network
import android.os.Build import android.os.Build
import android.os.ParcelFileDescriptor import android.os.ParcelFileDescriptor
import android.system.ErrnoException import android.system.ErrnoException
import android.system.Os
import com.github.shadowsocks.Core import com.github.shadowsocks.Core
import com.github.shadowsocks.VpnRequestActivity import com.github.shadowsocks.VpnRequestActivity
import com.github.shadowsocks.acl.Acl import com.github.shadowsocks.acl.Acl
...@@ -40,11 +39,12 @@ import com.github.shadowsocks.net.HostsFile ...@@ -40,11 +39,12 @@ import com.github.shadowsocks.net.HostsFile
import com.github.shadowsocks.net.Subnet import com.github.shadowsocks.net.Subnet
import com.github.shadowsocks.preference.DataStore import com.github.shadowsocks.preference.DataStore
import com.github.shadowsocks.utils.Key import com.github.shadowsocks.utils.Key
import com.github.shadowsocks.utils.int
import com.github.shadowsocks.utils.printLog import com.github.shadowsocks.utils.printLog
import com.github.shadowsocks.utils.use
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import java.io.Closeable
import java.io.File import java.io.File
import java.io.FileDescriptor import java.io.FileDescriptor
import java.io.IOException import java.io.IOException
...@@ -59,32 +59,26 @@ class VpnService : BaseVpnService(), LocalDnsService.Interface { ...@@ -59,32 +59,26 @@ class VpnService : BaseVpnService(), LocalDnsService.Interface {
private const val PRIVATE_VLAN4_ROUTER = "172.19.0.2" private const val PRIVATE_VLAN4_ROUTER = "172.19.0.2"
private const val PRIVATE_VLAN6_CLIENT = "fdfe:dcba:9876::1" private const val PRIVATE_VLAN6_CLIENT = "fdfe:dcba:9876::1"
private const val PRIVATE_VLAN6_ROUTER = "fdfe:dcba:9876::2" private const val PRIVATE_VLAN6_ROUTER = "fdfe:dcba:9876::2"
/**
* https://android.googlesource.com/platform/prebuilts/runtime/+/94fec32/appcompat/hiddenapi-light-greylist.txt#9466
*/
private val getInt = FileDescriptor::class.java.getDeclaredMethod("getInt$")
}
class CloseableFd(val fd: FileDescriptor) : Closeable {
override fun close() = Os.close(fd)
} }
private inner class ProtectWorker : ConcurrentLocalSocketListener("ShadowsocksVpnThread", private inner class ProtectWorker : ConcurrentLocalSocketListener("ShadowsocksVpnThread",
File(Core.deviceStorage.noBackupFilesDir, "protect_path")) { File(Core.deviceStorage.noBackupFilesDir, "protect_path")) {
override fun acceptInternal(socket: LocalSocket) { override fun acceptInternal(socket: LocalSocket) {
socket.inputStream.read() socket.inputStream.read()
val fd = socket.ancillaryFileDescriptors!!.single()!! socket.ancillaryFileDescriptors!!.single()!!.use { fd ->
CloseableFd(fd).use {
socket.outputStream.write(if (underlyingNetwork.let { network -> socket.outputStream.write(if (underlyingNetwork.let { network ->
if (network != null && Build.VERSION.SDK_INT >= 23) try { if (network != null) try {
network.bindSocket(fd) DnsResolverCompat.bindSocket(network, fd)
true return@let true
} catch (e: IOException) { } catch (e: IOException) {
// suppress ENONET (Machine is not on the network) // suppress ENONET (Machine is not on the network)
if ((e.cause as? ErrnoException)?.errno != 64) printLog(e) if ((e.cause as? ErrnoException)?.errno != 64) printLog(e)
false return@let false
} else protect(getInt.invoke(fd) as Int) } catch (e: ReflectiveOperationException) {
check(Build.VERSION.SDK_INT < 23)
printLog(e)
}
protect(fd.int)
}) 0 else 1) }) 0 else 1)
} }
} }
......
...@@ -26,8 +26,8 @@ import android.net.Network ...@@ -26,8 +26,8 @@ import android.net.Network
import android.net.NetworkCapabilities import android.net.NetworkCapabilities
import android.net.NetworkRequest import android.net.NetworkRequest
import android.os.Build import android.os.Build
import com.crashlytics.android.Crashlytics
import com.github.shadowsocks.Core import com.github.shadowsocks.Core
import com.github.shadowsocks.utils.printLog
import kotlinx.coroutines.CompletableDeferred import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.GlobalScope
...@@ -124,7 +124,7 @@ object DefaultNetworkListener { ...@@ -124,7 +124,7 @@ object DefaultNetworkListener {
Core.connectivity.requestNetwork(request, Callback) Core.connectivity.requestNetwork(request, Callback)
} catch (e: SecurityException) { } catch (e: SecurityException) {
// known bug: https://stackoverflow.com/a/33509180/2245107 // known bug: https://stackoverflow.com/a/33509180/2245107
if (Build.VERSION.SDK_INT != 23) Crashlytics.logException(e) if (Build.VERSION.SDK_INT != 23) printLog(e)
fallback = true fallback = true
} }
} }
......
...@@ -37,11 +37,9 @@ class HostsFile(input: String = "") { ...@@ -37,11 +37,9 @@ class HostsFile(input: String = "") {
} }
val configuredHostnames get() = map.size val configuredHostnames get() = map.size
fun resolve(hostname: String, isIpv6: Boolean? = null): Collection<InetAddress> { fun resolve(hostname: String, isIpv6: Boolean): List<InetAddress> {
var result: Collection<InetAddress> = map[hostname] ?: return emptyList() return (map[hostname] ?: return emptyList()).run {
if (isIpv6 != null) { if (isIpv6) filterIsInstance<Inet6Address>() else filterIsInstance<Inet4Address>()
result = if (isIpv6) result.filterIsInstance<Inet6Address>() else result.filterIsInstance<Inet4Address>() }.shuffled()
}
return result.shuffled()
} }
} }
...@@ -28,6 +28,7 @@ import android.graphics.BitmapFactory ...@@ -28,6 +28,7 @@ import android.graphics.BitmapFactory
import android.graphics.ImageDecoder import android.graphics.ImageDecoder
import android.net.Uri import android.net.Uri
import android.os.Build import android.os.Build
import android.system.ErrnoException
import android.system.Os import android.system.Os
import android.system.OsConstants import android.system.OsConstants
import android.util.TypedValue import android.util.TypedValue
...@@ -38,6 +39,7 @@ import kotlinx.coroutines.Dispatchers ...@@ -38,6 +39,7 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.suspendCancellableCoroutine import kotlinx.coroutines.suspendCancellableCoroutine
import java.io.FileDescriptor
import java.net.HttpURLConnection import java.net.HttpURLConnection
import java.net.InetAddress import java.net.InetAddress
import kotlin.coroutines.resume import kotlin.coroutines.resume
...@@ -58,6 +60,20 @@ fun <T> Iterable<T>.forEachTry(action: (T) -> Unit) { ...@@ -58,6 +60,20 @@ fun <T> Iterable<T>.forEachTry(action: (T) -> Unit) {
val Throwable.readableMessage get() = localizedMessage ?: javaClass.name val Throwable.readableMessage get() = localizedMessage ?: javaClass.name
/**
* https://android.googlesource.com/platform/prebuilts/runtime/+/94fec32/appcompat/hiddenapi-light-greylist.txt#9466
*/
private val getInt = FileDescriptor::class.java.getDeclaredMethod("getInt$")
val FileDescriptor.int get() = getInt.invoke(this) as Int
fun <T> FileDescriptor.use(block: (FileDescriptor) -> T) = try {
block(this)
} finally {
try {
Os.close(this)
} catch (_: ErrnoException) { }
}
private val parseNumericAddress by lazy @SuppressLint("DiscouragedPrivateApi") { private val parseNumericAddress by lazy @SuppressLint("DiscouragedPrivateApi") {
InetAddress::class.java.getDeclaredMethod("parseNumericAddress", String::class.java).apply { InetAddress::class.java.getDeclaredMethod("parseNumericAddress", String::class.java).apply {
isAccessible = true isAccessible = true
......
...@@ -27,6 +27,7 @@ import android.view.MotionEvent ...@@ -27,6 +27,7 @@ import android.view.MotionEvent
import androidx.appcompat.widget.AppCompatTextView import androidx.appcompat.widget.AppCompatTextView
import androidx.core.view.isGone import androidx.core.view.isGone
import com.crashlytics.android.Crashlytics import com.crashlytics.android.Crashlytics
import com.github.shadowsocks.utils.printLog
class AutoCollapseTextView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null, class AutoCollapseTextView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null,
defStyleAttr: Int = 0) : defStyleAttr: Int = 0) :
...@@ -40,15 +41,13 @@ class AutoCollapseTextView @JvmOverloads constructor(context: Context, attrs: At ...@@ -40,15 +41,13 @@ class AutoCollapseTextView @JvmOverloads constructor(context: Context, attrs: At
override fun onFocusChanged(focused: Boolean, direction: Int, previouslyFocusedRect: Rect?) = try { override fun onFocusChanged(focused: Boolean, direction: Int, previouslyFocusedRect: Rect?) = try {
super.onFocusChanged(focused, direction, previouslyFocusedRect) super.onFocusChanged(focused, direction, previouslyFocusedRect)
} catch (e: IndexOutOfBoundsException) { } catch (e: IndexOutOfBoundsException) {
e.printStackTrace() printLog(e)
Crashlytics.logException(e)
} }
override fun onTouchEvent(event: MotionEvent?) = try { override fun onTouchEvent(event: MotionEvent?) = try {
super.onTouchEvent(event) super.onTouchEvent(event)
} catch (e: IndexOutOfBoundsException) { } catch (e: IndexOutOfBoundsException) {
e.printStackTrace() printLog(e)
Crashlytics.logException(e)
false false
} }
} }
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment