From b1d22f3c3c085363de65487e36f7687eca0d849f Mon Sep 17 00:00:00 2001 From: Kr328 <39107975+Kr328@users.noreply.github.com> Date: Tue, 19 Nov 2019 23:20:50 +0800 Subject: [PATCH] clash service --- .../github/kr328/clash/IClashObserver.aidl | 7 ++ .../com/github/kr328/clash/IClashService.aidl | 4 +- .../github/kr328/clash/model/ClashStatus.aidl | 3 + .../com/github/kr328/clash/ClashService.kt | 25 +++-- .../java/com/github/kr328/clash/TunService.kt | 19 +++- .../github/kr328/clash/model/ClashStatus.kt | 38 ++++++++ build.gradle | 1 - buildSrc/src/main/java/MMDBDowloadTask.kt | 41 ++++++++ core/build.gradle | 36 ++++++- core/src/main/golang/profile/load.go | 20 ++++ core/src/main/golang/tun/tun.go | 20 +++- .../java/com/github/kr328/clash/core/Clash.kt | 7 +- .../github/kr328/clash/core/ClashProcess.kt | 97 +++++++++++++------ gradlew | 0 14 files changed, 267 insertions(+), 51 deletions(-) create mode 100644 app/src/main/aidl/com/github/kr328/clash/IClashObserver.aidl create mode 100644 app/src/main/aidl/com/github/kr328/clash/model/ClashStatus.aidl create mode 100644 app/src/main/java/com/github/kr328/clash/model/ClashStatus.kt create mode 100644 buildSrc/src/main/java/MMDBDowloadTask.kt mode change 100644 => 100755 gradlew diff --git a/app/src/main/aidl/com/github/kr328/clash/IClashObserver.aidl b/app/src/main/aidl/com/github/kr328/clash/IClashObserver.aidl new file mode 100644 index 0000000..77ce192 --- /dev/null +++ b/app/src/main/aidl/com/github/kr328/clash/IClashObserver.aidl @@ -0,0 +1,7 @@ +package com.github.kr328.clash; + +import com.github.kr328.clash.model.ClashStatus; + +interface IClashObserver { + void onStatusChanged(in ClashStatus status); +} diff --git a/app/src/main/aidl/com/github/kr328/clash/IClashService.aidl b/app/src/main/aidl/com/github/kr328/clash/IClashService.aidl index 5bbbff0..ec9a24f 100644 --- a/app/src/main/aidl/com/github/kr328/clash/IClashService.aidl +++ b/app/src/main/aidl/com/github/kr328/clash/IClashService.aidl @@ -2,6 +2,8 @@ package com.github.kr328.clash; interface IClashService { - void loadProfile(String path); + void start(); + void stop(); + void loadProfile(in Uri path); void startTunDevice(in ParcelFileDescriptor fd, int mtu); } diff --git a/app/src/main/aidl/com/github/kr328/clash/model/ClashStatus.aidl b/app/src/main/aidl/com/github/kr328/clash/model/ClashStatus.aidl new file mode 100644 index 0000000..4fdc488 --- /dev/null +++ b/app/src/main/aidl/com/github/kr328/clash/model/ClashStatus.aidl @@ -0,0 +1,3 @@ +package com.github.kr328.clash; + +parcelable ClashStatus; \ No newline at end of file diff --git a/app/src/main/java/com/github/kr328/clash/ClashService.kt b/app/src/main/java/com/github/kr328/clash/ClashService.kt index 44800d2..757404b 100644 --- a/app/src/main/java/com/github/kr328/clash/ClashService.kt +++ b/app/src/main/java/com/github/kr328/clash/ClashService.kt @@ -6,6 +6,7 @@ import android.net.Uri import android.os.IBinder import android.os.Parcel import android.os.ParcelFileDescriptor +import android.os.RemoteException import android.util.Log import com.github.kr328.clash.core.Clash import com.github.kr328.clash.core.ClashProcess @@ -19,8 +20,16 @@ class ClashService : Service() { private lateinit var clash: Clash private inner class ClashServiceImpl : IClashService.Stub() { - override fun loadProfile(url: String?) { - clash.loadProfile(Uri.parse(url)) + override fun start() { + clash.start() + } + + override fun stop() { + clash.stop() + } + + override fun loadProfile(url: Uri?) { + clash.loadProfile(url ?: throw RemoteException()) } override fun startTunDevice(fd: ParcelFileDescriptor, mtu: Int) { @@ -33,14 +42,8 @@ class ClashService : Service() { clash = Clash(this, filesDir.resolve("clash"), - cacheDir.resolve("clash_controller")) - - Log.d(TAG, "ping " + clash.ping()) - - thread { - clash.exec() - - Log.i(TAG, "Clash exited") + cacheDir.resolve("clash_controller")) { + } } @@ -49,6 +52,8 @@ class ClashService : Service() { } override fun onDestroy() { + clash.stop() + super.onDestroy() } } \ No newline at end of file diff --git a/app/src/main/java/com/github/kr328/clash/TunService.kt b/app/src/main/java/com/github/kr328/clash/TunService.kt index 744bc8f..4c69eca 100644 --- a/app/src/main/java/com/github/kr328/clash/TunService.kt +++ b/app/src/main/java/com/github/kr328/clash/TunService.kt @@ -11,6 +11,15 @@ import android.os.ParcelFileDescriptor import java.lang.RuntimeException class TunService : VpnService() { + companion object { + // from https://github.com/shadowsocks/shadowsocks-android/blob/master/core/src/main/java/com/github/shadowsocks/bg/VpnService.kt + private const val VPN_MTU = 1500 + private const val PRIVATE_VLAN4_CLIENT = "172.19.0.1" + 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" + } + private lateinit var fileDescriptor: ParcelFileDescriptor private val connection = object: ServiceConnection { override fun onServiceDisconnected(name: ComponentName?) { @@ -18,7 +27,7 @@ class TunService : VpnService() { } override fun onServiceConnected(name: ComponentName?, service: IBinder?) { - IClashService.Stub.asInterface(service)?.startTunDevice(fileDescriptor, 1500) ?: stopSelf() + IClashService.Stub.asInterface(service)?.startTunDevice(fileDescriptor, VPN_MTU) ?: stopSelf() } } @@ -26,10 +35,14 @@ class TunService : VpnService() { super.onStartCommand(intent, flags, startId) fileDescriptor = Builder() - .addAddress("10.0.0.1", 24) + .addAddress(PRIVATE_VLAN4_CLIENT, 30) + .addDnsServer(PRIVATE_VLAN4_ROUTER) + .addAddress(PRIVATE_VLAN6_CLIENT, 126) + .addDnsServer(PRIVATE_VLAN6_ROUTER) .addDisallowedApplication(packageName) .addRoute("0.0.0.0", 0) - .setMtu(1500) + .addRoute("::", 0) + .setMtu(VPN_MTU) .setBlocking(false) .establish() ?: throw RuntimeException("Unable to establish VPN") diff --git a/app/src/main/java/com/github/kr328/clash/model/ClashStatus.kt b/app/src/main/java/com/github/kr328/clash/model/ClashStatus.kt new file mode 100644 index 0000000..26cbc76 --- /dev/null +++ b/app/src/main/java/com/github/kr328/clash/model/ClashStatus.kt @@ -0,0 +1,38 @@ +package com.github.kr328.clash.model + +import android.os.Parcel +import android.os.Parcelable +import com.github.kr328.clash.core.ClashProcess + +data class ClashStatus(val status: ClashProcess.Status) : Parcelable { + override fun writeToParcel(parcel: Parcel, flags: Int) { + when ( status ) { + ClashProcess.Status.STOPPED -> parcel.writeInt(0) + ClashProcess.Status.STARTED -> parcel.writeInt(1) + } + } + + override fun describeContents(): Int { + return 0 + } + + companion object CREATOR : Parcelable.Creator { + override fun createFromParcel(parcel: Parcel): ClashStatus { + return ClashStatus(readFromParcel(parcel)) + } + + override fun newArray(size: Int): Array { + return arrayOfNulls(size) + } + + private fun readFromParcel(parcel: Parcel): ClashProcess.Status { + return when ( parcel.readInt() ) { + 0 -> ClashProcess.Status.STOPPED + 1 -> ClashProcess.Status.STARTED + else -> throw IllegalArgumentException() + } + } + } + + +} \ No newline at end of file diff --git a/build.gradle b/build.gradle index d068cef..88f44b3 100644 --- a/build.gradle +++ b/build.gradle @@ -19,7 +19,6 @@ allprojects { repositories { google() jcenter() - } } diff --git a/buildSrc/src/main/java/MMDBDowloadTask.kt b/buildSrc/src/main/java/MMDBDowloadTask.kt new file mode 100644 index 0000000..45af3a2 --- /dev/null +++ b/buildSrc/src/main/java/MMDBDowloadTask.kt @@ -0,0 +1,41 @@ +import org.gradle.api.DefaultTask +import org.gradle.api.GradleException +import org.gradle.api.tasks.TaskAction +import java.io.File +import java.io.FileOutputStream +import java.net.HttpURLConnection +import java.net.URL + +open class MMDBDowloadTask : DefaultTask() { + companion object { + const val URL = "http://geolite.maxmind.com/download/geoip/database/GeoLite2-Country.tar.gz" + } + + var output: String = "" + + @TaskAction + fun exec() { + val file = File(output).apply { + parentFile?.mkdirs() + } + + try { + (URL(URL).openConnection() as HttpURLConnection).apply { + instanceFollowRedirects = true + + connect() + require(responseCode / 100 == 2) + + FileOutputStream(file).use { + inputStream.copyTo(it) + } + + disconnect() + } + } + catch (e: Throwable) { + e.printStackTrace() + throw GradleException("Download failure", e) + } + } +} \ No newline at end of file diff --git a/core/build.gradle b/core/build.gradle index 34e1510..025aa05 100644 --- a/core/build.gradle +++ b/core/build.gradle @@ -24,6 +24,7 @@ android { sourceSets { main { + assets.srcDirs += ["$buildDir/intermediates/dynamic_assets"] jniLibs.srcDirs += ["$buildDir/intermediates/native_output"] } } @@ -34,7 +35,6 @@ dependencies { implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" } - afterEvaluate { def compileArch = ["arm64-v8a", "x86_64"] def options = new GolangBuildOptions(file("src/main/golang"), file("$buildDir/intermediates/native_output"), "23") @@ -49,7 +49,41 @@ afterEvaluate { if ( task.name.contains("JniLib") ) task.dependsOn(ts) } + + def ds = tasks.register("downloadMMDB", MMDBDowloadTask.class) { + onlyIf { + System.currentTimeMillis() - file("$buildDir/intermediates/cache/Country.tar.gz").lastModified() > 30 * 24 * 3600L + } + + output = "$buildDir/intermediates/cache/Country.tar.gz" + } + + def es = tasks.register("extractMMDB", Copy.class) { + onlyIf { + file("$buildDir/intermediates/cache/Country.tar.gz").lastModified() > file("$buildDir/intermediates/dynamic_assets/Country.mmdb").lastModified() + } + + from(tarTree("$buildDir/intermediates/cache/Country.tar.gz")) { + include "**/*.mmdb" + } + into "$buildDir/intermediates/cache/mmdb" + + doLast { + fileTree("$buildDir/intermediates/cache/mmdb").visit { FileVisitDetails details -> + if ( details.path.endsWith("Country.mmdb") ) + details.file.renameTo(file("$buildDir/intermediates/dynamic_assets/Country.mmdb")) + } + } + + dependsOn ds + } + + for ( task in tasks ) { + if ( task.name.contains("generate") && task.name.contains("Assets") ) + task.dependsOn(es) + } } + repositories { mavenCentral() } \ No newline at end of file diff --git a/core/src/main/golang/profile/load.go b/core/src/main/golang/profile/load.go index c2068cb..d714281 100644 --- a/core/src/main/golang/profile/load.go +++ b/core/src/main/golang/profile/load.go @@ -2,16 +2,20 @@ package profile import ( "net" + "strings" adapters "github.com/Dreamacro/clash/adapters/outbound" "github.com/Dreamacro/clash/component/auth" trie "github.com/Dreamacro/clash/component/domain-trie" + "github.com/Dreamacro/clash/component/socks5" "github.com/Dreamacro/clash/config" "github.com/Dreamacro/clash/constant" "github.com/Dreamacro/clash/dns" "github.com/Dreamacro/clash/hub/executor" "github.com/Dreamacro/clash/log" "github.com/Dreamacro/clash/tunnel" + + "github.com/kr328/clash/tun" ) // LoadDefault - load default configure @@ -60,6 +64,8 @@ func LoadDefault() { defaultC.Proxies["REJECT"] = reject defaultC.Proxies["GLOBAL"] = adapters.NewProxy(global) + tun.SetDNSRedirect(nil) + executor.ApplyConfig(defaultC, true) } @@ -71,5 +77,19 @@ func LoadFromFile(path string) error { } executor.ApplyConfig(cfg, true) + + if cfg.DNS != nil && cfg.DNS.Enable && dns.DefaultResolver != nil && dns.ReCreateServer(cfg.DNS.Listen, dns.DefaultResolver) == nil { + addr := cfg.DNS.Listen + addr = strings.ReplaceAll(addr, "0.0.0.0", "127.0.0.1") + addr = strings.ReplaceAll(addr, "::", "::1") + + if err == nil { + listen := socks5.ParseAddr(addr) + tun.SetDNSRedirect(&listen) + } + } else { + tun.SetDNSRedirect(nil) + } + return nil } diff --git a/core/src/main/golang/tun/tun.go b/core/src/main/golang/tun/tun.go index 4bb6425..ad79754 100644 --- a/core/src/main/golang/tun/tun.go +++ b/core/src/main/golang/tun/tun.go @@ -30,8 +30,9 @@ type tun struct { } var ( - tunnel = T.Instance() - instance *tun + tunnel = T.Instance() + instance *tun + dnsRedirectAddr *socks5.Addr ) // StartTunProxy - start @@ -55,6 +56,11 @@ func StopTunProxy() { instance = nil } +// SetDNSRedirect - set dns +func SetDNSRedirect(addr *socks5.Addr) { + dnsRedirectAddr = addr +} + func newTunProxy(fd int, mtu int) (*tun, error) { tundev, err := openTunDevice(fd, mtu) if err != nil { @@ -94,7 +100,7 @@ func newTunProxy(fd int, mtu int) (*tun, error) { r.Complete(false) conn := gonet.NewConn(&wq, ep) - target := getAddr(ep.Info().(*tcp.EndpointInfo).ID) + target := getAddr(ep.Info().(*tcp.EndpointInfo).ID, false) tunnel.Add(adapters.NewSocket(target, conn, C.REDIR, C.TCP)) }) @@ -109,7 +115,7 @@ func newTunProxy(fd int, mtu int) (*tun, error) { } conn := gonet.NewConn(&wq, ep) - target := getAddr(ep.Info().(*stack.TransportEndpointInfo).ID) + target := getAddr(ep.Info().(*stack.TransportEndpointInfo).ID, true) tunnel.Add(adapters.NewSocket(target, conn, C.REDIR, C.UDP)) }) @@ -131,9 +137,13 @@ func (t *tun) close() { t.ipstack.Close() } -func getAddr(id stack.TransportEndpointID) socks5.Addr { +func getAddr(id stack.TransportEndpointID, dnsRedirect bool) socks5.Addr { ipv4 := id.LocalAddress.To4() + if id.LocalPort == 53 && dnsRedirectAddr != nil && dnsRedirect { + return *dnsRedirectAddr + } + // get the big-endian binary represent of port port := make([]byte, 2) binary.BigEndian.PutUint16(port, id.LocalPort) diff --git a/core/src/main/java/com/github/kr328/clash/core/Clash.kt b/core/src/main/java/com/github/kr328/clash/core/Clash.kt index bbabc60..35fc20b 100644 --- a/core/src/main/java/com/github/kr328/clash/core/Clash.kt +++ b/core/src/main/java/com/github/kr328/clash/core/Clash.kt @@ -9,8 +9,11 @@ import android.util.Log import org.json.JSONObject import java.io.* -class Clash(private val context: Context, clashDir: File, private val controllerPath: File) : - ClashProcess(context, clashDir, controllerPath) { +class Clash(private val context: Context, + clashDir: File, + private val controllerPath: File, + listener: (Status) -> Unit) : + ClashProcess(context, clashDir, controllerPath, listener) { companion object { const val COMMAND_PING = 0 const val COMMAND_TUN_START = 1 diff --git a/core/src/main/java/com/github/kr328/clash/core/ClashProcess.kt b/core/src/main/java/com/github/kr328/clash/core/ClashProcess.kt index 70f2f6f..88f1544 100644 --- a/core/src/main/java/com/github/kr328/clash/core/ClashProcess.kt +++ b/core/src/main/java/com/github/kr328/clash/core/ClashProcess.kt @@ -1,30 +1,47 @@ package com.github.kr328.clash.core import android.content.Context +import android.os.Parcelable import android.util.Log import java.io.File +import java.io.FileOutputStream import java.io.IOException +import kotlin.concurrent.thread -abstract class ClashProcess(context: Context, clashDir: File, controllerPath: File) { +abstract class ClashProcess(private val context: Context, + private val clashDir: File, + private val controllerPath: File, + private val listener: (Status) -> Unit) { companion object { const val TAG = "ClashForAndroid" + private const val CLASH_COMMAND = "\"{CLASH}\" \"{CONTROLLER}\" 2>&1" - private val PID_PATTERN = Regex("PID=(\\d+)") + private const val CONTROLLER_STATUS_PREFIX = "[CONTROLLER]" + + private val PID_PATTERN = Regex("PID=(\\d+)") private val CONTROLLER_ERROR_PATTERN = Regex("\\[CONTROLLER] ERROR=\\{(.+)\\}") } - @set:Synchronized @get:Synchronized - var exited: Boolean = false - private val pid: Int - private val process: Process + enum class Status { + STARTED, STOPPED + } + + private var pid: Int = 0 + private var process: Process? = null + + @Synchronized + fun start() { + if ( pid > 0 ) + return + + extractMMDB() - init { clashDir.mkdirs() controllerPath.parentFile?.mkdirs() controllerPath.delete() - process = ProcessBuilder().apply { + val p = ProcessBuilder().apply { command("sh") directory(clashDir) }.start() @@ -36,22 +53,23 @@ abstract class ClashProcess(context: Context, clashDir: File, controllerPath: Fi Log.i(TAG, "Starting clash [$command]") - process.outputStream.write("echo PID=$$\n".toByteArray()) - process.outputStream.write("exec $command\n".toByteArray()) - process.outputStream.write("exit\n".toByteArray()) - process.outputStream.flush() + p.outputStream.use { + it.write("echo PID=$$\n".toByteArray()) + it.write("exec $command\n".toByteArray()) + it.write("exit\n".toByteArray()) + it.flush() + } var line = "" // Parse pid var currentPid = 0 - while ( process.inputStream.bufferedReader().readLine()?.apply { line = this.trim() } != null ) { + while ( p.inputStream.bufferedReader().readLine()?.apply { line = this.trim() } != null ) { if ( PID_PATTERN.matchEntire(line)?.apply { currentPid = groups[1]!!.value.toInt() } != null ) break } - pid = currentPid - while ( process.inputStream.bufferedReader().readLine()?.apply { line = this.trim() } != null ) { + while ( p.inputStream.bufferedReader().readLine()?.apply { line = this.trim() } != null ) { if ( line.startsWith(CONTROLLER_STATUS_PREFIX) ) { val error = CONTROLLER_ERROR_PATTERN.matchEntire(line)?.groups?.get(1)?.value @@ -62,24 +80,47 @@ abstract class ClashProcess(context: Context, clashDir: File, controllerPath: Fi } } - Log.i(TAG, "Clash started pid=$pid") + process = p + pid = currentPid + + listener(Status.STARTED) + + Log.i(TAG, "Clash started pid = $pid") + + thread { + // Redirect stdout to log + while ( p.inputStream.bufferedReader().readLine()?.apply { line = this } != null ) { + Log.i(TAG, line.trim()) + } + + synchronized(this@ClashProcess) { + p.destroy() + process = null + pid = -1 + } + + listener(Status.STOPPED) + } } - @Throws(IOException::class) - fun exec() { - var line = "" - - // Redirect stdout to log - while ( process.inputStream.bufferedReader().readLine()?.apply { line = this } != null ) { - Log.i(TAG, line.trim()) - } - - synchronized(this@ClashProcess) { - exited = true - } + @Synchronized + fun isRunning(): Boolean { + return pid > 0 } fun stop() { android.os.Process.killProcess(pid) } + + private fun extractMMDB() { + if ( context.packageManager.getPackageInfo(context.packageName, 0).lastUpdateTime + < clashDir.resolve("Country.mmdb").lastModified() ) + return + + context.resources.assets.open("Country.mmdb").use { input -> + FileOutputStream(clashDir.resolve("Country.mmdb")).use { output -> + input.copyTo(output) + } + } + } } \ No newline at end of file diff --git a/gradlew b/gradlew old mode 100644 new mode 100755