clash service
This commit is contained in:
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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -19,7 +19,6 @@ allprojects {
|
||||
repositories {
|
||||
google()
|
||||
jcenter()
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
@@ -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()
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user