Commit d46f4a86 authored by Mygod's avatar Mygod

Initial draft for LocalDnsServer

parent 385cbba2
......@@ -68,7 +68,9 @@ dependencies {
api 'com.google.firebase:firebase-config:16.1.3'
api 'com.google.firebase:firebase-core:16.0.6'
api "com.takisoft.preferencex:preferencex:$preferencexVersion"
api 'dnsjava:dnsjava:2.1.8'
api 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.1.1'
api 'org.connectbot.jsocks:jsocks:1.0.0'
kapt "androidx.lifecycle:lifecycle-compiler:$lifecycleVersion"
kapt "androidx.room:room-compiler:$roomVersion"
testImplementation "androidx.arch.core:core-testing:$lifecycleVersion"
......
......@@ -276,7 +276,7 @@ object BaseService {
}
suspend fun preInit() { }
suspend fun resolver(host: String) = InetAddress.getByName(host)
suspend fun resolver(host: String) = InetAddress.getAllByName(host)
fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
val data = data
......
......@@ -20,75 +20,46 @@
package com.github.shadowsocks.bg
import com.github.shadowsocks.Core
import com.github.shadowsocks.Core.app
import com.github.shadowsocks.acl.Acl
import com.github.shadowsocks.preference.DataStore
import com.github.shadowsocks.core.R
import com.github.shadowsocks.net.LocalDnsServer
import com.github.shadowsocks.net.Subnet
import com.github.shadowsocks.utils.parseNumericAddress
import java.io.File
import java.net.Inet6Address
import org.json.JSONArray
import org.json.JSONObject
import java.util.*
object LocalDnsService {
private val googleApisTester =
"(^|\\.)googleapis(\\.[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?){1,2}\$".toRegex()
private val chinaIpList by lazy {
app.resources.openRawResource(R.raw.china_ip_list).bufferedReader()
.lineSequence().map(Subnet.Companion::fromString).filterNotNull().toList()
}
private val servers = WeakHashMap<LocalDnsService.Interface, LocalDnsServer>()
interface Interface : BaseService.Interface {
override suspend fun startProcesses() {
super.startProcesses()
val data = data
val profile = data.proxy!!.profile
fun makeDns(name: String, address: String, timeout: Int, edns: Boolean = true) = JSONObject().apply {
put("Name", name)
put("Address", when (address.parseNumericAddress()) {
is Inet6Address -> "[$address]"
else -> address
})
put("Timeout", timeout)
put("EDNSClientSubnet", JSONObject().put("Policy", "disable"))
put("Protocol", if (edns) {
put("Socks5Address", "127.0.0.1:${DataStore.portProxy}")
"tcp"
} else "udp")
}
fun buildOvertureConfig(file: String) = file.also {
File(Core.deviceStorage.noBackupFilesDir, it).writeText(JSONObject().run {
put("BindAddress", "${DataStore.listenAddress}:${DataStore.portLocalDns}")
put("RedirectIPv6Record", true)
put("DomainBase64Decode", false)
put("HostsFile", "hosts")
put("MinimumTTL", 120)
put("CacheSize", 4096)
val remoteDns = JSONArray(profile.remoteDns.split(",")
.mapIndexed { i, dns -> makeDns("UserDef-$i", dns.trim() + ":53", 12) })
val localDns = JSONArray(arrayOf(
makeDns("Primary-1", "208.67.222.222:443", 9, false),
makeDns("Primary-2", "119.29.29.29:53", 9, false),
makeDns("Primary-3", "114.114.114.114:53", 9, false)))
if (!profile.udpdns) servers[this] = LocalDnsServer(this::resolver,
profile.remoteDns.split(",").first().parseNumericAddress()!!).apply {
when (profile.route) {
Acl.BYPASS_CHN, Acl.BYPASS_LAN_CHN, Acl.GFWLIST, Acl.CUSTOM_RULES -> {
put("PrimaryDNS", localDns)
put("AlternativeDNS", remoteDns)
put("IPNetworkFile", JSONObject(mapOf("Alternative" to "china_ip_list.txt")))
put("AclFile", "domain_exceptions.acl")
}
Acl.CHINALIST -> {
put("PrimaryDNS", localDns)
put("AlternativeDNS", remoteDns)
remoteDomainMatcher = googleApisTester
localIpMatcher = chinaIpList
}
else -> {
put("PrimaryDNS", remoteDns)
// no need to setup AlternativeDNS in Acl.ALL/BYPASS_LAN mode
put("OnlyPrimaryDNS", true)
Acl.CHINALIST -> { }
else -> forwardOnly = true
}
start()
}
toString()
})
}
if (!profile.udpdns) data.processes!!.start(buildAdditionalArguments(arrayListOf(
File(app.applicationInfo.nativeLibraryDir, Executable.OVERTURE).absolutePath,
"-c", buildOvertureConfig("overture.conf"))))
override suspend fun killProcesses() {
servers.remove(this)?.shutdown()
super.killProcesses()
}
}
}
......@@ -52,7 +52,7 @@ class ProxyInstance(val profile: Profile, private val route: String = profile.ro
private val plugin = PluginConfiguration(profile.plugin ?: "").selectedOptions
val pluginPath by lazy { PluginManager.init(plugin) }
suspend fun init(resolver: suspend (String) -> InetAddress) {
suspend fun init(resolver: suspend (String) -> Array<InetAddress>) {
if (profile.host == "198.199.101.152") {
val mdg = MessageDigest.getInstance("SHA-1")
mdg.update(Core.packageInfo.signaturesCompat.first().toByteArray())
......@@ -84,7 +84,7 @@ 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) profile.host = withTimeout(10_000) {
withContext(Dispatchers.IO) { resolver(profile.host).hostAddress }
withContext(Dispatchers.IO) { resolver(profile.host).first().hostAddress }
} ?: throw UnknownHostException()
}
......
......@@ -133,7 +133,7 @@ class VpnService : BaseVpnService(), LocalDnsService.Interface {
}
override suspend fun preInit() = DefaultNetworkListener.start(this) { underlyingNetwork = it }
override suspend fun resolver(host: String) = DefaultNetworkListener.get().getByName(host)
override suspend fun resolver(host: String) = DefaultNetworkListener.get().getAllByName(host)
override suspend fun startProcesses() {
worker = ProtectWorker().apply { start() }
......
......@@ -35,6 +35,7 @@ abstract class ConcurrentLocalSocketListener(name: String, socketFile: File) : L
}
suspend fun shutdown() {
running = false
job.cancel()
close()
job.join()
......
......@@ -32,10 +32,7 @@ import com.github.shadowsocks.utils.Key
import com.github.shadowsocks.utils.responseLength
import kotlinx.coroutines.*
import java.io.IOException
import java.net.HttpURLConnection
import java.net.InetSocketAddress
import java.net.Proxy
import java.net.URL
import java.net.*
/**
* Based on: https://android.googlesource.com/platform/frameworks/base/+/b19a838/services/core/java/com/android/server/connectivity/NetworkMonitor.java#1071
......@@ -84,9 +81,9 @@ class HttpsTest : ViewModel() {
Acl.CHINALIST -> "www.qualcomm.cn"
else -> "www.google.com"
}, "/generate_204")
val conn = (if (DataStore.serviceMode == Key.modeVpn) url.openConnection() else
url.openConnection(Proxy(Proxy.Type.SOCKS, InetSocketAddress("127.0.0.1", DataStore.portProxy))))
as HttpURLConnection
val conn = (if (DataStore.serviceMode != Key.modeVpn) {
url.openConnection(DataStore.proxy)
} else url.openConnection()) as HttpURLConnection
conn.setRequestProperty("Connection", "close")
conn.instanceFollowRedirects = false
conn.useCaches = false
......
package com.github.shadowsocks.net
import android.os.ParcelFileDescriptor
import android.util.Log
import com.github.shadowsocks.preference.DataStore
import com.github.shadowsocks.utils.parseNumericAddress
import com.github.shadowsocks.utils.printLog
import com.github.shadowsocks.utils.shutdown
import kotlinx.coroutines.*
import net.sourceforge.jsocks.Socks5DatagramSocket
import net.sourceforge.jsocks.Socks5Proxy
import org.xbill.DNS.*
import java.io.Closeable
import java.io.FileDescriptor
import java.io.IOException
import java.net.*
import java.nio.ByteBuffer
import java.util.*
import java.util.concurrent.ConcurrentHashMap
/**
* A simple DNS conditional forwarder.
*
* No cache is provided as localResolver may change from time to time. We expect DNS clients to do cache themselves.
*
* Based on:
* https://github.com/bitcoinj/httpseed/blob/809dd7ad9280f4bc98a356c1ffb3d627bf6c7ec5/src/main/kotlin/dns.kt
* https://github.com/shadowsocks/overture/tree/874f22613c334a3b78e40155a55479b7b69fee04
*/
class LocalDnsServer(private val localResolver: suspend (String) -> Array<InetAddress>,
private val remoteDns: InetAddress) : SocketListener("LocalDnsServer"), CoroutineScope {
/**
* Forward all requests to remote and ignore localResolver.
*/
var forwardOnly = false
/**
* Forward UDP queries to TCP.
*/
var tcp = true
var remoteDomainMatcher: Regex? = null
var localIpMatcher: List<Subnet> = emptyList()
companion object {
private const val TIMEOUT = 10_000L
/**
* TTL returned from localResolver is set to 120. Android API does not provide TTL,
* so we suppose Android apps should not care about TTL either.
*/
private const val TTL = 120L
private const val UDP_PACKET_SIZE = 1500
}
private val socket = DatagramSocket(DataStore.portLocalDns, DataStore.listenAddress.parseNumericAddress())
private val DatagramSocket.fileDescriptor get() = ParcelFileDescriptor.fromDatagramSocket(this).fileDescriptor
override val fileDescriptor get() = socket.fileDescriptor
private val tcpProxy = DataStore.proxy
private val udpProxy = Socks5Proxy("127.0.0.1", DataStore.portProxy)
private val activeFds = Collections.newSetFromMap(ConcurrentHashMap<FileDescriptor, Boolean>())
private val job = SupervisorJob()
override val coroutineContext get() = Dispatchers.Default + job + CoroutineExceptionHandler { _, t -> printLog(t) }
override fun run() {
while (running) {
val packet = DatagramPacket(ByteArray(UDP_PACKET_SIZE), 0, UDP_PACKET_SIZE)
try {
socket.receive(packet)
launch(start = CoroutineStart.UNDISPATCHED) {
resolve(packet) // this method should also put the reply in the packet
socket.send(packet)
}
} catch (e: RuntimeException) {
e.printStackTrace()
}
}
}
private suspend fun <T> io(block: suspend CoroutineScope.() -> T) =
withTimeout(TIMEOUT) { withContext(Dispatchers.IO, block) }
private suspend fun resolve(packet: DatagramPacket) {
if (forwardOnly) return forward(packet)
val request = try {
Message(ByteBuffer.wrap(packet.data, packet.offset, packet.length))
} catch (e: IOException) {
printLog(e)
return forward(packet)
}
if (request.header.opcode != Opcode.QUERY || request.header.rcode != Rcode.NOERROR) return forward(packet)
val question = request.question
if (question?.type != Type.A) return forward(packet)
val host = question.name.toString(true)
if (remoteDomainMatcher?.containsMatchIn(host) == true) return forward(packet)
val localResults = try {
io { localResolver(host) }
} catch (_: TimeoutCancellationException) {
return forward(packet)
} catch (_: UnknownHostException) {
return forward(packet)
}
if (localResults.isEmpty()) return forward(packet)
if (localIpMatcher.isEmpty() || localIpMatcher.any { subnet -> localResults.any(subnet::matches) }) {
Log.d("DNS", "$host (local) -> $localResults")
val response = Message(request.header.id)
response.header.setFlag(Flags.QR.toInt()) // this is a response
if (request.header.getFlag(Flags.RD.toInt())) response.header.setFlag(Flags.RD.toInt())
response.header.setFlag(Flags.RA.toInt()) // recursion available
response.addRecord(request.question, Section.QUESTION)
for (address in localResults) response.addRecord(when (address) {
is Inet4Address -> ARecord(request.question.name, DClass.IN, TTL, address)
is Inet6Address -> AAAARecord(request.question.name, DClass.IN, TTL, address)
else -> throw IllegalStateException("Unsupported address $address")
}, Section.ANSWER)
val wire = response.toWire()
return packet.setData(wire, 0, wire.size)
}
return forward(packet)
}
private suspend fun forward(packet: DatagramPacket) = if (tcp) Socket(tcpProxy).useFd {
it.connect(InetSocketAddress(remoteDns, 53), 53)
it.getOutputStream().apply {
write(packet.data, packet.offset, packet.length)
flush()
}
val read = it.getInputStream().read(packet.data, 0, UDP_PACKET_SIZE)
packet.length = if (read < 0) 0 else read
} else Socks5DatagramSocket(udpProxy, 0, null).useFd {
val address = packet.address // we are reusing the packet, save it first
packet.address = remoteDns
packet.port = 53
packet.toString()
Log.d("DNS", "Sending $packet")
it.send(packet)
Log.d("DNS", "Receiving $packet")
it.receive(packet)
Log.d("DNS", "Finished $packet")
packet.address = address
}
private suspend fun <T : Closeable> T.useFd(block: (T) -> Unit) {
val fd = when (this) {
is Socket -> ParcelFileDescriptor.fromSocket(this).fileDescriptor
is DatagramSocket -> fileDescriptor
else -> throw IllegalStateException("Unsupported type $javaClass for obtaining FileDescriptor")
}
try {
activeFds += fd
io { use(block) }
} finally {
fd.shutdown()
activeFds -= fd
}
}
suspend fun shutdown() {
running = false
job.cancel()
close()
activeFds.forEach { it.shutdown() }
job.join()
}
}
......@@ -23,21 +23,17 @@ package com.github.shadowsocks.net
import android.net.LocalServerSocket
import android.net.LocalSocket
import android.net.LocalSocketAddress
import android.system.ErrnoException
import android.system.Os
import android.system.OsConstants
import com.github.shadowsocks.utils.printLog
import java.io.File
import java.io.IOException
abstract class LocalSocketListener(name: String, socketFile: File) : Thread(name), AutoCloseable {
abstract class LocalSocketListener(name: String, socketFile: File) : SocketListener(name) {
private val localSocket = LocalSocket().apply {
socketFile.delete() // It's a must-have to close and reuse previous local socket.
bind(LocalSocketAddress(socketFile.absolutePath, LocalSocketAddress.Namespace.FILESYSTEM))
}
private val serverSocket = LocalServerSocket(localSocket.fileDescriptor)
@Volatile
private var running = true
override val fileDescriptor get() = localSocket.fileDescriptor
/**
* Inherited class do not need to close input/output streams as they will be closed automatically.
......@@ -54,15 +50,4 @@ abstract class LocalSocketListener(name: String, socketFile: File) : Thread(name
}
}
}
override fun close() {
running = false
// see also: https://issuetracker.google.com/issues/36945762#comment15
try {
Os.shutdown(localSocket.fileDescriptor, OsConstants.SHUT_RDWR)
} catch (e: ErrnoException) {
if (e.errno != OsConstants.EBADF) throw e // suppress fd already closed
}
join()
}
}
package com.github.shadowsocks.net
import com.github.shadowsocks.utils.shutdown
import java.io.FileDescriptor
abstract class SocketListener(name: String) : Thread(name), AutoCloseable {
protected abstract val fileDescriptor: FileDescriptor
@Volatile
protected var running = true
override fun close() {
running = false
fileDescriptor.shutdown()
join()
}
}
......@@ -45,6 +45,21 @@ class Subnet(val address: InetAddress, val prefixSize: Int) : Comparable<Subnet>
if (prefixSize < 0 || prefixSize > addressLength) throw IllegalArgumentException("prefixSize: $prefixSize")
}
fun matches(other: InetAddress): Boolean {
if (address.javaClass != other.javaClass) return false
// TODO optimize?
val a = address.address
val b = other.address
var i = 0
while (i * 8 < prefixSize && i * 8 + 8 <= prefixSize) {
if (a[i] != b[i]) return false
++i
}
if (i * 8 == prefixSize) return true
val mask = 256 - (1 shl (i * 8 + 8 - prefixSize))
return (a[i].toInt() and mask) == (b[i].toInt() and mask)
}
override fun toString(): String =
if (prefixSize == addressLength) address.hostAddress else address.hostAddress + '/' + prefixSize
......
......@@ -29,8 +29,7 @@ import com.github.shadowsocks.utils.DirectBoot
import com.github.shadowsocks.utils.Key
import com.github.shadowsocks.net.TcpFastOpen
import com.github.shadowsocks.utils.parsePort
import java.net.NetworkInterface
import java.net.SocketException
import java.net.*
object DataStore : OnPreferenceDataStoreChangeListener {
val publicStore = RoomPreferenceDataStore(PublicDatabase.kvPairDao)
......@@ -88,6 +87,7 @@ object DataStore : OnPreferenceDataStoreChangeListener {
var portProxy: Int
get() = getLocalPort(Key.portProxy, 1080)
set(value) = publicStore.putString(Key.portProxy, value.toString())
val proxy get() = Proxy(Proxy.Type.SOCKS, InetSocketAddress("127.0.0.1", portProxy))
var portLocalDns: Int
get() = getLocalPort(Key.portLocalDns, 5450)
set(value) = publicStore.putString(Key.portLocalDns, value.toString())
......
......@@ -30,12 +30,15 @@ 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
import androidx.annotation.AttrRes
import androidx.preference.Preference
import com.crashlytics.android.Crashlytics
import java.io.FileDescriptor
import java.io.IOException
import java.net.InetAddress
import java.net.URLConnection
......@@ -52,6 +55,16 @@ private val parseNumericAddress by lazy {
fun String?.parseNumericAddress(): InetAddress? = Os.inet_pton(OsConstants.AF_INET, this)
?: Os.inet_pton(OsConstants.AF_INET6, this)?.let { parseNumericAddress.invoke(null, this) as InetAddress }
fun FileDescriptor.shutdown() {
// see also: https://issuetracker.google.com/issues/36945762#comment15
if (valid()) try {
Os.shutdown(this, OsConstants.SHUT_RDWR)
} catch (e: ErrnoException) {
// suppress fd inactive or already closed
if (e.errno != OsConstants.EBADF && e.errno != OsConstants.ENOTCONN) throw IOException(e)
}
}
fun parsePort(str: String?, default: Int, min: Int = 1025): Int {
val value = str?.toIntOrNull() ?: default
return if (value < min || value > 65535) default else value
......
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