Commit 89988ec3 authored by Mygod's avatar Mygod

Improve IPv4/IPv6 detection

parent d1e3e612
......@@ -20,17 +20,28 @@
package com.github.shadowsocks.bg
import android.annotation.SuppressLint
import android.annotation.TargetApi
import android.app.ActivityManager
import android.net.DnsResolver
import android.net.Network
import android.os.Build
import android.os.CancellationSignal
import android.system.ErrnoException
import android.system.Os
import android.system.OsConstants
import androidx.core.content.getSystemService
import com.github.shadowsocks.Core
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 java.io.FileDescriptor
import java.io.IOException
import java.net.Inet4Address
import java.net.Inet6Address
import java.net.InetAddress
import java.util.concurrent.Executor
import java.util.concurrent.Executors
......@@ -39,16 +50,69 @@ import kotlin.coroutines.resumeWithException
sealed class 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 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 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.
*
......@@ -65,6 +129,11 @@ sealed class DnsResolverCompat {
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)
private object DnsResolverCompat29 : DnsResolverCompat(), Executor {
/**
......@@ -72,6 +141,8 @@ sealed class DnsResolverCompat {
*/
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> {
return suspendCancellableCoroutine { cont ->
val signal = CancellationSignal()
......
......@@ -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
if (profile.host.parseNumericAddress() == null) {
// if fails/null, use IPv4 only, otherwise pick a random IPv4/IPv6 address
val hasIpv6 = service.getActiveNetwork()?.let { Core.connectivity.getLinkProperties(it) }?.linkAddresses
?.any { it.address.run { this is Inet6Address && !isLinkLocalAddress && !isIPv4CompatibleAddress } }
profile.host = (hosts.resolve(profile.host, if (hasIpv6 == true) null else false).firstOrNull() ?: try {
val network = service.getActiveNetwork() ?: throw UnknownHostException()
val hasIpv4 = DnsResolverCompat.haveIpv4(network)
val hasIpv6 = DnsResolverCompat.haveIpv6(network)
if (!hasIpv4 && !hasIpv6) throw UnknownHostException()
profile.host = (hosts.resolve(profile.host, hasIpv6).firstOrNull() ?: try {
service.resolver(profile.host).firstOrNull()
} catch (_: IOException) {
null
......
......@@ -29,7 +29,6 @@ import android.net.Network
import android.os.Build
import android.os.ParcelFileDescriptor
import android.system.ErrnoException
import android.system.Os
import com.github.shadowsocks.Core
import com.github.shadowsocks.VpnRequestActivity
import com.github.shadowsocks.acl.Acl
......@@ -40,11 +39,12 @@ import com.github.shadowsocks.net.HostsFile
import com.github.shadowsocks.net.Subnet
import com.github.shadowsocks.preference.DataStore
import com.github.shadowsocks.utils.Key
import com.github.shadowsocks.utils.int
import com.github.shadowsocks.utils.printLog
import com.github.shadowsocks.utils.use
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import java.io.Closeable
import java.io.File
import java.io.FileDescriptor
import java.io.IOException
......@@ -59,32 +59,26 @@ class VpnService : BaseVpnService(), LocalDnsService.Interface {
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_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",
File(Core.deviceStorage.noBackupFilesDir, "protect_path")) {
override fun acceptInternal(socket: LocalSocket) {
socket.inputStream.read()
val fd = socket.ancillaryFileDescriptors!!.single()!!
CloseableFd(fd).use {
socket.ancillaryFileDescriptors!!.single()!!.use { fd ->
socket.outputStream.write(if (underlyingNetwork.let { network ->
if (network != null && Build.VERSION.SDK_INT >= 23) try {
network.bindSocket(fd)
true
if (network != null) try {
DnsResolverCompat.bindSocket(network, fd)
return@let true
} catch (e: IOException) {
// suppress ENONET (Machine is not on the network)
if ((e.cause as? ErrnoException)?.errno != 64) printLog(e)
false
} else protect(getInt.invoke(fd) as Int)
return@let false
} catch (e: ReflectiveOperationException) {
check(Build.VERSION.SDK_INT < 23)
printLog(e)
}
protect(fd.int)
}) 0 else 1)
}
}
......
......@@ -26,8 +26,8 @@ import android.net.Network
import android.net.NetworkCapabilities
import android.net.NetworkRequest
import android.os.Build
import com.crashlytics.android.Crashlytics
import com.github.shadowsocks.Core
import com.github.shadowsocks.utils.printLog
import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
......@@ -124,7 +124,7 @@ object DefaultNetworkListener {
Core.connectivity.requestNetwork(request, Callback)
} catch (e: SecurityException) {
// 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
}
}
......
......@@ -37,11 +37,9 @@ class HostsFile(input: String = "") {
}
val configuredHostnames get() = map.size
fun resolve(hostname: String, isIpv6: Boolean? = null): Collection<InetAddress> {
var result: Collection<InetAddress> = map[hostname] ?: return emptyList()
if (isIpv6 != null) {
result = if (isIpv6) result.filterIsInstance<Inet6Address>() else result.filterIsInstance<Inet4Address>()
}
return result.shuffled()
fun resolve(hostname: String, isIpv6: Boolean): List<InetAddress> {
return (map[hostname] ?: return emptyList()).run {
if (isIpv6) filterIsInstance<Inet6Address>() else filterIsInstance<Inet4Address>()
}.shuffled()
}
}
......@@ -28,6 +28,7 @@ import android.graphics.BitmapFactory
import android.graphics.ImageDecoder
import android.net.Uri
import android.os.Build
import android.system.ErrnoException
import android.system.Os
import android.system.OsConstants
import android.util.TypedValue
......@@ -38,6 +39,7 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import kotlinx.coroutines.suspendCancellableCoroutine
import java.io.FileDescriptor
import java.net.HttpURLConnection
import java.net.InetAddress
import kotlin.coroutines.resume
......@@ -58,6 +60,20 @@ fun <T> Iterable<T>.forEachTry(action: (T) -> Unit) {
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") {
InetAddress::class.java.getDeclaredMethod("parseNumericAddress", String::class.java).apply {
isAccessible = true
......
......@@ -27,6 +27,7 @@ import android.view.MotionEvent
import androidx.appcompat.widget.AppCompatTextView
import androidx.core.view.isGone
import com.crashlytics.android.Crashlytics
import com.github.shadowsocks.utils.printLog
class AutoCollapseTextView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null,
defStyleAttr: Int = 0) :
......@@ -40,15 +41,13 @@ class AutoCollapseTextView @JvmOverloads constructor(context: Context, attrs: At
override fun onFocusChanged(focused: Boolean, direction: Int, previouslyFocusedRect: Rect?) = try {
super.onFocusChanged(focused, direction, previouslyFocusedRect)
} catch (e: IndexOutOfBoundsException) {
e.printStackTrace()
Crashlytics.logException(e)
printLog(e)
}
override fun onTouchEvent(event: MotionEvent?) = try {
super.onTouchEvent(event)
} catch (e: IndexOutOfBoundsException) {
e.printStackTrace()
Crashlytics.logException(e)
printLog(e)
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