clash service

This commit is contained in:
Kr328 committed 2019-11-19 23:20:50 +08:00
1 parent e8a3335e21
commit b1d22f3c3c
14 files changed
+267 -51

No files matched your search

@@ -0,0 +1,7 @@
package com.github.kr328.clash;
import com.github.kr328.clash.model.ClashStatus;
interface IClashObserver {
void onStatusChanged(in ClashStatus status);
}
@@ -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);
}
@@ -0,0 +1,3 @@
package com.github.kr328.clash;
parcelable ClashStatus;
@@ -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()
}
}
@@ -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")
@@ -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<ClashStatus> {
override fun createFromParcel(parcel: Parcel): ClashStatus {
return ClashStatus(readFromParcel(parcel))
}
override fun newArray(size: Int): Array<ClashStatus?> {
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()
}
}
}
}
-1
View File
@@ -19,7 +19,6 @@ allprojects {
repositories {
google()
jcenter()
}
}
+41
View File
@@ -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)
}
}
}
+35 -1
View File
@@ -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()
}
+20
View File
@@ -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
}
+15 -5
View File
@@ -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)
@@ -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
@@ -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)
}
}
}
}
Vendored Regular → Executable
View File
File mode changed.