diff --git a/README.md b/README.md index 72f1b1fa..ddfe32c7 100644 --- a/README.md +++ b/README.md @@ -83,8 +83,6 @@ GPLv3
  • kotlin: APL 2.0
  • material: APL 2.0
  • gson: APL 2.0
  • -
  • dnsjava: BSD
  • -
  • jsocks: LGPL 2.1
  • preferencex-simplemenu: APL 2.0
  • android-plugin-api-for-locale: APL 2.0
  • qrgen: APL 2.0
  • diff --git a/core/build.gradle b/core/build.gradle index d9663061..14b98c8a 100644 --- a/core/build.gradle +++ b/core/build.gradle @@ -78,9 +78,7 @@ dependencies { api "androidx.work:work-runtime-ktx:2.7.1" api "androidx.work:work-multiprocess:2.7.1" api 'com.google.code.gson:gson:2.9.0' - api 'dnsjava:dnsjava:3.5.0' api "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.1" - implementation 'com.github.ssrlive:jsocks:v1.0.1' implementation 'androidx.core:core-ktx:1.7.0' kapt "androidx.room:room-compiler:2.4.2" } diff --git a/core/src/main/java/com/github/shadowsocks/bg/LocalDnsService.kt b/core/src/main/java/com/github/shadowsocks/bg/LocalDnsService.kt deleted file mode 100644 index d3c4e077..00000000 --- a/core/src/main/java/com/github/shadowsocks/bg/LocalDnsService.kt +++ /dev/null @@ -1,66 +0,0 @@ -/******************************************************************************* - * * - * Copyright (C) 2017 by Max Lv * - * Copyright (C) 2017 by Mygod Studio * - * * - * 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 . * - * * - *******************************************************************************/ - -package com.github.shadowsocks.bg - -import com.github.shadowsocks.acl.Acl -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.preference.DataStore -import kotlinx.coroutines.CoroutineScope -import java.net.InetSocketAddress -import java.net.URI -import java.net.URISyntaxException -import java.util.WeakHashMap - -object LocalDnsService { - private val servers = WeakHashMap() - - interface Interface : BaseService.Interface { - override suspend fun startProcesses(hosts: HostsFile) { - super.startProcesses(hosts) - val profile = data.proxy!!.profile - val dns = try { - URI("dns://${profile.remoteDns}") - } catch (e: URISyntaxException) { - throw BaseService.ExpectedExceptionWrapper(e) - } - LocalDnsServer( - this::resolver, - Socks5Endpoint(dns.host, if (dns.port < 0) 53 else dns.port), - DataStore.proxyAddress, - hosts, - !profile.udpdns, - if (profile.route == Acl.ALL) null else object { - suspend fun createAcl() = AclMatcher().apply { init(profile.route) } - }::createAcl - ).also { - servers[this] = it - }.start(InetSocketAddress(DataStore.listenAddress, DataStore.portLocalDns)) - } - - override fun killProcesses(scope: CoroutineScope) { - servers.remove(this)?.shutdown(scope) - super.killProcesses(scope) - } - } -} diff --git a/core/src/main/java/com/github/shadowsocks/bg/SsrVpnService.kt b/core/src/main/java/com/github/shadowsocks/bg/SsrVpnService.kt index 709cfa30..0624d298 100644 --- a/core/src/main/java/com/github/shadowsocks/bg/SsrVpnService.kt +++ b/core/src/main/java/com/github/shadowsocks/bg/SsrVpnService.kt @@ -48,7 +48,7 @@ import java.io.File import java.io.FileDescriptor import java.io.IOException -class SsrVpnService : VpnService(), LocalDnsService.Interface { +class SsrVpnService : VpnService(), BaseService.Interface { companion object { private const val VPN_MTU = 1500 private const val PRIVATE_VLAN4_CLIENT = "172.19.0.1" @@ -84,7 +84,7 @@ class SsrVpnService : VpnService(), LocalDnsService.Interface { override fun onBind(intent: Intent) = when (intent.action) { SERVICE_INTERFACE -> super.onBind(intent) - else -> super.onBind(intent) + else -> super.onBind(intent) } override fun onRevoke() = stopRunner() @@ -105,7 +105,9 @@ class SsrVpnService : VpnService(), LocalDnsService.Interface { if (DataStore.serviceMode == Key.modeVpn) { if (prepare(this) != null) { startActivity(Intent(this, VpnRequestActivity::class.java).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)) - } else return super.onStartCommand(intent, flags, startId) + } else { + return super.onStartCommand(intent, flags, startId) + } } stopRunner() return Service.START_NOT_STICKY diff --git a/core/src/main/java/com/github/shadowsocks/net/LocalDnsServer.kt b/core/src/main/java/com/github/shadowsocks/net/LocalDnsServer.kt deleted file mode 100644 index 9bfc5aa2..00000000 --- a/core/src/main/java/com/github/shadowsocks/net/LocalDnsServer.kt +++ /dev/null @@ -1,213 +0,0 @@ -/******************************************************************************* - * * - * Copyright (C) 2019 by Max Lv * - * Copyright (C) 2019 by Mygod Studio * - * * - * 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 . * - * * - *******************************************************************************/ - -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.* -import org.xbill.DNS.* -import java.io.IOException -import java.net.* -import java.nio.ByteBuffer -import java.nio.channels.DatagramChannel -import java.nio.channels.SelectionKey -import java.nio.channels.SocketChannel - -/** - * 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, - private val remoteDns: Socks5Endpoint, - private val proxy: SocketAddress, - private val hosts: HostsFile, - /** - * Forward UDP queries to TCP. - */ - private val tcp: Boolean = false, - aclSpawn: (suspend () -> AclMatcher)? = null) : CoroutineScope { - companion object { - private const val TAG = "LocalDnsServer" - 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 = 512 - - private fun prepareDnsResponse(request: Message) = Message(request.header.id).apply { - header.setFlag(Flags.QR.toInt()) // this is a response - if (request.header.getFlag(Flags.RD.toInt())) header.setFlag(Flags.RD.toInt()) - request.question?.also { addRecord(it, Section.QUESTION) } - } - - private fun cookDnsResponse(request: Message, results: Iterable) = - ByteBuffer.wrap(prepareDnsResponse(request).apply { - header.setFlag(Flags.RA.toInt()) // recursion available - for (address in results) addRecord(when (address) { - is Inet4Address -> ARecord(question.name, DClass.IN, TTL, address) - is Inet6Address -> AAAARecord(question.name, DClass.IN, TTL, address) - else -> error("Unsupported address $address") - }, Section.ANSWER) - }.toWire()) - } - - private val monitor = ChannelMonitor() - - override val coroutineContext = SupervisorJob() + CoroutineExceptionHandler { _, t -> - if (t is IOException) Crashlytics.log(Log.WARN, TAG, t.message) else printLog(t) - } - private val acl = aclSpawn?.let { async { it() } } - - suspend fun start(listen: SocketAddress) = DatagramChannel.open().run { - configureBlocking(false) - try { - socket().bind(listen) - } catch (e: BindException) { - throw BaseService.ExpectedExceptionWrapper(e) - } - monitor.register(this, SelectionKey.OP_READ) { handlePacket(this) } - } - - private fun handlePacket(channel: DatagramChannel) { - val buffer = ByteBuffer.allocateDirect(UDP_PACKET_SIZE) - val source = channel.receive(buffer)!! - buffer.flip() - launch { - val reply = resolve(buffer) - while (channel.send(reply, source) <= 0) monitor.wait(channel, SelectionKey.OP_WRITE) - } - } - - private suspend fun resolve(packet: ByteBuffer): ByteBuffer { - val request = try { - Message(packet) - } catch (e: IOException) { // we cannot parse the message, do not attempt to handle it at all - Crashlytics.log(Log.WARN, TAG, e.message) - return forward(packet) - } - return supervisorScope { - val remote = async { withTimeout(TIMEOUT) { forward(packet) } } - try { - if (request.header.opcode != Opcode.QUERY) return@supervisorScope remote.await() - val question = request.question - val isIpv6 = when (question?.type) { - Type.A -> false - Type.AAAA -> true - else -> return@supervisorScope remote.await() - } - val host = question.name.canonicalize().toString(true) - val hostsResults = hosts.resolve(host) - if (hostsResults.isNotEmpty()) { - remote.cancel() - return@supervisorScope cookDnsResponse(request, hostsResults.run { - if (isIpv6) filterIsInstance() else filterIsInstance() - }) - } - val acl = acl?.await() ?: 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) { - Crashlytics.log(Log.WARN, TAG, "Local resolving timed out, falling back to remote resolving") - return@supervisorScope remote.await() - } catch (_: UnknownHostException) { - return@supervisorScope remote.await() - } - if (isIpv6) { - val filtered = localResults.filterIsInstance() - if (useLocal) return@supervisorScope cookDnsResponse(request, filtered) - if (filtered.any { acl.shouldBypassIpv6(it.address) }) { - remote.cancel() - cookDnsResponse(request, filtered) - } else remote.await() - } else { - val filtered = localResults.filterIsInstance() - if (useLocal) return@supervisorScope cookDnsResponse(request, filtered) - if (filtered.any { acl.shouldBypassIpv4(it.address) }) { - remote.cancel() - cookDnsResponse(request, filtered) - } else remote.await() - } - } catch (e: Exception) { - remote.cancel() - when (e) { - is TimeoutCancellationException -> Crashlytics.log(Log.WARN, TAG, "Remote resolving timed out") - is CancellationException -> { } // ignore - is IOException -> Crashlytics.log(Log.WARN, TAG, e.message) - else -> printLog(e) - } - ByteBuffer.wrap(prepareDnsResponse(request).apply { - header.rcode = Rcode.SERVFAIL - }.toWire()) - } - } - } - - private suspend fun forward(packet: ByteBuffer): ByteBuffer { - packet.position(0) // the packet might have been parsed, reset to beginning - return if (tcp) SocketChannel.open().use { channel -> - channel.configureBlocking(false) - channel.connect(proxy) - val wrapped = remoteDns.tcpWrap(packet) - while (!channel.finishConnect()) monitor.wait(channel, SelectionKey.OP_CONNECT) - while (channel.write(wrapped) >= 0 && wrapped.hasRemaining()) monitor.wait(channel, SelectionKey.OP_WRITE) - val result = remoteDns.tcpReceiveBuffer(UDP_PACKET_SIZE) - remoteDns.tcpUnwrap(result, channel::read) { monitor.wait(channel, SelectionKey.OP_READ) } - result - } else DatagramChannel.open().use { channel -> - channel.configureBlocking(false) - monitor.wait(channel, SelectionKey.OP_WRITE) - check(channel.send(remoteDns.udpWrap(packet), proxy) > 0) - val result = remoteDns.udpReceiveBuffer(UDP_PACKET_SIZE) - while (isActive) { - monitor.wait(channel, SelectionKey.OP_READ) - if (channel.receive(result) == proxy) break - result.clear() - } - result.flip() - remoteDns.udpUnwrap(result) - result - } - } - - fun shutdown(scope: CoroutineScope) { - cancel() - monitor.close(scope) - scope.launch { - this@LocalDnsServer.coroutineContext[Job]!!.join() - acl?.also { it.await().close() } - } - } -} diff --git a/core/src/main/java/com/github/shadowsocks/net/Socks5Endpoint.kt b/core/src/main/java/com/github/shadowsocks/net/Socks5Endpoint.kt deleted file mode 100644 index f30b98a4..00000000 --- a/core/src/main/java/com/github/shadowsocks/net/Socks5Endpoint.kt +++ /dev/null @@ -1,128 +0,0 @@ -/******************************************************************************* - * * - * Copyright (C) 2019 by Max Lv * - * Copyright (C) 2019 by Mygod Studio * - * * - * 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 . * - * * - *******************************************************************************/ - -package com.github.shadowsocks.net - -import com.github.shadowsocks.utils.parseNumericAddress -import net.sourceforge.jsocks.Socks4Message -import net.sourceforge.jsocks.Socks5Message -import java.io.EOFException -import java.io.IOException -import java.net.Inet4Address -import java.net.Inet6Address -import java.nio.ByteBuffer -import kotlin.math.max - -class Socks5Endpoint(host: String, port: Int) { - private val dest = host.parseNumericAddress().let { numeric -> - val bytes = numeric?.address ?: host.toByteArray().apply { check(size < 256) { "Hostname too long" } } - val type = when (numeric) { - null -> Socks5Message.SOCKS_ATYP_DOMAINNAME - is Inet4Address -> Socks5Message.SOCKS_ATYP_IPV4 - is Inet6Address -> Socks5Message.SOCKS_ATYP_IPV6 - else -> error("Unsupported address type $numeric") - } - ByteBuffer.allocate(bytes.size + (if (numeric == null) 1 else 0) + 3).apply { - put(type.toByte()) - if (numeric == null) put(bytes.size.toByte()) - put(bytes) - putShort(port.toShort()) - } - }.array() - private val headerReserved = max(3 + 3 + 16, 3 + dest.size) - - fun tcpWrap(message: ByteBuffer): ByteBuffer { - check(message.remaining() < 65536) { "TCP message too large" } - return ByteBuffer.allocateDirect(8 + dest.size + message.remaining()).apply { - put(Socks5Message.SOCKS_VERSION.toByte()) - put(1) // nmethods - put(0) // no authentication required - // header - put(Socks5Message.SOCKS_VERSION.toByte()) - put(Socks4Message.REQUEST_CONNECT.toByte()) - put(0) // reserved - put(dest) - // data - putShort(message.remaining().toShort()) - put(message) - flip() - } - } - fun tcpReceiveBuffer(size: Int) = ByteBuffer.allocateDirect(headerReserved + 4 + size) - suspend fun tcpUnwrap(buffer: ByteBuffer, reader: (ByteBuffer) -> Int, wait: suspend () -> Unit) { - suspend fun readBytes(till: Int) { - if (buffer.position() >= till) return - while (reader(buffer) >= 0 && buffer.position() < till) wait() - if (buffer.position() < till) throw EOFException("${buffer.position()} < $till") - } - suspend fun read(index: Int): Byte { - readBytes(index + 1) - return buffer[index] - } - if (read(0) != Socks5Message.SOCKS_VERSION.toByte()) throw IOException("Unsupported SOCKS version ${buffer[0]}") - if (read(1) != 0.toByte()) throw IOException("Unsupported authentication ${buffer[1]}") - if (read(2) != Socks5Message.SOCKS_VERSION.toByte()) throw IOException("Unsupported SOCKS version ${buffer[2]}") - if (read(3) != 0.toByte()) throw IOException("SOCKS5 server returned error ${buffer[3]}") - val dataOffset = when (val type = read(5)) { - Socks5Message.SOCKS_ATYP_IPV4.toByte() -> 4 - Socks5Message.SOCKS_ATYP_DOMAINNAME.toByte() -> 1 + read(6) - Socks5Message.SOCKS_ATYP_IPV6.toByte() -> 16 - else -> throw IOException("Unsupported address type $type") - } + 8 - readBytes(dataOffset + 2) - buffer.limit(buffer.position()) // store old position to update mark - buffer.position(dataOffset) - val dataLength = buffer.short.toUShort().toInt() - val end = buffer.position() + dataLength - if (end > buffer.capacity()) throw IOException( - "Buffer too small to contain the message: $dataLength > ${buffer.capacity() - buffer.position()}") - buffer.mark() - buffer.position(buffer.limit()) // restore old position - buffer.limit(end) - readBytes(buffer.limit()) - buffer.reset() - } - - private fun ByteBuffer.tryPosition(newPosition: Int) { - if (limit() < newPosition) throw EOFException("${limit()} < $newPosition") - position(newPosition) - } - - fun udpWrap(packet: ByteBuffer) = ByteBuffer.allocateDirect(3 + dest.size + packet.remaining()).apply { - // header - putShort(0) // reserved - put(0) // fragment number - put(dest) - // data - put(packet) - flip() - } - fun udpReceiveBuffer(size: Int) = ByteBuffer.allocateDirect(headerReserved + size) - fun udpUnwrap(packet: ByteBuffer) { - packet.tryPosition(3) - packet.tryPosition(6 + when (val type = packet.get()) { - Socks5Message.SOCKS_ATYP_IPV4.toByte() -> 4 - Socks5Message.SOCKS_ATYP_DOMAINNAME.toByte() -> 1 + packet.get() - Socks5Message.SOCKS_ATYP_IPV6.toByte() -> 16 - else -> throw IOException("Unsupported address type $type") - }) - packet.mark() - } -} diff --git a/mobile/src/main/res/raw/about.html b/mobile/src/main/res/raw/about.html index e62e352a..6d5d988f 100644 --- a/mobile/src/main/res/raw/about.html +++ b/mobile/src/main/res/raw/about.html @@ -31,8 +31,6 @@

    Open Source Licenses

  • kotlin: APL 2.0 
  • material: APL 2.0 
  • gson: APL 2.0 
  • -
  • dnsjava: BSD 
  • -
  • jsocks: LGPL 2.1 
  • preferencex-simplemenu: APL 2.0 
  • android-plugin-api-for-locale: APL 2.0 
  • qrgen: APL 2.0