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 aa47b66..5bbbff0 100644 --- a/app/src/main/aidl/com/github/kr328/clash/IClashService.aidl +++ b/app/src/main/aidl/com/github/kr328/clash/IClashService.aidl @@ -3,5 +3,5 @@ package com.github.kr328.clash; interface IClashService { void loadProfile(String path); - void startTunDevice(int fd, int mtu); + void startTunDevice(in ParcelFileDescriptor fd, int mtu); } 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 b6a3d00..44800d2 100644 --- a/app/src/main/java/com/github/kr328/clash/ClashService.kt +++ b/app/src/main/java/com/github/kr328/clash/ClashService.kt @@ -2,10 +2,14 @@ package com.github.kr328.clash import android.app.Service import android.content.Intent +import android.net.Uri import android.os.IBinder +import android.os.Parcel +import android.os.ParcelFileDescriptor import android.util.Log import com.github.kr328.clash.core.Clash import com.github.kr328.clash.core.ClashProcess +import java.io.FileDescriptor import kotlin.concurrent.thread class ClashService : Service() { @@ -15,12 +19,12 @@ class ClashService : Service() { private lateinit var clash: Clash private inner class ClashServiceImpl : IClashService.Stub() { - override fun loadProfile(path: String?) { - + override fun loadProfile(url: String?) { + clash.loadProfile(Uri.parse(url)) } - override fun startTunDevice(fd: Int, mtu: Int) { - + override fun startTunDevice(fd: ParcelFileDescriptor, mtu: Int) { + clash.startTunDevice(fd.fileDescriptor, mtu) } } @@ -31,6 +35,7 @@ class ClashService : Service() { filesDir.resolve("clash"), cacheDir.resolve("clash_controller")) + Log.d(TAG, "ping " + clash.ping()) thread { clash.exec() @@ -44,8 +49,6 @@ class ClashService : Service() { } override fun onDestroy() { - - super.onDestroy() } } \ No newline at end of file diff --git a/app/src/main/java/com/github/kr328/clash/MainActivity.kt b/app/src/main/java/com/github/kr328/clash/MainActivity.kt index 191d578..85119f5 100644 --- a/app/src/main/java/com/github/kr328/clash/MainActivity.kt +++ b/app/src/main/java/com/github/kr328/clash/MainActivity.kt @@ -8,13 +8,9 @@ import android.content.ServiceConnection import android.net.VpnService import android.os.Bundle import android.os.IBinder -import android.util.Log import android.widget.Toast import androidx.appcompat.app.AppCompatActivity import kotlinx.android.synthetic.main.activity_main.* -import java.io.FileOutputStream -import java.lang.Exception -import java.lang.RuntimeException import kotlin.concurrent.thread class MainActivity : AppCompatActivity() { @@ -23,7 +19,7 @@ class MainActivity : AppCompatActivity() { private const val VPN_REQUEST = 234 } - private val connection = object: ServiceConnection { + private val connection = object : ServiceConnection { override fun onServiceDisconnected(name: ComponentName?) { Toast.makeText(this@MainActivity, "Disconnected", Toast.LENGTH_LONG).show() } @@ -34,11 +30,8 @@ class MainActivity : AppCompatActivity() { clash.loadProfile(cacheDir.resolve("config.yaml").absolutePath) -// VpnService.prepare(this@MainActivity)?.apply { -// startActivityForResult(this, VPN_REQUEST) -// } ?: startService(Intent(this@MainActivity, TunService::class.java)) - } - catch (e: Exception) { + + } catch (e: Exception) { runOnUiThread { Toast.makeText(this@MainActivity, e.toString(), Toast.LENGTH_LONG).show() } @@ -57,23 +50,28 @@ class MainActivity : AppCompatActivity() { } override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { - if ( requestCode == REQUEST_CODE && resultCode == Activity.RESULT_OK ) { + if (requestCode == REQUEST_CODE && resultCode == Activity.RESULT_OK) { Toast.makeText(this, intent?.dataString, Toast.LENGTH_LONG).show() thread { - FileOutputStream(cacheDir.resolve("config.yaml")).use { - contentResolver.openInputStream(data?.data ?: throw RuntimeException())?.apply { copyTo(it) }?.close() - } + bindService(Intent(this, ClashService::class.java), object : ServiceConnection { + override fun onServiceDisconnected(name: ComponentName?) { + Toast.makeText(this@MainActivity, "Disconnected", Toast.LENGTH_LONG).show() + } - //startService(Intent(this, ClashService::class.java)) + override fun onServiceConnected(name: ComponentName?, service: IBinder?) { + IClashService.Stub.asInterface(service)?.loadProfile(data?.dataString) - bindService(Intent(this, ClashService::class.java), connection, Context.BIND_AUTO_CREATE) + VpnService.prepare(this@MainActivity)?.apply { + startActivityForResult(this, VPN_REQUEST) + } ?: startService(Intent(this@MainActivity, TunService::class.java)) + } + + }, Context.BIND_AUTO_CREATE) } - } - else if ( requestCode == VPN_REQUEST && resultCode == Activity.RESULT_OK ) { + } else if (requestCode == VPN_REQUEST && resultCode == Activity.RESULT_OK) { startService(Intent(this, TunService::class.java)) - } - else { + } else { super.onActivityResult(requestCode, resultCode, data) } } 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 a2364e0..744bc8f 100644 --- a/app/src/main/java/com/github/kr328/clash/TunService.kt +++ b/app/src/main/java/com/github/kr328/clash/TunService.kt @@ -18,7 +18,7 @@ class TunService : VpnService() { } override fun onServiceConnected(name: ComponentName?, service: IBinder?) { - IClashService.Stub.asInterface(service)?.startTunDevice(fileDescriptor.fd, 1500) ?: stopSelf() + IClashService.Stub.asInterface(service)?.startTunDevice(fileDescriptor, 1500) ?: stopSelf() } } @@ -35,7 +35,7 @@ class TunService : VpnService() { bindService(Intent(this, ClashService::class.java), connection, Context.BIND_AUTO_CREATE) - return Service.START_STICKY + return Service.START_NOT_STICKY } override fun onDestroy() { diff --git a/core/src/main/golang/main.go b/core/src/main/golang/main.go index 5570bb3..522d062 100644 --- a/core/src/main/golang/main.go +++ b/core/src/main/golang/main.go @@ -23,9 +23,9 @@ func main() { } else { return } - - if err := server.Start(os.Args[1]) ; err != nil { - fmt.Println("[CONTROLLER] ERROR={"+ err.Error() +"}") + + if err := server.Start(os.Args[1]); err != nil { + fmt.Println("[CONTROLLER] ERROR={" + err.Error() + "}") return } diff --git a/core/src/main/golang/profile/load.go b/core/src/main/golang/profile/load.go index 2084919..c2068cb 100644 --- a/core/src/main/golang/profile/load.go +++ b/core/src/main/golang/profile/load.go @@ -52,10 +52,12 @@ func LoadDefault() { Proxies: map[string]constant.Proxy{}, } - global, _ := adapters.NewSelector("GLOBAL", make([]constant.Proxy, 0)) + reject := adapters.NewProxy(adapters.NewReject()) + direct := adapters.NewProxy(adapters.NewDirect()) + global, _ := adapters.NewSelector("GLOBAL", []constant.Proxy{direct}) - defaultC.Proxies["DIRECT"] = adapters.NewProxy(adapters.NewDirect()) - defaultC.Proxies["REJECT"] = adapters.NewProxy(adapters.NewReject()) + defaultC.Proxies["DIRECT"] = direct + defaultC.Proxies["REJECT"] = reject defaultC.Proxies["GLOBAL"] = adapters.NewProxy(global) executor.ApplyConfig(defaultC, true) @@ -67,7 +69,7 @@ func LoadFromFile(path string) error { if err != nil { return err } - + executor.ApplyConfig(cfg, true) return nil } diff --git a/core/src/main/golang/server/profile.go b/core/src/main/golang/server/profile.go index 9ec1173..fe1a394 100644 --- a/core/src/main/golang/server/profile.go +++ b/core/src/main/golang/server/profile.go @@ -12,6 +12,8 @@ import ( func handleProfileDefault(client *net.UnixConn) { profile.LoadDefault() + log.Infoln("Profile default loaded") + client.Close() } @@ -46,5 +48,7 @@ func handleProfileReload(client *net.UnixConn) { binary.Write(client, binary.BigEndian, uint32(0)) } + log.Infoln("Profile " + payload.Path + " loaded") + client.Close() } diff --git a/core/src/main/golang/server/server.go b/core/src/main/golang/server/server.go index 0107608..96e9bf6 100644 --- a/core/src/main/golang/server/server.go +++ b/core/src/main/golang/server/server.go @@ -8,17 +8,21 @@ import ( ) const ( - commandPing = 0 - commandTunStart = 1 - commandTunStop = 2 + commandPing = 0 + commandTunStart = 1 + commandTunStop = 2 + commandProfileDefault = 3 + commandProfileReload = 4 ) var handlers map[int]func(*net.UnixConn) = make(map[int]func(*net.UnixConn)) func init() { - handlers[commandPing] = handlePing // ping.go - handlers[commandTunStart] = handleTunStart // tun.go - handlers[commandTunStop] = handleTunStop // tun.go + handlers[commandPing] = handlePing // ping.go + handlers[commandTunStart] = handleTunStart // tun.go + handlers[commandTunStop] = handleTunStop // tun.go + handlers[commandProfileDefault] = handleProfileDefault // profile.go + handlers[commandProfileReload] = handleProfileReload // profile.go } // Start local control server diff --git a/core/src/main/golang/server/tun.go b/core/src/main/golang/server/tun.go index b5203dc..929796b 100644 --- a/core/src/main/golang/server/tun.go +++ b/core/src/main/golang/server/tun.go @@ -15,9 +15,8 @@ const ( func handleTunStart(client *net.UnixConn) { buffer := make([]byte, unix.CmsgLen(4*1)) - data := make([]byte, 8) - _, noob, _, _, err := client.ReadMsgUnix(data, buffer) + _, noob, _, _, err := client.ReadMsgUnix(nil, buffer) if err != nil { log.Warnln("Read tun socket failure, %s", err.Error()) return @@ -35,9 +34,13 @@ func handleTunStart(client *net.UnixConn) { return } - mtu := binary.BigEndian.Uint32(data) + var mtu uint32 + var end uint32 - if binary.BigEndian.Uint32(data[4:]) != tunCommandEnd { + binary.Read(client, binary.BigEndian, &mtu) + binary.Read(client, binary.BigEndian, &end) + + if end != tunCommandEnd { log.Warnln("Invalid tun command end") return } 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 b4e00c4..bbabc60 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 @@ -1,9 +1,108 @@ package com.github.kr328.clash.core import android.content.Context -import java.io.File +import android.net.LocalSocket +import android.net.LocalSocketAddress +import android.net.Uri +import android.os.ParcelFileDescriptor +import android.util.Log +import org.json.JSONObject +import java.io.* -class Clash(context: Context, clashDir: File, controllerPath: File) : +class Clash(private val context: Context, clashDir: File, private val controllerPath: File) : ClashProcess(context, clashDir, controllerPath) { + companion object { + const val COMMAND_PING = 0 + const val COMMAND_TUN_START = 1 + const val COMMAND_TUN_STOP = 2 + const val COMMAND_PROFILE_DEFAULT = 3 + const val COMMAND_PROFILE_RELOAD = 4 + const val PING_REPLY = 233 + } + + private val cacheFile = context.cacheDir.resolve("config.yaml") + + fun ping(): Boolean { + try { + return runControl(COMMAND_PING) {_, input, _ -> + input.readInt() == PING_REPLY + } + } + catch (e: IOException) { + Log.w(TAG, "Clash ping failure", e) + } + return false + } + + @Throws(IOException::class) + @Synchronized + fun loadProfile(url: Uri) { + synchronized(cacheFile) { + context.contentResolver.openInputStream(url)?.use { + it.copyTo(FileOutputStream(cacheFile)) + } + + runControl(COMMAND_PROFILE_RELOAD) {_, input, output -> + output.writeString(JSONObject().apply { + put("path", cacheFile.absolutePath) + }.toString()) + + val result = input.readString() + if (result.isNotEmpty()) { + throw IOException(JSONObject(result).getString("error")) + } + } + } + } + + @Throws(IOException::class) + fun startTunDevice(fd: FileDescriptor, mtu: Int) { + runControl(COMMAND_TUN_START) { socket, _, output -> + socket.setFileDescriptorsForSend(arrayOf(fd)) + socket.outputStream.write(0) + socket.outputStream.flush() + output.writeInt(mtu) + output.writeInt(0x243) + } + } + + private fun runControl(command: Int, block: (LocalSocket, DataInputStream, DataOutputStream) -> R): R { + val socket = LocalSocket() + + socket.connect(LocalSocketAddress(controllerPath.absolutePath, LocalSocketAddress.Namespace.FILESYSTEM)) + + val input = DataInputStream(socket.inputStream) + val output = DataOutputStream(socket.outputStream) + + output.writeInt(command) + + val result = block(socket, input, output) + + try { + input.close() + output.close() + + socket.close() + } + catch (ignore: IOException) { + + } + + return result + } + + private fun DataOutputStream.writeString(string: String) { + this.writeInt(string.length) + this.writeBytes(string) + } + + private fun DataInputStream.readString(): String { + val len = this.readInt() + val buffer = ByteArray(len) + + this.read(buffer) + + return String(buffer) + } } \ No newline at end of file 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 cb28613..70f2f6f 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 @@ -5,9 +5,9 @@ import android.util.Log import java.io.File import java.io.IOException -abstract class ClashProcess(context: Context, clashDir: File, private val controllerPath: File) { +abstract class ClashProcess(context: Context, clashDir: File, controllerPath: File) { companion object { - private const val TAG = "ClashForAndroid" + 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]" @@ -57,6 +57,8 @@ abstract class ClashProcess(context: Context, clashDir: File, private val contro if ( error != null ) throw IOException("Controller: $error") + + break } }