Skip to content

Commit

Permalink
Initial draft for LocalDnsServer
Browse files Browse the repository at this point in the history
  • Loading branch information
Mygod committed Jan 28, 2019
1 parent 385cbba commit d46f4a8
Show file tree
Hide file tree
Showing 14 changed files with 249 additions and 86 deletions.
2 changes: 2 additions & 0 deletions core/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
83 changes: 27 additions & 56 deletions core/src/main/java/com/github/shadowsocks/bg/LocalDnsService.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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)))
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)
}
else -> {
put("PrimaryDNS", remoteDns)
// no need to setup AlternativeDNS in Acl.ALL/BYPASS_LAN mode
put("OnlyPrimaryDNS", true)
}
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 -> {
remoteDomainMatcher = googleApisTester
localIpMatcher = chinaIpList
}
toString()
})
Acl.CHINALIST -> { }
else -> forwardOnly = true
}
start()
}
}

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()
}
}
}
4 changes: 2 additions & 2 deletions core/src/main/java/com/github/shadowsocks/bg/ProxyInstance.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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())
Expand Down Expand Up @@ -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()
}

Expand Down
2 changes: 1 addition & 1 deletion core/src/main/java/com/github/shadowsocks/bg/VpnService.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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() }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ abstract class ConcurrentLocalSocketListener(name: String, socketFile: File) : L
}

suspend fun shutdown() {
running = false
job.cancel()
close()
job.join()
Expand Down
11 changes: 4 additions & 7 deletions core/src/main/java/com/github/shadowsocks/net/HttpsTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
163 changes: 163 additions & 0 deletions core/src/main/java/com/github/shadowsocks/net/LocalDnsServer.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
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()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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()
}
}

0 comments on commit d46f4a8

Please sign in to comment.