Commit 55b960fe authored by Mygod's avatar Mygod

Respect ACL files when doing local DNS

parent 9f26ec8f
/*******************************************************************************
* *
* Copyright (C) 2019 by Max Lv <max.c.lv@gmail.com> *
* Copyright (C) 2019 by Mygod Studio <contact-shadowsocks-android@mygod.be> *
* *
* This program is free software: you can redistribute it and/or modify *
* it under the terms of the GNU General Public License as published by *
* the Free Software Foundation, either version 3 of the License, or *
* (at your option) any later version. *
* *
* This program is distributed in the hope that it will be useful, *
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
* GNU General Public License for more details. *
* *
* You should have received a copy of the GNU General Public License *
* along with this program. If not, see <http://www.gnu.org/licenses/>. *
* *
*******************************************************************************/
package com.github.shadowsocks.acl
import com.github.shadowsocks.utils.asIterable
import java.net.Inet4Address
import java.net.Inet6Address
class AclMatcher(acl: Acl) {
private val subnetsIpv4 = acl.subnets.asIterable().filter { it.address is Inet4Address }
private val subnetsIpv6 = acl.subnets.asIterable().filter { it.address is Inet6Address }
private val bypassDomains = acl.bypassHostnames.asIterable().map { it.toRegex() }
private val proxyDomains = acl.proxyHostnames.asIterable().map { it.toRegex() }
private val bypass = acl.bypass
fun shouldBypass(ip: Inet4Address) = bypass xor subnetsIpv4.any { it.matches(ip) }
fun shouldBypass(ip: Inet6Address) = bypass xor subnetsIpv6.any { it.matches(ip) }
fun shouldBypass(host: String): Boolean? {
if (bypassDomains.any { it.matches(host) }) return true
if (proxyDomains.any { it.matches(host) }) return false
return null
}
}
......@@ -234,7 +234,7 @@ object BaseService {
fun buildAdditionalArguments(cmd: ArrayList<String>): ArrayList<String> = cmd
suspend fun startProcesses(hosts: HostsFile) {
suspend fun startProcesses(hosts: HostsFile, acl: Lazy<Acl?>) {
val configRoot = (if (Build.VERSION.SDK_INT < 24 || app.getSystemService<UserManager>()
?.isUserUnlocked != false) app else Core.deviceStorage).noBackupFilesDir
val udpFallback = data.udpFallback
......@@ -349,19 +349,21 @@ object BaseService {
val hosts = HostsFile(DataStore.publicStore.getString(Key.hosts) ?: "")
proxy.init(this@Interface, hosts)
data.udpFallback?.init(this@Interface, hosts)
if (profile.route == Acl.CUSTOM_RULES) try {
val acl = if (profile.route == Acl.CUSTOM_RULES) try {
withContext(Dispatchers.IO) {
Acl.save(Acl.CUSTOM_RULES, Acl.customRules.flatten(10, this@Interface::openConnection))
}
Acl.customRules.flatten(10, this@Interface::openConnection).also {
Acl.save(Acl.CUSTOM_RULES, it)
}
}.let { lazy { it } }
} catch (e: IOException) {
throw ExpectedExceptionWrapper(e)
}
} else lazy { if (profile.route == Acl.ALL) null else Acl().fromId(profile.route) }
data.processes = GuardedProcessPool {
printLog(it)
stopRunner(false, it.readableMessage)
}
startProcesses(hosts)
startProcesses(hosts, acl)
proxy.scheduleUpdate()
data.udpFallback?.scheduleUpdate()
......
......@@ -20,13 +20,11 @@
package com.github.shadowsocks.bg
import com.github.shadowsocks.Core.app
import com.github.shadowsocks.acl.Acl
import com.github.shadowsocks.core.R
import com.github.shadowsocks.acl.AclMatcher
import com.github.shadowsocks.net.HostsFile
import com.github.shadowsocks.net.LocalDnsServer
import com.github.shadowsocks.net.Socks5Endpoint
import com.github.shadowsocks.net.Subnet
import com.github.shadowsocks.preference.DataStore
import kotlinx.coroutines.CoroutineScope
import java.net.InetSocketAddress
......@@ -35,24 +33,11 @@ import java.net.URISyntaxException
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 chinaIpv4List by lazy {
app.resources.openRawResource(R.raw.zone_cn).bufferedReader().lineSequence().map {
Subnet.fromString(it, 4)
}.filterNotNull().toList()
}
private val chinaIpv6List by lazy {
app.resources.openRawResource(R.raw.zone_cn_ipv6).bufferedReader().lineSequence().map {
Subnet.fromString(it, 16)
}.filterNotNull().toList()
}
private val servers = WeakHashMap<Interface, LocalDnsServer>()
interface Interface : BaseService.Interface {
override suspend fun startProcesses(hosts: HostsFile) {
super.startProcesses(hosts)
override suspend fun startProcesses(hosts: HostsFile, acl: Lazy<Acl?>) {
super.startProcesses(hosts, acl)
val profile = data.proxy!!.profile
val dns = try {
URI("dns://${profile.remoteDns}")
......@@ -62,18 +47,11 @@ object LocalDnsService {
LocalDnsServer(this::resolver,
Socks5Endpoint(dns.host, if (dns.port < 0) 53 else dns.port),
DataStore.proxyAddress,
hosts).apply {
tcp = !profile.udpdns
when (profile.route) {
Acl.BYPASS_CHN, Acl.BYPASS_LAN_CHN, Acl.GFWLIST, Acl.CUSTOM_RULES -> {
remoteDomainMatcher = googleApisTester
localIpv4Matcher = chinaIpv4List
localIpv6Matcher = chinaIpv6List
}
Acl.CHINALIST -> { }
else -> forwardOnly = true
}
}.also { servers[this] = it }.start(InetSocketAddress(DataStore.listenAddress, DataStore.portLocalDns))
hosts,
!profile.udpdns,
acl.value?.let { AclMatcher(it) }).also {
servers[this] = it
}.start(InetSocketAddress(DataStore.listenAddress, DataStore.portLocalDns))
}
override fun killProcesses(scope: CoroutineScope) {
......
......@@ -23,6 +23,7 @@ package com.github.shadowsocks.bg
import android.app.Service
import android.content.Intent
import com.github.shadowsocks.Core
import com.github.shadowsocks.acl.Acl
import com.github.shadowsocks.net.HostsFile
import com.github.shadowsocks.preference.DataStore
import java.io.File
......@@ -57,9 +58,9 @@ redsocks {
File(applicationInfo.nativeLibraryDir, Executable.REDSOCKS).absolutePath, "-c", "redsocks.conf"))
}
override suspend fun startProcesses(hosts: HostsFile) {
override suspend fun startProcesses(hosts: HostsFile, acl: Lazy<Acl?>) {
startRedsocksDaemon()
super.startProcesses(hosts)
super.startProcesses(hosts, acl)
}
override fun onDestroy() {
......
......@@ -144,9 +144,9 @@ class VpnService : BaseVpnService(), LocalDnsService.Interface {
override suspend fun resolver(host: String) = DnsResolverCompat.resolve(DefaultNetworkListener.get(), host)
override suspend fun openConnection(url: URL) = DefaultNetworkListener.get().openConnection(url)
override suspend fun startProcesses(hosts: HostsFile) {
override suspend fun startProcesses(hosts: HostsFile, acl: Lazy<Acl?>) {
worker = ProtectWorker().apply { start() }
super.startProcesses(hosts)
super.startProcesses(hosts, acl)
sendFd(startVpn())
}
......
......@@ -22,6 +22,7 @@ package com.github.shadowsocks.net
import android.util.Log
import com.crashlytics.android.Crashlytics
import com.github.shadowsocks.acl.AclMatcher
import com.github.shadowsocks.bg.BaseService
import com.github.shadowsocks.utils.printLog
import kotlinx.coroutines.*
......@@ -45,19 +46,12 @@ import java.nio.channels.SocketChannel
class LocalDnsServer(private val localResolver: suspend (String) -> Array<InetAddress>,
private val remoteDns: Socks5Endpoint,
private val proxy: SocketAddress,
private val hosts: HostsFile) : 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 localIpv4Matcher: List<Subnet> = emptyList()
var localIpv6Matcher: List<Subnet> = emptyList()
private val hosts: HostsFile,
/**
* Forward UDP queries to TCP.
*/
private val tcp: Boolean = false,
private val acl: AclMatcher? = null) : CoroutineScope {
companion object {
private const val TAG = "LocalDnsServer"
private const val TIMEOUT = 10_000L
......@@ -134,8 +128,12 @@ class LocalDnsServer(private val localResolver: suspend (String) -> Array<InetAd
remote.cancel()
return@supervisorScope cookDnsResponse(request, hostsResults)
}
if (forwardOnly) return@supervisorScope remote.await()
if (remoteDomainMatcher?.containsMatchIn(host) == true) return@supervisorScope remote.await()
if (acl == null) return@supervisorScope remote.await()
val useLocal = when (acl.shouldBypass(host)) {
true -> true.also { remote.cancel() }
false -> return@supervisorScope remote.await()
null -> false
}
val localResults = try {
withTimeout(TIMEOUT) { localResolver(host) }
} catch (_: TimeoutCancellationException) {
......@@ -144,12 +142,21 @@ class LocalDnsServer(private val localResolver: suspend (String) -> Array<InetAd
} catch (_: UnknownHostException) {
return@supervisorScope remote.await()
}
if (localResults.isEmpty()) return@supervisorScope remote.await()
val matcher = if (isIpv6) localIpv6Matcher else localIpv4Matcher
if (matcher.isEmpty() || matcher.any { subnet -> localResults.any(subnet::matches) }) {
remote.cancel()
cookDnsResponse(request, localResults.asIterable())
} else remote.await()
if (isIpv6) {
val filtered = localResults.filterIsInstance<Inet6Address>()
if (useLocal) return@supervisorScope cookDnsResponse(request, filtered)
if (filtered.any { acl.shouldBypass(it) }) {
remote.cancel()
cookDnsResponse(request, filtered)
} else remote.await()
} else {
val filtered = localResults.filterIsInstance<Inet4Address>()
if (useLocal) return@supervisorScope cookDnsResponse(request, filtered)
if (filtered.any { acl.shouldBypass(it) }) {
remote.cancel()
cookDnsResponse(request, filtered)
} else remote.await()
}
} catch (e: Exception) {
remote.cancel()
when (e) {
......
This diff is collapsed.
This diff is collapsed.
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