Compare commits

...

92 Commits

Author SHA1 Message Date
Kr328
389ecb3e8f update version 2020-06-13 19:07:26 +08:00
Kr328
160b96d829 fix crash on find method ClashException 2020-06-13 19:03:42 +08:00
Kr328
5b80873810 remove unused proguard rule 2020-06-13 19:03:10 +08:00
Kr328
9cae7f9ad7 add default fake ip filter list 2020-06-13 14:08:14 +08:00
Kr328
1333c554a9 load only libbridge.so 2020-06-13 14:07:51 +08:00
Kr328
4d6000af22 update tun2socket 2020-06-13 14:07:33 +08:00
Kr328
3601d1166b fix default fake ip filter 2020-06-13 11:11:55 +08:00
Kr328
7691a96996 fix native integer type overflow 2020-06-13 02:37:12 +08:00
Kr328
db215a5510 update clash core & add default fake-ip filter 2020-06-13 01:38:02 +08:00
Kr328
17c17452b3 refactor build script dependencies define 2020-06-13 01:05:30 +08:00
Kr328
d7b0fa5e29 add windows build compat 2020-06-13 00:25:18 +08:00
Kr328
a0b5e595c8 Merge pull request #413 from goomadao/fix-gradle
Fix build on macOS and Windows
2020-06-12 19:06:10 +08:00
goomadao
728cc0d10b Fix build on macOS and Windows 2020-06-12 16:31:38 +08:00
Kr328
514c84ec7e remove unused debug log output 2020-06-12 13:55:19 +08:00
Kr328
f4af98f54a add increase max connection & clean up code 2020-06-12 00:38:21 +08:00
Kr328
e1f4fef157 version 1.3.1 2020-06-12 00:00:26 +08:00
Kr328
2004f2392a version 1.3.1 2020-06-11 23:49:15 +08:00
Kr328
d7f0cf91c7 refactor clash bridge 2020-06-11 23:48:44 +08:00
Kr328
f34d71eb88 refactor clash bridge 2020-06-11 15:57:34 +08:00
Kr328
b0ed302d5e refactor clash bridge 2020-06-10 21:24:38 +08:00
Kr328
a4b74a7523 update tun2socket & rename token 2020-06-10 21:24:38 +08:00
Kr328
8581f53b20 refactor clash bridge 2020-06-10 21:24:38 +08:00
Kr328
432fa115e2 refactor clash bridge 2020-06-10 21:24:38 +08:00
Kr328
2759f59029 refactor clash bridge 2020-06-10 21:24:38 +08:00
Kr328
d24496a01e refactor clash bridge 2020-06-10 21:24:38 +08:00
Kr328
123bf65c55 refactor clash bridge 2020-06-10 21:24:38 +08:00
Kr328
f6263940f1 update clash core 2020-06-10 21:24:37 +08:00
Kr328
cba3d2d848 refactor clash bridge 2020-06-10 21:24:37 +08:00
Kr328
cecb1522fe refactor build clash scripts 2020-06-10 21:24:37 +08:00
Kr328
fd6ecda520 Merge pull request #412 from Ximu-Luya/master
Provide string translation for Hong Kong and Taiwan
2020-06-10 21:23:21 +08:00
Ximu
147df6c180 Provide string translation for Hong Kong and Taiwan 2020-06-10 19:03:46 +08:00
Ximu
8d6310d0ea Merge pull request #1 from Kr328/master
update issue template
2020-06-02 04:06:41 -05:00
Kr328
c455dfc7df update issue template 2020-06-01 23:35:03 +08:00
Kr328
e381319ae8 update tun2socket 2020-05-24 18:57:26 +08:00
Kr328
297b39625f update clash core & tun2socket 2020-05-24 13:03:55 +08:00
Kr328
b42d762fa0 update clash core 2020-05-14 21:33:29 +08:00
Kr328
f23e6e82b0 update tun2socket 2020-05-11 18:23:11 +08:00
Kr328
2af11c29a5 fix bypass private route comment 2020-05-03 22:43:44 +08:00
Kr328
b0bf57e715 reject all localhost traffic 2020-05-03 21:33:49 +08:00
Kr328
cdd0e3dd30 fix break connection 2020-05-03 00:51:51 +08:00
Kr328
5823955c71 update clash core 2020-05-03 00:50:34 +08:00
Kr328
714c9a554f break connection if set proxy for selector 2020-05-03 00:50:14 +08:00
Kr328
e621324b48 update clash core 2020-05-03 00:29:45 +08:00
Kr328
dfe1e7ecd9 exclude 127.0.0.0/8 from route table 2020-05-03 00:17:55 +08:00
Kr328
4c3380d822 update clash core & tun2socket 2020-04-29 13:22:36 +08:00
Kr328
1d68516cdb fix receivers & service connection leak 2020-04-24 00:49:03 +08:00
Kr328
821bb49914 update issue template 2020-04-23 19:33:28 +08:00
Kr328
c95993a629 fix crash on uploaded 2020-04-23 19:15:46 +08:00
Kr328
e6495f5e1f add uploaded feedback 2020-04-23 19:15:16 +08:00
Kr328
c104942b5f update version 2020-04-23 19:13:34 +08:00
Kr328
59835667aa add report logcat button 2020-04-23 19:04:51 +08:00
Kr328
8a98eea8fa improve core log 2020-04-23 18:52:23 +08:00
Kr328
3dc20b9e70 update issue template 2020-04-23 16:29:17 +08:00
Kr328
d8ab6fd755 update tun2socket version 2020-04-23 13:10:22 +08:00
Kr328
cc499a7897 improve package name modify 2020-04-23 12:14:05 +08:00
Kr328
76f5261b20 Revert "Fix tun start at second user"
This reverts commit 94a0282c
2020-04-23 11:08:36 +08:00
Kr328
6a506b761f update tun2socket 2020-04-23 11:06:50 +08:00
Kr328
fe966f9c5d Revert "Fix tun start at second user"
This reverts commit 94a0282c
2020-04-23 10:52:59 +08:00
Kr328
bcf7f793ec add stop tun on destroy & update version 2020-04-23 10:51:20 +08:00
Kr328
75de6ce041 add full crash logcat report 2020-04-23 00:12:07 +08:00
Kr328
4417a85ef7 update version 2020-04-23 00:09:24 +08:00
Kr328
9e0b67c04a add dump AndroidRuntime tag 2020-04-23 00:06:53 +08:00
Kr328
8cd203e7b0 detect vpn establish failure 2020-04-23 00:03:05 +08:00
Kr328
211160f7f8 Rename fields to let lint happy :) 2020-04-22 13:25:50 +08:00
Kr328
2afc7262eb Update version 2020-04-22 11:25:01 +08:00
Kr328
94a0282c70 Fix tun start at second user 2020-04-22 11:05:10 +08:00
Kr328
96b78829ab Fix cancel profile auto update 2020-04-22 00:25:22 +08:00
Kr328
35d5a90b2a Improve database migrations 2020-04-21 23:50:01 +08:00
Kr328
50d73ad498 update version & dependencies 2020-04-21 22:38:43 +08:00
Kr328
d1f8bc3062 fix delete profile not cancel auto update 2020-04-21 22:32:16 +08:00
Kr328
d3353b1aa5 fix update interval reset 2020-04-21 22:29:21 +08:00
Kr328
cd48fb4e08 fix dialer hook 2020-04-21 22:27:09 +08:00
Kr328
f70738ecf5 update clash core 2020-04-21 22:18:40 +08:00
Kr328
2d63466597 update tun2socket 2020-04-21 21:43:35 +08:00
Kr328
ecf67443fb remove reduce zero ip detect 2020-04-20 12:35:07 +08:00
Kr328
43987ad111 remove reduce zero ip detect 2020-04-20 12:34:27 +08:00
Kr328
5fbd6c6196 Update bug report issue 2020-04-19 02:06:12 +08:00
Kr328
1ec7f1f7e9 Merge branch 'master' into dev 2020-04-19 02:01:39 +08:00
Kr328
c5104c241a add basic url auth support 2020-04-18 20:41:20 +08:00
Kr328
021ba3519d format export log file entry 2020-04-17 13:50:10 +08:00
Kr328
119f74cd05 fix profile edit fragment cast crash 2020-04-17 00:19:39 +08:00
Kr328
f0b61c9e1f cleanup code 2020-04-16 22:40:27 +08:00
Kr328
a573631068 use set directly instead of toList 2020-04-16 22:36:45 +08:00
Kr328
a12444b258 add tun start failure detect 2020-04-16 21:52:11 +08:00
Kr328
17257228a9 Update clash core & add udp packet recycle 2020-04-16 21:26:33 +08:00
Kr328
53b6a9dfc1 Fix pack mmdb failure if downloadGepip before assembleClashCore 2020-04-16 12:13:43 +08:00
Kr328
c45f8ddfc3 Add simple package name modify detect 2020-04-16 11:05:49 +08:00
Kr328
e04c9c15ff Fix Database migrations 2020-04-16 11:03:43 +08:00
Kr328
d5cb363579 Update core 2020-04-15 21:17:26 +08:00
Kr328
0748021ffa Fix cloned profile type 2020-04-15 20:44:49 +08:00
Kr328
e958e7f675 Update 03-bug-report-zh-cn.md 2020-03-22 20:45:20 +08:00
Kr328
68113e694e update issue template 2020-03-22 13:04:50 +08:00
114 changed files with 3361 additions and 1341 deletions

View File

@@ -7,6 +7,10 @@ assignees: ''
---
<!-- Be sure to put a clear title after [BUG] in the text box above -->
<!-- Be sure to put a clear title after [BUG] in the text box above -->
<!-- Be sure to put a clear title after [BUG] in the text box above -->
**Describe the bug**
A clear and concise description of what the bug is.
@@ -23,15 +27,12 @@ A clear and concise description of what you expected to happen.
**Screenshots**
If applicable, add screenshots to help explain your problem.
**Logs**
if applicable, add logs to help detect problem
**Device Info (please complete the following information):**
- Device: [e.g. Pixel 4]
- ROM: [e.g: AOSP]
- ROM Version:
- Android Version [e.g. Oreo]
- Android Version [e.g. 10]
**Application Info (please complete the following information):**
@@ -40,4 +41,17 @@ if applicable, add logs to help detect problem
- Distribution Channel: [e.g. Google Play]
**Additional context**
Add any other context about the problem here.
Add any other context about the problem here.
**Configure**
If applicable, paste **removed server info** configure
```yaml
# paste here
```
**Logs**
If applicable, paste logs to help detect problem
```
<paste here>
```

View File

@@ -7,6 +7,10 @@ assignees: ''
---
<!-- Be sure to put a clear title after [Feature Request] in the text box above -->
<!-- Be sure to put a clear title after [Feature Request] in the text box above -->
<!-- Be sure to put a clear title after [Feature Request] in the text box above -->
**Is your feature request related to a problem? Please describe.**
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]

View File

@@ -7,6 +7,10 @@ assignees: ''
---
<!-- 请务必在上方文本框处 [BUG] 后填入清晰明了的标题 -->
<!-- 请务必在上方文本框处 [BUG] 后填入清晰明了的标题 -->
<!-- 请务必在上方文本框处 [BUG] 后填入清晰明了的标题 -->
**描述出现的错误**
请简洁的描述你遇到的错误
@@ -23,14 +27,10 @@ assignees: ''
**屏幕截图**
如果适用, 上传屏幕截图以帮助描述错误
**日志**
如果适用, 上传日志以帮助侦测错误
**设备信息 (请完成一下信息):**
**设备信息 (请完成以下信息):**
- 机型: [例如: Pixel 4]
- 系统/ROM: [例如: MIUI 11]
- Android 版本 [例如: Oreo]
- Android 版本 [例如: 10]
- ROM版本 [例如: 20.3.19]
**应用信息**
@@ -40,3 +40,16 @@ assignees: ''
**附加信息**
其他的可能与改错误相关的信息
**配置文件**
如果适用, 在此粘贴 **去除服务器信息的****配置文件**
```yaml
# 在此粘贴
```
**日志**
如果适用, 粘贴日志以帮助侦测错误
```
<在此粘贴>
```

View File

@@ -7,6 +7,10 @@ assignees: ''
---
<!-- 请务必在上方文本框处 [Feature Request] 后填入清晰明了的标题 -->
<!-- 请务必在上方文本框处 [Feature Request] 后填入清晰明了的标题 -->
<!-- 请务必在上方文本框处 [Feature Request] 后填入清晰明了的标题 -->
**功能描述**
请清晰的描述你想要的功能

1
.github/ISSUE_TEMPLATE/config.yml vendored Normal file
View File

@@ -0,0 +1 @@
blank_issues_enabled: false

View File

@@ -6,26 +6,24 @@ plugins {
id("kotlin-android-extensions")
}
val rootExtra = rootProject.extra
val gCompileSdkVersion: String by project
val gBuildToolsVersion: String by project
val gCompileSdkVersion: Int by rootExtra
val gBuildToolsVersion: String by rootExtra
val gMinSdkVersion: String by project
val gTargetSdkVersion: String by project
val gMinSdkVersion: Int by rootExtra
val gTargetSdkVersion: Int by rootExtra
val gVersionCode: String by project
val gVersionName: String by project
val gVersionCode: Int by rootExtra
val gVersionName: String by rootExtra
val gKotlinVersion: String by rootExtra
val gKotlinCoroutineVersion: String by rootExtra
val gAppCenterVersion: String by rootExtra
val gAndroidKtxVersion: String by rootExtra
val gRecyclerviewVersion: String by rootExtra
val gAppCompatVersion: String by rootExtra
val gMaterialDesignVersion: String by rootExtra
val gShizukuPreferenceVersion: String by rootExtra
val gMultiprocessPreferenceVersion: String by rootExtra
val gKotlinVersion: String by project
val gKotlinCoroutineVersion: String by project
val gAppCenterVersion: String by project
val gAndroidKtxVersion: String by project
val gRecyclerviewVersion: String by project
val gAppCompatVersion: String by project
val gMaterialDesignVersion: String by project
val gShizukuPreferenceVersion: String by project
val gMultiprocessPreferenceVersion: String by project
android {
compileSdkVersion(gCompileSdkVersion)
@@ -37,12 +35,12 @@ android {
minSdkVersion(gMinSdkVersion)
targetSdkVersion(gTargetSdkVersion)
versionCode = gVersionCode
versionCode = gVersionCode.toInt()
versionName = gVersionName
}
buildTypes {
maybeCreate("release").apply {
named("release") {
isMinifyEnabled = true
isShrinkResources = true
proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro")
@@ -73,7 +71,7 @@ android {
}
}
signingConfigs {
maybeCreate("release").apply {
named("release") {
storeFile = rootProject.file(Objects.requireNonNull(properties.getProperty("storeFile")))
storePassword = Objects.requireNonNull(properties.getProperty("storePassword"))
keyAlias = Objects.requireNonNull(properties.getProperty("keyAlias"))
@@ -81,7 +79,7 @@ android {
}
}
buildTypes {
maybeCreate("release").apply {
named("release") {
this.signingConfig = signingConfigs.findByName("release")
}
}
@@ -121,6 +119,18 @@ task("injectAppCenterKey") {
}
}
task("injectPackageNameBase64") {
doFirst {
val packageName = android.defaultConfig.applicationId ?: return@doFirst
val base64 = Base64.getEncoder().encodeToString(packageName.toByteArray(Charsets.UTF_8))
android.buildTypes.forEach {
it.buildConfigField("String", "PACKAGE_NAME_BASE64", "\"$base64\"")
}
}
}
afterEvaluate {
tasks["preBuild"].dependsOn(tasks["injectAppCenterKey"])
tasks["preBuild"].dependsOn(tasks["injectAppCenterKey"], tasks["injectPackageNameBase64"])
}

View File

@@ -16,11 +16,9 @@ import kotlinx.android.synthetic.main.activity_log_viewer.*
import kotlinx.coroutines.*
import kotlinx.coroutines.sync.Mutex
import java.io.File
import kotlin.streams.toList
class LogViewerActivity : BaseActivity() {
private val pauseMutex = Mutex()
private var pollingThread: Thread? = null
private val connection = object : ServiceConnection {
override fun onServiceDisconnected(name: ComponentName?) {
finish()
@@ -49,12 +47,6 @@ class LogViewerActivity : BaseActivity() {
startFileMode(file.toFile())
}
override fun onDestroy() {
super.onDestroy()
pollingThread?.interrupt()
}
override fun onStop() {
super.onStop()
@@ -79,6 +71,7 @@ class LogViewerActivity : BaseActivity() {
mainList.itemAnimator?.removeDuration = 100
stop.setOnClickListener {
unbindService(connection)
stopService(LogcatService::class.intent)
finish()
}
@@ -92,15 +85,15 @@ class LogViewerActivity : BaseActivity() {
launch {
val items = withContext(Dispatchers.IO) {
try {
file.readText()
.split("\n")
.parallelStream()
.map { it.trim() }
.filter { it.isNotEmpty() && !it.startsWith("#") }
.map { it.split(" ", limit = 3) }
.filter { it.size == 3 }
.map { LogEvent(LogEvent.Level.valueOf(it[1]), it[2], it[0].toLong()) }
.toList()
file.bufferedReader().useLines { lines ->
lines
.map { it.trim() }
.filter { it.isNotEmpty() && !it.startsWith("#") }
.map { it.split(" ", limit = 3) }
.filter { it.size == 3 }
.map { LogEvent(LogEvent.Level.valueOf(it[1]), it[2], it[0].toLong()) }
.toList()
}
} catch (e: Exception) {
showSnackbarException(getString(R.string.open_log_failure), e.message)

View File

@@ -13,6 +13,7 @@ import androidx.recyclerview.widget.LinearLayoutManager
import com.github.kr328.clash.adapter.LogFileAdapter
import com.github.kr328.clash.common.utils.intent
import com.github.kr328.clash.common.utils.startForegroundServiceCompat
import com.github.kr328.clash.core.event.LogEvent
import com.github.kr328.clash.design.common.Category
import com.github.kr328.clash.design.view.CommonUiLayout
import com.github.kr328.clash.model.LogFile
@@ -24,12 +25,15 @@ import kotlinx.android.synthetic.main.activity_logs.*
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.io.FileInputStream
import java.text.SimpleDateFormat
import java.util.*
class LogsActivity : BaseActivity() {
companion object {
const val REQUEST_CODE = 50000
private val LOG_EXPORT_DATE_FORMAT = SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.ENGLISH)
private val LOG_EXPORT_TIME_FORMAT = SimpleDateFormat("HH:mm:ss", Locale.ENGLISH)
}
private var lastWriteFile: LogFile? = null
@@ -102,9 +106,31 @@ class LogsActivity : BaseActivity() {
launch {
withContext(Dispatchers.IO) {
contentResolver.openOutputStream(url)?.use { output ->
FileInputStream(logsDir.resolve(file.fileName)).use { input ->
input.copyTo(output)
contentResolver.openOutputStream(url)?.bufferedWriter()?.use { output ->
output.write("# Logcat on " + LOG_EXPORT_DATE_FORMAT.format(Date(file.date)) + "\n")
logsDir.resolve(file.fileName).bufferedReader().useLines { lines ->
lines.map { it.trim() }
.filter { it.isNotEmpty() && !it.startsWith("#") }
.map { it.split(" ", limit = 3) }
.filter { it.size == 3 }
.map {
LogEvent(
LogEvent.Level.valueOf(it[1]),
it[2],
it[0].toLong()
)
}
.forEach {
output.write(
String.format(
"%s |%s| %s\n",
LOG_EXPORT_TIME_FORMAT.format(Date(it.time)),
it.level.toString(),
it.message
)
)
}
}
}
}

View File

@@ -42,7 +42,7 @@ class MainApplication : Application() {
if (!report.stackTrace.contains("DeadObjectException"))
return mutableListOf()
val logcat = LogcatDumper.dump().joinToString(separator = "\n")
val logcat = LogcatDumper.dumpCrash()
return mutableListOf(
ErrorAttachmentLog.attachmentWithText(logcat, "logcat.txt")

View File

@@ -119,11 +119,6 @@ class ProxiesActivity : BaseActivity(), ScrollBinding.Callback {
put(UiSettings.PROXY_GROUP_SORT, UiSettings.PROXY_SORT_NAME)
}
}
R.id.groupDelay -> {
uiSettings.commit {
put(UiSettings.PROXY_GROUP_SORT, UiSettings.PROXY_SORT_DELAY)
}
}
R.id.proxyDefault -> {
uiSettings.commit {
put(UiSettings.PROXY_PROXY_SORT, UiSettings.PROXY_SORT_DEFAULT)
@@ -186,7 +181,7 @@ class ProxiesActivity : BaseActivity(), ScrollBinding.Callback {
UiSettings.PROXY_SORT_NAME ->
findItem(R.id.groupName).isChecked = true
UiSettings.PROXY_SORT_DELAY ->
findItem(R.id.proxyDelay).isChecked = true
findItem(R.id.proxyDefault).isChecked = true
}
when (uiSettings.get(UiSettings.PROXY_PROXY_SORT)) {
UiSettings.PROXY_SORT_DEFAULT ->
@@ -206,7 +201,7 @@ class ProxiesActivity : BaseActivity(), ScrollBinding.Callback {
private fun setGroupSelected(group: String, select: String) {
launch {
withClash {
setSelectProxy(group, select)
setSelector(group, select)
}
}
}
@@ -240,7 +235,7 @@ class ProxiesActivity : BaseActivity(), ScrollBinding.Callback {
queryGeneral()
}
val proxies = withClash {
queryAllProxyGroups()
queryProxyGroups()
}
val merged = Pipeline(proxies, uiSettings).mergePrefix()

View File

@@ -4,9 +4,19 @@ import android.content.Intent
import android.net.Uri
import android.os.Bundle
import android.text.Html
import androidx.appcompat.app.AlertDialog
import com.github.kr328.clash.dump.LogcatDumper
import com.google.android.material.snackbar.Snackbar
import com.microsoft.appcenter.crashes.Crashes
import com.microsoft.appcenter.crashes.ingestion.models.ErrorAttachmentLog
import kotlinx.android.synthetic.main.activity_support.*
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
class SupportActivity : BaseActivity() {
class UserRequestTrackException: Exception()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
@@ -46,6 +56,20 @@ class SupportActivity : BaseActivity() {
category(text = getString(R.string.feedback))
option(
title = getString(R.string.upload_logcat),
summary = getString(R.string.upload_logcat_summary)
) {
onClick {
AlertDialog.Builder(this@SupportActivity)
.setTitle(R.string.upload_logcat)
.setMessage(R.string.upload_logcat_warn)
.setNegativeButton(R.string.cancel) {_, _ -> }
.setPositiveButton(R.string.ok) {_, _ -> upload() }
.show()
}
}
option(
title = getString(R.string.github_issues),
summary = getString(R.string.github_issues_url)
@@ -77,4 +101,19 @@ class SupportActivity : BaseActivity() {
}
}
}
private fun upload() {
launch {
withContext(Dispatchers.IO) {
val attachment = ErrorAttachmentLog
.attachmentWithText(LogcatDumper.dumpAll(), "logcat.txt")
Crashes.trackError(UserRequestTrackException(), null, listOf(attachment))
}
withContext(Dispatchers.Main) {
Snackbar.make(rootView, R.string.uploaded, Snackbar.LENGTH_LONG).show()
}
}
}
}

View File

@@ -1,34 +1,37 @@
package com.github.kr328.clash.dump
object LogcatDumper {
fun dump(): List<String> {
fun dumpCrash(): String {
return try {
val process =
Runtime.getRuntime().exec(arrayOf("logcat", "-d", "-s", "-v", "raw", "Go"))
Runtime.getRuntime().exec(arrayOf("logcat", "-d", "-s", "Go", "AndroidRuntime", "DEBUG"))
val result = process.inputStream.bufferedReader().useLines {
var list = mutableListOf<String>()
var capture = false
it.forEach { line ->
if (line.startsWith("panic")) {
capture = true
list = mutableListOf()
}
if (capture)
list.add(line)
}
list
val result = process.inputStream.use {
it.reader().readText()
}
process.waitFor()
result
} catch (e: Exception) {
emptyList()
""
}
}
fun dumpAll(): String {
return try {
val process =
Runtime.getRuntime().exec(arrayOf("logcat", "-d"))
val result = process.inputStream.use {
it.reader().readText()
}
process.waitFor()
result
} catch (e: Exception) {
""
}
}
}

View File

@@ -27,6 +27,8 @@ class ProfileEditFragment(
private val type: Type,
private val source: String?
) : Fragment() {
private var root: CommonUiLayout? = null
var isModified = false
companion object {
@@ -46,6 +48,7 @@ class ProfileEditFragment(
savedInstanceState: Bundle?
): View? {
return CommonUiLayout(requireContext()).apply {
root = this
layoutParams = LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT)
build {
@@ -111,12 +114,14 @@ class ProfileEditFragment(
if (s.isBlank()) {
content = ""
interval = 0
return@onTextChanged
}
val value = s.toIntOrNull()
if (value == null || value < 15) {
content = ""
interval = 0
Snackbar.make(view, R.string.invalid_interval, Snackbar.LENGTH_LONG)
.show()
return@onTextChanged
@@ -143,16 +148,16 @@ class ProfileEditFragment(
if (resultCode != Activity.RESULT_OK || data == null)
return
val layout = view as CommonUiLayout
root?.apply {
data.data?.apply {
screen.requireElement<TextInput>(KEY_URL).content = this.toString()
}
data.data?.apply {
layout.screen.requireElement<TextInput>(KEY_URL).content = this.toString()
}
data.getStringExtra(Constants.URL_PROVIDER_INTENT_EXTRA_NAME)?.also {
layout.screen.requireElement<TextInput>(KEY_NAME).apply {
if (content.isBlank())
content = it
data.getStringExtra(Constants.URL_PROVIDER_INTENT_EXTRA_NAME)?.also {
screen.requireElement<TextInput>(KEY_NAME).apply {
if (content.isBlank())
content = it
}
}
}
}
@@ -163,7 +168,9 @@ class ProfileEditFragment(
override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
(view as CommonUiLayout?)?.screen?.saveState(outState)
root?.apply {
screen.saveState(outState)
}
}
private fun openUrlProvider(): Boolean {
@@ -182,11 +189,13 @@ class ProfileEditFragment(
else -> return false
}
} catch (e: Exception) {
Snackbar.make(
view as ViewGroup,
R.string.start_url_provider_failure,
Snackbar.LENGTH_LONG
).show()
root?.apply {
Snackbar.make(
this,
R.string.start_url_provider_failure,
Snackbar.LENGTH_LONG
).show()
}
}
return true

View File

@@ -11,13 +11,13 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
class ClashClient(val service: IClashManager) {
suspend fun setSelectProxy(name: String, proxy: String): Boolean = withContext(Dispatchers.IO) {
service.setSelectProxy(name, proxy)
suspend fun setSelector(group: String, selected: String) = withContext(Dispatchers.IO) {
service.setSelector(group, selected)
}
suspend fun startHealthCheck(group: String) = withContext(Dispatchers.IO) {
CompletableDeferred<Unit>().apply {
service.startHealthCheck(group, object : IStreamCallback.Stub() {
service.performHealthCheck(group, object : IStreamCallback.Stub() {
override fun complete() {
this@apply.complete(Unit)
}
@@ -31,8 +31,8 @@ class ClashClient(val service: IClashManager) {
}
}.await()
suspend fun queryAllProxyGroups(): List<ProxyGroup> = withContext(Dispatchers.IO) {
service.queryAllProxies().list
suspend fun queryProxyGroups(): List<ProxyGroup> = withContext(Dispatchers.IO) {
service.queryProxyGroups().list
}
suspend fun queryGeneral(): General = withContext(Dispatchers.IO) {

View File

@@ -8,8 +8,10 @@ import android.content.ServiceConnection
import android.os.Build
import android.os.Handler
import android.os.IBinder
import android.util.Base64
import androidx.core.content.edit
import com.github.kr328.clash.ApkBrokenActivity
import com.github.kr328.clash.BuildConfig
import com.github.kr328.clash.Constants
import com.github.kr328.clash.common.Global
import com.github.kr328.clash.common.utils.intent
@@ -88,7 +90,7 @@ object Remote {
val application = Global.application
if ( it ) {
if (it) {
handler.removeMessages(0)
GlobalScope.launch {
@@ -152,11 +154,43 @@ object Remote {
if (sp.getLong(Constants.PREFERENCE_KEY_LAST_INSTALL, 0) == pkg.lastUpdateTime)
return true
val pkgName: String = try {
application::class.java.getMethod(
String(
charArrayOf(
'g',
'e',
't',
'P',
'a',
'c',
'k',
'a',
'g',
'e',
'N',
'a',
'm',
'e'
)
)
).invoke(application)?.toString()
} catch (e: Exception) {
Log.w("getPackageName failure", e)
null
} ?: application.packageName
val packageNameBase64 = Base64
.encodeToString(pkgName.toByteArray(Charsets.UTF_8), Base64.NO_WRAP)
if (packageNameBase64 != BuildConfig.PACKAGE_NAME_BASE64)
return false
val info = application.applicationInfo
val sources =
info.splitSourceDirs ?: arrayOf(info.sourceDir) ?: return false
val regexNativeLibrary = Regex("lib/(\\S+)/libgojni.so")
val regexNativeLibrary = Regex("lib/(\\S+)/libclash.so")
val availableAbi = Build.SUPPORTED_ABIS.toSet()
val apkAbi =
sources

View File

@@ -7,6 +7,7 @@ import com.github.kr328.clash.ApkBrokenActivity
import com.github.kr328.clash.common.utils.intent
import com.github.kr328.clash.service.Constants
import com.github.kr328.clash.service.ServiceStatusProvider
import java.lang.Exception
object RemoteUtils {
fun detectClashRunning(context: Context): Boolean {
@@ -24,7 +25,7 @@ object RemoteUtils {
)
return pong != null
} catch (e: IllegalArgumentException) {
} catch (e: Exception) {
context.startActivity(ApkBrokenActivity::class.intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK))
return false
@@ -46,7 +47,7 @@ object RemoteUtils {
)
return pong?.getString("name")
} catch (e: IllegalArgumentException) {
} catch (e: Exception) {
context.startActivity(ApkBrokenActivity::class.intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK))
return null

View File

@@ -24,10 +24,9 @@ class ProxySorter(private val groupOrder: Order, private val proxyOrder: Order)
val sortedGroup = when (groupOrder) {
Order.DEFAULT -> groupSortWithDefault(global, other)
Order.DELAY_INCREASE -> groupSortWithDelay(true, other)
Order.DELAY_DECREASE -> groupSortWithDelay(false, other)
Order.NAME_INCREASE -> groupSortWithName(true, other)
Order.NAME_DECREASE -> groupSortWithName(false, other)
else -> groupSortWithDefault(global, other)
}
val sorted = if (global == null)
@@ -73,16 +72,6 @@ class ProxySorter(private val groupOrder: Order, private val proxyOrder: Order)
proxyGroup.sortedByDescending { it.name }
}
private fun groupSortWithDelay(
increase: Boolean,
proxyGroup: List<ProxyGroup>
): List<ProxyGroup> {
return if (increase)
proxyGroup.sortedBy { it.delay }
else
proxyGroup.sortedByDescending { it.delay }
}
private fun proxySortWithName(
increase: Boolean,
proxies: List<Proxy>

View File

@@ -15,7 +15,6 @@
<group android:checkableBehavior="single">
<item android:id="@+id/groupDefault" android:title="@string/default_" />
<item android:id="@+id/groupName" android:title="@string/name" />
<item android:id="@+id/groupDelay" android:title="@string/delay" />
</group>
</menu>
</item>

View File

@@ -0,0 +1,20 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string-array name="access_control_mode_display">
<item>允許所有應用</item>
<item>僅允許已選擇的應用</item>
<item>不允許已選擇的應用</item>
</string-array>
<string-array name="dark_mode">
<item>自動</item>
<item>暗黑</item>
<item>明亮</item>
</string-array>
<string-array name="language">
<item>自動</item>
<item>英文</item>
<item>簡體中文</item>
<item>繁體中文-香港</item>
<item>繁體中文-台灣</item>
</string-array>
</resources>

View File

@@ -0,0 +1,138 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="launch_name">Clash</string>
<string name="application_name">Clash for Android</string>
<string name="stopped">已停止</string>
<string name="tap_to_start">點此啟動</string>
<string name="running">運行中</string>
<string name="format_traffic_forwarded">%s 已轉發</string>
<string name="proxy">代理</string>
<string name="direct_mode">直連模式</string>
<string name="rule_mode">規則模式</string>
<string name="global_mode">全局模式</string>
<string name="profiles">配置</string>
<string name="not_selected">未選擇</string>
<string name="format_profile_activated">%s 已激活</string>
<string name="logs">日誌</string>
<string name="settings">設置</string>
<string name="support">支持</string>
<string name="about">關於</string>
<string name="unknown">未知</string>
<string name="recently">近期</string>
<string name="format_minutes">%d 分鐘</string>
<string name="format_hours">%d 小時</string>
<string name="format_days">%d 天</string>
<string name="format_months">%d 月</string>
<string name="format_years">%d 年</string>
<string name="create_profile">創建配置</string>
<string name="file">文件</string>
<string name="import_from_file">從文件導入</string>
<string name="url">URL</string>
<string name="import_from_url">從 URL 導入</string>
<string name="external">外部</string>
<string name="application_broken">應用損壞</string>
<string name="profile">配置</string>
<string name="name">名稱</string>
<string name="profile_name">配置名稱</string>
<string name="profile_url">配置 URL</string>
<string name="auto_update">自動更新 (分鐘)</string>
<string name="more_than_15_minutes">大於15分鐘</string>
<string name="invalid_interval">無效的間隔</string>
<string name="download_failure">下載失敗</string>
<string name="detail">詳情</string>
<string name="update">更新</string>
<string name="edit">編輯</string>
<string name="delete">刪除</string>
<string name="export">導出</string>
<string name="properties">參數</string>
<string name="duplicate">複製</string>
<string name="reset_provider">重置外部引用</string>
<string name="exit_without_save">退出而不保存</string>
<string name="exit_without_save_warning">所有變更將會丟失</string>
<string name="disabled">已禁用</string>
<string name="empty_name">空名稱</string>
<string name="invalid_url">無效的 URL</string>
<string name="processing">處理中</string>
<string name="loading">載入中</string>
<string name="new_profile">新配置</string>
<string name="clone_profile">複製配置</string>
<string name="edit_profile">編輯配置</string>
<string name="clash_start_failure">Clash 啟動失敗</string>
<string name="refresh">刷新</string>
<string name="delay">延遲</string>
<string name="direct">直連</string>
<string name="global">全局</string>
<string name="rule">規則</string>
<string name="mode">模式</string>
<string name="sort_group">代理組排序</string>
<string name="sort_proxy">代理排序</string>
<string name="default_">默認</string>
<string name="utils">實用工具</string>
<string name="merge_prefix">前綴合並</string>
<string name="start_url_provider_failure">打開 URL 提供程序失敗</string>
<string name="clash_new_profile">新配置</string>
<string name="ok">確認</string>
<string name="cancel">取消</string>
<string name="clash_logcat">Clash 日誌捕捉工具</string>
<string name="history">歷史</string>
<string name="log_viewer">日誌查看器</string>
<string name="format_export_log_name">%s.log</string>
<string name="delete_all_logs">刪除所有日誌</string>
<string name="delete_all_logs_warn">所有歷史日誌將丟失</string>
<string name="delete_log">刪除日誌</string>
<string name="delete_log_warn">%s 將被刪除</string>
<string name="open_log_failure">打開日誌失敗</string>
<string name="file_exported">文件已導出</string>
<string name="behavior">行為</string>
<string name="network">網絡</string>
<string name="interface_">界面</string>
<string name="restart">重啟</string>
<string name="auto_restart">自動重啟</string>
<string name="allow_clash_auto_restart">允許 Clash 自動重啟</string>
<string name="notification">通知</string>
<string name="show_traffic">顯示流量</string>
<string name="show_traffic_summary">在通知中自動刷新流量</string>
<string name="route_system_traffic">自動路由系統流量</string>
<string name="routing_via_vpn_service">通過 VpnService 自動路由所有系統流量</string>
<string name="vpn_service">VPN Service</string>
<string name="bypass_private_network">繞過私有網絡</string>
<string name="bypass_private_network_summary">繞過私有網路地址</string>
<string name="dns_hijacking">DNS 劫持</string>
<string name="dns_hijacking_summary">處理所有 DNS 數據包</string>
<string name="dns_override">DNS 配置覆盖</string>
<string name="dns_override_summary">強制使用內置 DNS 配置</string>
<string name="append_system_dns">追加系統 DNS</string>
<string name="append_system_dns_summary">自動追加系統 DNS 到 Clash</string>
<string name="access_control_mode">訪問控制模式</string>
<string name="access_control_packages">訪問控制應用包列表</string>
<string name="access_control_packages_summary">為應用配置訪問權限</string>
<string name="language">語言</string>
<string name="dark_mode">暗黑模式</string>
<string name="options_unavailable">選項在 Clash 運行時不可用</string>
<string name="system_apps">系統應用</string>
<string name="sort">排序</string>
<string name="package_name">應用包名稱</string>
<string name="update_time">更新時間</string>
<string name="install_time">安裝時間</string>
<string name="reverse">反轉</string>
<string name="search">查找</string>
<string name="sources">源代碼</string>
<string name="clash">Clash</string>
<string name="clash_for_android">Clash for Android</string>
<string name="feedback">反饋</string>
<string name="donate">捐贈</string>
<string name="github_issues">Github Issues</string>
<string name="telegram_channel">Telegram 頻道</string>
<string name="tips_profile"><![CDATA[僅接受 <strong>Clash 配置文件</strong> <br />其中包含了 <strong>代理, 代理組 和 規則</strong>]]></string>
<string name="tips_support"><![CDATA[Clash for Android 是一個<strong>免費開源</strong>的項目<br /> 我們<strong>不提供</strong>任何代理服務<br />請務必<strong>不要</strong>反饋非應用自身引起的問題]]></string>
<string name="application_broken_description"><![CDATA[這通常意味着您使用來自 <strong>Google Play</strong> 的副本, 但是生成該副本的應用未能正確處理 <strong>分包機制</strong><br />這意味着您獲取的是 <strong>應用的一部分</strong>]]></string>
<string name="learn_more_about_split_apks">瞭解更多關於分包機制</string>
<string name="reinstall_from_google_play">重新從 Google Play 安裝</string>
<string name="download_from_github_releases">從 Github Release 下載</string>
<string name="missing_vpn_component">系統 VPN 組件缺失</string>
<string name="profile_not_found">配置文件丢失</string>
<string name="upload_logcat">上傳日誌</string>
<string name="upload_logcat_summary">上傳日誌以幫助我們偵測問題</string>
<string name="upload_logcat_warn">請注意, 上傳的日誌可能包含個人敏感信息, 仍要繼續嗎?</string>
<string name="uploaded">已上傳</string>
</resources>

View File

@@ -0,0 +1,20 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string-array name="access_control_mode_display">
<item>允許所有應用</item>
<item>僅允許已選擇的應用</item>
<item>不允許已選擇的應用</item>
</string-array>
<string-array name="dark_mode">
<item>自動</item>
<item>暗黑</item>
<item>明亮</item>
</string-array>
<string-array name="language">
<item>自動</item>
<item>英文</item>
<item>簡體中文</item>
<item>繁體中文-香港</item>
<item>繁體中文-臺灣</item>
</string-array>
</resources>

View File

@@ -0,0 +1,138 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="launch_name">Clash</string>
<string name="application_name">Clash for Android</string>
<string name="stopped">已停止</string>
<string name="tap_to_start">點此啟動</string>
<string name="running">執行中</string>
<string name="format_traffic_forwarded">%s 已轉發</string>
<string name="proxy">代理</string>
<string name="direct_mode">直連模式</string>
<string name="rule_mode">規則模式</string>
<string name="global_mode">全域性模式</string>
<string name="profiles">配置</string>
<string name="not_selected">未選擇</string>
<string name="format_profile_activated">%s 已啟用</string>
<string name="logs">日誌</string>
<string name="settings">設定</string>
<string name="support">支援</string>
<string name="about">關於</string>
<string name="unknown">未知</string>
<string name="recently">近期</string>
<string name="format_minutes">%d 分鐘</string>
<string name="format_hours">%d 小時</string>
<string name="format_days">%d 天</string>
<string name="format_months">%d 月</string>
<string name="format_years">%d 年</string>
<string name="create_profile">建立配置</string>
<string name="file">檔案</string>
<string name="import_from_file">從檔案匯入</string>
<string name="url">URL</string>
<string name="import_from_url">從 URL 匯入</string>
<string name="external">外部</string>
<string name="application_broken">應用損壞</string>
<string name="profile">配置</string>
<string name="name">名稱</string>
<string name="profile_name">配置名稱</string>
<string name="profile_url">配置 URL</string>
<string name="auto_update">自動更新 (分鐘)</string>
<string name="more_than_15_minutes">大於15分鐘</string>
<string name="invalid_interval">無效的間隔</string>
<string name="download_failure">下載失敗</string>
<string name="detail">詳情</string>
<string name="update">更新</string>
<string name="edit">編輯</string>
<string name="delete">刪除</string>
<string name="export">匯出</string>
<string name="properties">引數</string>
<string name="duplicate">複製</string>
<string name="reset_provider">重置外部引用</string>
<string name="exit_without_save">退出而不儲存</string>
<string name="exit_without_save_warning">所有變更將會丟失</string>
<string name="disabled">已禁用</string>
<string name="empty_name">空名稱</string>
<string name="invalid_url">無效的 URL</string>
<string name="processing">處理中</string>
<string name="loading">載入中</string>
<string name="new_profile">新配置</string>
<string name="clone_profile">複製配置</string>
<string name="edit_profile">編輯配置</string>
<string name="clash_start_failure">Clash 啟動失敗</string>
<string name="refresh">重新整理</string>
<string name="delay">延遲</string>
<string name="direct">直連</string>
<string name="global">全域性</string>
<string name="rule">規則</string>
<string name="mode">模式</string>
<string name="sort_group">代理組排序</string>
<string name="sort_proxy">代理排序</string>
<string name="default_">預設</string>
<string name="utils">實用工具</string>
<string name="merge_prefix">前綴合並</string>
<string name="start_url_provider_failure">開啟 URL 提供程式失敗</string>
<string name="clash_new_profile">新配置</string>
<string name="ok">確認</string>
<string name="cancel">取消</string>
<string name="clash_logcat">Clash 日誌捕捉工具</string>
<string name="history">歷史</string>
<string name="log_viewer">日誌檢視器</string>
<string name="format_export_log_name">%s.log</string>
<string name="delete_all_logs">刪除所有日誌</string>
<string name="delete_all_logs_warn">所有歷史日誌將丟失</string>
<string name="delete_log">刪除日誌</string>
<string name="delete_log_warn">%s 將被刪除</string>
<string name="open_log_failure">開啟日誌失敗</string>
<string name="file_exported">檔案已匯出</string>
<string name="behavior">行為</string>
<string name="network">網路</string>
<string name="interface_">介面</string>
<string name="restart">重啟</string>
<string name="auto_restart">自動重啟</string>
<string name="allow_clash_auto_restart">允許 Clash 自動重啟</string>
<string name="notification">通知</string>
<string name="show_traffic">顯示流量</string>
<string name="show_traffic_summary">在通知中自動重新整理流量</string>
<string name="route_system_traffic">自動路由系統流量</string>
<string name="routing_via_vpn_service">通過 VpnService 自動路由所有系統流量</string>
<string name="vpn_service">VPN Service</string>
<string name="bypass_private_network">繞過私有網路</string>
<string name="bypass_private_network_summary">繞過私有網路地址</string>
<string name="dns_hijacking">DNS 劫持</string>
<string name="dns_hijacking_summary">處理所有 DNS 資料包</string>
<string name="dns_override">DNS 配置覆蓋</string>
<string name="dns_override_summary">強制使用內建 DNS 配置</string>
<string name="append_system_dns">追加系統 DNS</string>
<string name="append_system_dns_summary">自動追加系統 DNS 到 Clash</string>
<string name="access_control_mode">訪問控制模式</string>
<string name="access_control_packages">訪問控制應用包列表</string>
<string name="access_control_packages_summary">為應用配置訪問許可權</string>
<string name="language">語言</string>
<string name="dark_mode">暗黑模式</string>
<string name="options_unavailable">選項在 Clash 執行時不可用</string>
<string name="system_apps">系統應用</string>
<string name="sort">排序</string>
<string name="package_name">應用包名稱</string>
<string name="update_time">更新時間</string>
<string name="install_time">安裝時間</string>
<string name="reverse">反轉</string>
<string name="search">查詢</string>
<string name="sources">原始碼</string>
<string name="clash">Clash</string>
<string name="clash_for_android">Clash for Android</string>
<string name="feedback">反饋</string>
<string name="donate">捐贈</string>
<string name="github_issues">Github Issues</string>
<string name="telegram_channel">Telegram 頻道</string>
<string name="tips_profile"><![CDATA[僅接受 <strong>Clash 配置檔案</strong> <br />其中包含了 <strong>代理, 代理組 和 規則</strong>]]></string>
<string name="tips_support"><![CDATA[Clash for Android 是一個<strong>免費開源</strong>的專案<br /> 我們<strong>不提供</strong>任何代理服務<br />請務必<strong>不要</strong>反饋非應用自身引起的問題]]></string>
<string name="application_broken_description"><![CDATA[這通常意味著您使用來自 <strong>Google Play</strong> 的副本, 但是生成該副本的應用未能正確處理 <strong>分包機制</strong><br />這意味著您獲取的是 <strong>應用的一部分</strong>]]></string>
<string name="learn_more_about_split_apks">瞭解更多關於分包機制</string>
<string name="reinstall_from_google_play">重新從 Google Play 安裝</string>
<string name="download_from_github_releases">從 Github Release 下載</string>
<string name="missing_vpn_component">系統 VPN 元件缺失</string>
<string name="profile_not_found">配置檔案丟失</string>
<string name="upload_logcat">上傳日誌</string>
<string name="upload_logcat_summary">上傳日誌以幫助我們偵測問題</string>
<string name="upload_logcat_warn">請注意, 上傳的日誌可能包含個人敏感資訊, 仍要繼續嗎?</string>
<string name="uploaded">已上傳</string>
</resources>

View File

@@ -14,5 +14,7 @@
<item>自动</item>
<item>英文</item>
<item>简体中文</item>
<item>繁体中文-香港</item>
<item>繁体中文-台湾</item>
</string-array>
</resources>

View File

@@ -131,4 +131,8 @@
<string name="tips_support"><![CDATA[Clash for Android 是一个<strong>免费开源</strong>的项目<br /> 我们<strong>不提供</strong>任何代理服务<br />请务必<strong>不要</strong>反馈非应用自身引起的问题]]></string>
<string name="donate">捐赠</string>
<string name="clone_profile">复制配置</string>
<string name="upload_logcat">上传日志</string>
<string name="upload_logcat_summary">上传日志以帮助我们侦测问题</string>
<string name="upload_logcat_warn">请注意, 上传的日志可能包含个人敏感信息, 仍要继续吗?</string>
<string name="uploaded">已上传</string>
</resources>

View File

@@ -14,6 +14,8 @@
<item>Auto</item>
<item>English</item>
<item>Simplified Chinese</item>
<item>Traditional Chinese-Hong Kong</item>
<item>Traditional Chinese-Taiwan</item>
</string-array>
<string-array name="dark_mode_value" translatable="false">
@@ -30,5 +32,7 @@
<item />
<item>en</item>
<item>zh-rCN</item>
<item>zh-rHK</item>
<item>zh-rTW</item>
</string-array>
</resources>

View File

@@ -173,4 +173,9 @@
<string name="missing_vpn_component">Missing VPN Components</string>
<string name="profile_not_found">Profile not found</string>
<string name="upload_logcat">Upload Logcat</string>
<string name="upload_logcat_summary">Upload logcat to help us detect issues</string>
<string name="upload_logcat_warn">Please note that the uploaded logs may contain personally sensitive information, do you still want to continue?</string>
<string name="uploaded">Uploaded</string>
</resources>

View File

@@ -1,38 +1,16 @@
// Top-level build file where you can add configuration options common to all sub-projects/modules.
buildscript {
val kotlinVersion = "1.3.71"
val gKotlinVersion: String by project
rootProject.extra.apply {
this["gBuildToolsVersion"] = "29.0.3"
this["gCompileSdkVersion"] = 29
this["gMinSdkVersion"] = 24
this["gTargetSdkVersion"] = 29
this["gVersionCode"] = 10204
this["gVersionName"] = "1.2.4"
this["gKotlinVersion"] = kotlinVersion
this["gKotlinCoroutineVersion"] = "1.3.5"
this["gKotlinSerializationVersion"] = "0.20.0"
this["gRoomVersion"] = "2.2.5"
this["gAppCenterVersion"] = "2.5.1"
this["gAndroidKtxVersion"] = "1.2.0"
this["gRecyclerviewVersion"] = "1.1.0"
this["gAppCompatVersion"] = "1.1.0"
this["gMaterialDesignVersion"] = "1.1.0"
this["gShizukuPreferenceVersion"] = "4.2.0"
this["gMultiprocessPreferenceVersion"] = "1.0.0"
}
repositories {
google()
jcenter()
}
dependencies {
classpath("com.android.tools.build:gradle:4.0.0-beta04")
classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlinVersion")
classpath("org.jetbrains.kotlin:kotlin-serialization:$kotlinVersion")
classpath("com.android.tools.build:gradle:4.0.0")
classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:$gKotlinVersion")
classpath("org.jetbrains.kotlin:kotlin-serialization:$gKotlinVersion")
}
}

View File

@@ -4,21 +4,19 @@ plugins {
id("kotlin-android-extensions")
}
val rootExtra = rootProject.extra
val gCompileSdkVersion: String by project
val gBuildToolsVersion: String by project
val gCompileSdkVersion: Int by rootExtra
val gBuildToolsVersion: String by rootExtra
val gMinSdkVersion: String by project
val gTargetSdkVersion: String by project
val gMinSdkVersion: Int by rootExtra
val gTargetSdkVersion: Int by rootExtra
val gVersionCode: String by project
val gVersionName: String by project
val gVersionCode: Int by rootExtra
val gVersionName: String by rootExtra
val gKotlinVersion: String by rootExtra
val gKotlinCoroutineVersion: String by rootExtra
val gAndroidKtxVersion: String by rootExtra
val gKotlinSerializationVersion: String by rootExtra
val gKotlinVersion: String by project
val gKotlinCoroutineVersion: String by project
val gAndroidKtxVersion: String by project
val gKotlinSerializationVersion: String by project
android {
compileSdkVersion(gCompileSdkVersion)
@@ -28,14 +26,14 @@ android {
minSdkVersion(gMinSdkVersion)
targetSdkVersion(gTargetSdkVersion)
versionCode = gVersionCode
versionCode = gVersionCode.toInt()
versionName = gVersionName
consumerProguardFiles("consumer-rules.pro")
}
buildTypes {
maybeCreate("release").apply {
named("release") {
isMinifyEnabled = false
proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro")
}

View File

@@ -1,4 +1,4 @@
apply(from = "clash.gradle.kts")
import android.databinding.tool.ext.toCamelCase
plugins {
id("com.android.library")
@@ -7,23 +7,26 @@ plugins {
id("kotlinx-serialization")
}
val rootExtra = rootProject.extra
apply(from = "clash.gradle.kts")
val gCompileSdkVersion: Int by rootExtra
val gBuildToolsVersion: String by rootExtra
val gCompileSdkVersion: String by project
val gBuildToolsVersion: String by project
val gMinSdkVersion: Int by rootExtra
val gTargetSdkVersion: Int by rootExtra
val gMinSdkVersion: String by project
val gTargetSdkVersion: String by project
val gVersionCode: Int by rootExtra
val gVersionName: String by rootExtra
val gVersionCode: String by project
val gVersionName: String by project
val gKotlinVersion: String by rootExtra
val gKotlinCoroutineVersion: String by rootExtra
val gKotlinSerializationVersion: String by rootExtra
val gAndroidKtxVersion: String by rootExtra
val gKotlinVersion: String by project
val gKotlinCoroutineVersion: String by project
val gKotlinSerializationVersion: String by project
val gAndroidKtxVersion: String by project
val clashCoreOutput = buildDir.resolve("extraSources")
val geoipOutput = buildDir.resolve("outputs/geoip")
val golangSource = file("src/main/golang")
val golangOutput = buildDir.resolve("outputs/golang")
val nativeAbis = listOf("armeabi-v7a", "arm64-v8a", "x86", "x86_64")
android {
compileSdkVersion(gCompileSdkVersion)
@@ -33,24 +36,30 @@ android {
minSdkVersion(gMinSdkVersion)
targetSdkVersion(gTargetSdkVersion)
versionCode = gVersionCode
versionCode = gVersionCode.toInt()
versionName = gVersionName
consumerProguardFiles("consumer-rules.pro")
externalNativeBuild {
cmake {
abiFilters(*nativeAbis.toTypedArray())
arguments("-DCLASH_OUTPUT=$golangOutput", "-DCLASH_SOURCE=$golangSource")
}
}
}
buildTypes {
maybeCreate("release").apply {
named("release") {
isMinifyEnabled = false
proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro")
}
}
sourceSets {
maybeCreate("main").apply {
assets.srcDir(clashCoreOutput.resolve("assets"))
jniLibs.srcDir(clashCoreOutput.resolve("jniLibs"))
java.srcDir(clashCoreOutput.resolve("classes"))
named("main") {
assets.srcDir(geoipOutput)
jniLibs.srcDir(golangOutput)
}
}
@@ -62,6 +71,12 @@ android {
kotlinOptions {
jvmTarget = "1.8"
}
externalNativeBuild {
cmake {
path = file("src/main/cpp/CMakeLists.txt")
}
}
}
dependencies {
@@ -77,6 +92,10 @@ repositories {
}
afterEvaluate {
tasks["clean"].dependsOn(tasks["resetGolangMode"])
tasks["preBuild"].dependsOn(tasks["extractSources"], tasks["downloadGeoipDatabase"])
android.buildTypes.forEach {
val cName = it.name.toCamelCase()
tasks["externalNativeBuild${cName}"].dependsOn(tasks["compileClashCore"])
tasks["package${cName}Assets"].dependsOn(tasks["downloadGeoipDatabase"])
}
}

View File

@@ -2,85 +2,110 @@ import org.apache.tools.ant.taskdefs.condition.Os
import java.io.*
import java.util.*
import java.net.*
import java.time.*
object Constants {
const val GEOIP_DATABASE_URL = "https://github.com/Dreamacro/maxmind-geoip/releases/latest/download/Country.mmdb"
const val GEOIP_INVALID_INTERVAL = 1000L * 60 * 60 * 24 * 7
val gMinSdkVersion: String by project
const val SOURCE_PATH = "src/main/golang"
const val OUTPUT_PATH = "extraSources"
val geoipDatabaseUrl = "https://github.com/Dreamacro/maxmind-geoip/releases/latest/download/Country.mmdb"
val geoipInvalidate = Duration.ofDays(7)
val geoipOutput = buildDir.resolve("outputs/geoip")
val golangSource = file("src/main/golang")
val golangOutput = buildDir.resolve("outputs/golang")
val nativeAbis = listOf("armeabi-v7a", "arm64-v8a", "x86", "x86_64")
const val GOLANG_BASE = "intermediates/golang"
const val GOLANG_PATH = "$GOLANG_BASE/path"
const val GOLANG_BIND = "$GOLANG_BASE/bind"
const val GOLANG_BINARY = "$GOLANG_PATH/bin"
const val GOLANG_OUTPUT = "$GOLANG_BASE/bridge.aar"
const val GOLANG_OUTPUT_SOURCES = "$GOLANG_BASE/bridge-sources.jar"
val String.exe: String
get() {
return if ( Os.isFamily(Os.FAMILY_WINDOWS) )
"$this.exe"
else
this
}
val STUB_GO_FILE_CONTENT = """
package main
fun generateGolangBuildEnvironment(abi: String): Map<String, String> {
val properties = Properties().apply {
load(FileInputStream(rootProject.file("local.properties")))
}
import "github.com/kr328/cfa/bridge"
func main() {}
""".trimIndent()
val STUB_GO_MOD_CONTENT = """
module github.com/kr328/cfa-bind
require github.com/kr328/cfa v0.0.0 // redirect
replace github.com/kr328/cfa => {SOURCE_PATH}
""".trimIndent()
val ndk = properties.getProperty("ndk.dir")
?: throw GradleScriptException("ndk.dir not found in local.properties",
FileNotFoundException("ndk.dir not found in local.properties"))
val REGEX_REPLACE = Regex("replace\\s+(\\S+)\\s+(\\S*)\\s*=>\\s*(\\S+)\\s*(\\S*)\\s*")
val REGEX_JNI = Regex("^jni/")
}
val host = when {
Os.isFamily(Os.FAMILY_WINDOWS) ->
"windows"
Os.isFamily(Os.FAMILY_MAC) ->
"darwin"
Os.isFamily(Os.FAMILY_UNIX) ->
"linux"
else ->
throw GradleScriptException("Unsupported host", FileNotFoundException("Unsupported host"))
}
fun generateGolangBuildEnvironment(vararg pathAppend: String): Map<String, String> {
val environment = TreeMap<String, String>(String.CASE_INSENSITIVE_ORDER).apply { putAll(System.getenv()) }
val properties = Properties().apply { load(rootProject.file("local.properties").inputStream()) }
val minSdkVersion = gMinSdkVersion.removePrefix("android-")
val sdkPath = properties.getProperty("sdk.dir")
?: throw GradleScriptException("sdk.dir not found", FileNotFoundException())
val ndkPath = properties.getProperty("ndk.dir")
?: throw GradleScriptException("ndk.dir not found", FileNotFoundException())
val compilerBase = rootProject.file(ndk).resolve("toolchains/llvm/prebuilt/$host-x86_64/bin")
val pathSeparator = if ( Os.isFamily(Os.FAMILY_WINDOWS) ) ";" else ":"
val cCompiler = when(abi) {
"armeabi-v7a" ->
"armv7a-linux-androideabi$minSdkVersion-clang"
"arm64-v8a" ->
"aarch64-linux-android$minSdkVersion-clang"
"x86" ->
"i686-linux-android$minSdkVersion-clang"
"x86_64" ->
"x86_64-linux-android$minSdkVersion-clang"
else ->
throw GradleScriptException("Unsupported abi $abi", FileNotFoundException("Unsupported abi $abi"))
}
environment["GOPATH"] = listOf(buildDir.resolve(Constants.GOLANG_PATH).absolutePath, *pathAppend)
.joinToString(separator = pathSeparator)
environment["ANDROID_HOME"] = sdkPath
environment["ANDROID_NDK_HOME"] = ndkPath
environment["PATH"] += "$pathSeparator${buildDir.resolve(Constants.GOLANG_BINARY)}"
val cppCompiler = when(abi) {
"armeabi-v7a" ->
"armv7a-linux-androideabi$minSdkVersion-clang++"
"arm64-v8a" ->
"aarch64-linux-android$minSdkVersion-clang++"
"x86" ->
"i686-linux-android$minSdkVersion-clang++"
"x86_64" ->
"x86_64-linux-android$minSdkVersion-clang++"
else ->
throw GradleScriptException("Unsupported abi $abi", FileNotFoundException("Unsupported abi $abi"))
}
return environment
}
val linker = when(abi) {
"armeabi-v7a" ->
"arm-linux-androideabi-ld"
"arm64-v8a" ->
"aarch64-linux-android-ld"
"x86" ->
"i686-linux-android-ld"
"x86_64" ->
"x86_64-linux-android-ld"
else ->
throw GradleScriptException("Unsupported abi $abi", FileNotFoundException("Unsupported abi $abi"))
}
fun generateGolangModule(): String {
val moduleFile = file(Constants.SOURCE_PATH).resolve("go.mod")
val golangArch = when(abi) {
"armeabi-v7a" ->
"arm"
"arm64-v8a" ->
"arm64"
"x86" ->
"386"
"x86_64" ->
"amd64"
else ->
throw GradleScriptException("Unsupported abi $abi", FileNotFoundException("Unsupported abi $abi"))
}
val replaces = moduleFile
.readLines()
.asSequence()
.map { line -> Constants.REGEX_REPLACE.matchEntire(line) }
.filterNotNull()
.map { match ->
val source = match.groupValues[1].trim()
val sVersion = match.groupValues[2].trim()
val target = match.groupValues[3].trim()
val tVersion = match.groupValues[4].trim()
val resolvedTarget = if ( target.startsWith("./") )
moduleFile.parentFile!!.resolve(target).canonicalPath
else
target
"replace $source $sVersion => $resolvedTarget $tVersion"
}.joinToString(separator = "\n")
return Constants.STUB_GO_MOD_CONTENT
.replace("{SOURCE_PATH}", file(Constants.SOURCE_PATH).absolutePath) + replaces
return mapOf(
"CC" to compilerBase.resolve(cCompiler.exe).absolutePath,
"CXX" to compilerBase.resolve(cppCompiler.exe).absolutePath,
"LD" to compilerBase.resolve(linker.exe).absolutePath,
"GOOS" to "android",
"GOARCH" to golangArch,
"CGO_ENABLED" to "1",
"CFLAGS" to "-O3 -Werror"
)
}
fun String.exec(pwd: File = buildDir, env: Map<String, String> = System.getenv()): String {
@@ -109,114 +134,51 @@ fun String.exec(pwd: File = buildDir, env: Map<String, String> = System.getenv()
return outputStream.toString("utf-8")
}
task("generateClashBindSources") {
task("compileClashCore") {
onlyIf {
val lastModified = file(Constants.SOURCE_PATH).walk()
.filter { it.extension == "go" || it.extension == "mod" }
val sourceModified = golangSource.walk()
.filter {
when ( it.extension ) {
"c", "cpp", "h", "go", "mod" -> true
else -> false
}
}
.map { it.lastModified() }
.max() ?: 0L
.max() ?: Long.MAX_VALUE
val targetModified = golangOutput.walk()
.filter { it.extension == "so" }
.map { it.lastModified() }
.min() ?: Long.MIN_VALUE
return@onlyIf lastModified > buildDir.resolve(Constants.GOLANG_OUTPUT).lastModified()
}
doFirst {
buildDir.resolve(Constants.GOLANG_BIND).apply {
deleteRecursively()
mkdirs()
}
sourceModified > targetModified
}
doLast {
val environment = generateGolangBuildEnvironment()
nativeAbis.parallelStream().forEach {
val env = generateGolangBuildEnvironment(it)
val out = golangOutput.resolve(it).apply {
mkdirs()
}.resolve("libclash.so")
val bind = buildDir.resolve(Constants.GOLANG_BIND).apply {
resolve("main.go").writeText(Constants.STUB_GO_FILE_CONTENT)
resolve("go.mod").writeText(generateGolangModule())
}
"go mod vendor".exec(pwd = bind, env = environment)
buildDir.resolve(Constants.GOLANG_BIND).apply {
resolve("vendor").renameTo(resolve("src"))
resolve("go.mod").delete()
resolve("main.go").delete()
resolve("go.sum").delete()
"go build --buildmode=c-shared -trimpath -o \"$out\"".exec(pwd = golangSource, env = env)
}
}
}
task("bindClashCore") {
dependsOn(tasks["generateClashBindSources"])
onlyIf {
!tasks["generateClashBindSources"].state.skipped
}
doFirst {
val environment = generateGolangBuildEnvironment()
"go get golang.org/x/mobile/cmd/gomobile".exec(env = environment)
}
doLast {
val bind = buildDir.resolve(Constants.GOLANG_BIND)
val environment = generateGolangBuildEnvironment(bind.absolutePath)
"gomobile init".exec(pwd = bind, env = environment)
"gomobile bind -target=android -trimpath github.com/kr328/cfa/bridge"
.exec(pwd = buildDir.resolve(Constants.GOLANG_BASE), env = environment)
}
}
task("extractSources", type = Copy::class) {
dependsOn(tasks["bindClashCore"])
doFirst {
buildDir.resolve(Constants.OUTPUT_PATH).apply {
resolve("jniLibs").deleteRecursively()
resolve("classes").deleteRecursively()
}
}
from(zipTree(buildDir.resolve(Constants.GOLANG_OUTPUT))) {
include("**/*.so")
eachFile {
path = path.replace(Constants.REGEX_JNI, "jniLibs/")
}
}
from(zipTree(buildDir.resolve(Constants.GOLANG_OUTPUT_SOURCES))) {
include("**/*.java")
into("classes")
}
destinationDir = buildDir.resolve(Constants.OUTPUT_PATH)
}
task("downloadGeoipDatabase") {
onlyIf {
val file = buildDir.resolve(Constants.OUTPUT_PATH).resolve("assets/Country.mmdb")
val geoipFile = geoipOutput.resolve("Country.mmdb")
System.currentTimeMillis() - file.lastModified() > Constants.GEOIP_INVALID_INTERVAL
onlyIf {
System.currentTimeMillis() - geoipFile.lastModified() > geoipInvalidate.toMillis()
}
doLast {
val assets = buildDir.resolve(Constants.OUTPUT_PATH).resolve("assets")
geoipOutput.mkdirs()
assets.mkdirs()
URL(Constants.GEOIP_DATABASE_URL).openConnection().getInputStream().use { input ->
FileOutputStream(assets.resolve("Country.mmdb")).use { output ->
URL(geoipDatabaseUrl).openConnection().getInputStream().use { input ->
FileOutputStream(geoipFile).use { output ->
input.copyTo(output)
}
}
}
}
task("resetGolangMode", type = Exec::class) {
onlyIf {
!Os.isFamily(Os.FAMILY_WINDOWS)
}
commandLine("chmod", "-R", "777", buildDir.resolve(Constants.GOLANG_PATH))
isIgnoreExitValue = true
}

View File

@@ -1,6 +0,0 @@
-keep class go.* {
*;
}
-keep class bridge.* {
*;
}

View File

@@ -0,0 +1,11 @@
cmake_minimum_required(VERSION 3.0)
project(clash_bridge)
set(CMAKE_C_STANDARD 11)
set(CMAKE_CXX_STANDARD 11)
include_directories(${CLASH_OUTPUT}/${ANDROID_ABI} ${CLASH_SOURCE})
link_directories(${CLASH_OUTPUT}/${CMAKE_ANDROID_ARCH_ABI})
link_libraries(log clash)
add_library(bridge SHARED main.cpp main.h init.cpp query.cpp patch.cpp tun.cpp defer.cpp log.cpp event_queue.cpp)

112
core/src/main/cpp/defer.cpp Normal file
View File

@@ -0,0 +1,112 @@
#include "main.h"
#include <android/log.h>
static std::pair<jobject, uint64_t> completableFutureWithToken(Master::Context *context) {
uint64_t token = EventQueue::getInstance()->obtainToken();
jobject completableFuture = context->newGlobalReference(context->newCompletableFuture());
EventQueue::getInstance()->registerHandler(COMPLETE, token, [completableFuture](const event_t *event) {
Master::runWithAttached<int>([&](JNIEnv *env) -> int {
Master::runWithContext<void>(env, [&](Master::Context *context) {
if ( strlen(event->payload) == 0 ) {
context->completeCompletableFuture(completableFuture, nullptr);
} else {
context->completeExceptionallyCompletableFuture(completableFuture, context->newClashException(event->payload));
}
context->removeGlobalReference(completableFuture);
});
return 0;
});
EventQueue::getInstance()->unregisterHandler(COMPLETE, event->token);
});
return {completableFuture, token};
}
extern "C"
JNIEXPORT jobject JNICALL
Java_com_github_kr328_clash_core_bridge_Bridge_downloadProfile__ILjava_lang_String_2Ljava_lang_String_2(
JNIEnv *env, jclass clazz, jint fd, jstring base, jstring output) {
UNUSED(clazz);
return Master::runWithContext<jobject>(env, [&](Master::Context *context) -> jobject {
const char *b = context->getString(base);
const char *o = context->getString(output);
auto completableFuture = completableFutureWithToken(context);
downloadProfileFromFd(fd, b, o, completableFuture.second);
context->releaseString(base, b);
context->releaseString(output, o);
return completableFuture.first;
});
}
extern "C"
JNIEXPORT jobject JNICALL
Java_com_github_kr328_clash_core_bridge_Bridge_downloadProfile__Ljava_lang_String_2Ljava_lang_String_2Ljava_lang_String_2(
JNIEnv *env, jclass clazz, jstring url, jstring base, jstring output) {
UNUSED(clazz);
return Master::runWithContext<jobject>(env, [&](Master::Context *context) -> jobject {
const char *u = context->getString(url);
const char *b = context->getString(base);
const char *o = context->getString(output);
auto completableFuture = completableFutureWithToken(context);
downloadProfileFromUrl(u, b, o, completableFuture.second);
context->releaseString(url, u);
context->releaseString(base, b);
context->releaseString(output, o);
return completableFuture.first;
});
}
extern "C"
JNIEXPORT jobject JNICALL
Java_com_github_kr328_clash_core_bridge_Bridge_loadProfile(JNIEnv *env, jclass clazz, jstring path,
jstring base) {
UNUSED(clazz);
return Master::runWithContext<jobject>(env, [&](Master::Context *context) -> jobject {
const char *p = context->getString(path);
const char *b = context->getString(base);
auto completableFuture = completableFutureWithToken(context);
loadProfile(p, b, completableFuture.second);
context->releaseString(path, p);
context->releaseString(base, b);
return completableFuture.first;
});
}
extern "C"
JNIEXPORT jobject JNICALL
Java_com_github_kr328_clash_core_bridge_Bridge_performHealthCheck(JNIEnv *env, jclass clazz,
jstring group) {
UNUSED(clazz);
return Master::runWithContext<jobject>(env, [&](Master::Context *context) -> jobject {
const char *g = context->getString(group);
auto completableFuture = completableFutureWithToken(context);
performHealthCheck(g, completableFuture.second);
context->releaseString(group, g);
return completableFuture.first;
});
}

View File

@@ -0,0 +1,101 @@
#include "event_queue.h"
static void *handleEventQueue(void *context) {
auto *queue = reinterpret_cast<EventQueue*>(context);
while ( !queue->isClosed() ) {
const event_t *e = queue->dequeueEvent();
EventQueue::Handler h = queue->findHandler(e->type, e->token);
h(e);
answer_event(e);
}
return nullptr;
}
EventQueue::EventQueue(): lock(), condition(), closed(false), currentToken(0) {
instance = this;
pthread_mutex_init(&lock, nullptr);
pthread_cond_init(&condition, nullptr);
for ( int i = 0 ; i < DEFAULT_EVENT_QUEUE_PROCESSES ; i++ ) {
pthread_t tid = 0;
if ( pthread_create(&tid, nullptr, &handleEventQueue, this) < 0 )
abort();
}
}
void EventQueue::enqueueEvent(const event_t *event) {
pthread_mutex_lock(&lock);
queue.push_back(event);
pthread_cond_signal(&condition);
pthread_mutex_unlock(&lock);
}
const event_t *EventQueue::dequeueEvent() {
pthread_mutex_lock(&lock);
while ( queue.empty() )
pthread_cond_wait(&condition, &lock);
auto *result = queue.back();
queue.pop_back();
pthread_mutex_unlock(&lock);
return result;
}
void EventQueue::registerHandler(event_type_t type, uint64_t token, const EventQueue::Handler& handler) {
pthread_mutex_lock(&lock);
handlers[type][token] = handler;
pthread_mutex_unlock(&lock);
}
void EventQueue::unregisterHandler(event_type_t type, uint64_t token) {
pthread_mutex_lock(&lock);
handlers[type].erase(token);
pthread_mutex_unlock(&lock);
}
EventQueue::Handler EventQueue::findHandler(event_type_t type, uint64_t token) {
pthread_mutex_lock(&lock);
Handler result = handlers[type][token];
pthread_mutex_unlock(&lock);
if (result == nullptr)
return [](const event_t*){};
return result;
}
EventQueue *EventQueue::getInstance() {
return instance;
}
uint64_t EventQueue::obtainToken() {
pthread_mutex_lock(&lock);
uint64_t r = currentToken++;
pthread_mutex_unlock(&lock);
return r;
}
EventQueue *EventQueue::instance;

View File

@@ -0,0 +1,51 @@
#pragma once
#include <pthread.h>
#include <vector>
#include <map>
#include <functional>
#include <cstdint>
#include <string>
#include "event.h"
#define DEFAULT_EVENT_QUEUE_PROCESSES 8
class EventQueue {
public:
typedef std::function<void (const event_t *event)> Handler;
public:
EventQueue();
public:
void enqueueEvent(const event_t *event);
const event_t *dequeueEvent();
public:
void registerHandler(event_type_t type, uint64_t token, const Handler& handler);
void unregisterHandler(event_type_t type, uint64_t token);
Handler findHandler(event_type_t type, uint64_t token);
public:
uint64_t obtainToken();
public:
static EventQueue *getInstance();
public:
inline bool isClosed() {
return closed;
}
private:
bool closed;
std::vector<const event_t*> queue;
std::map<event_type_t, std::map<uint64_t, Handler>> handlers;
pthread_mutex_t lock;
pthread_cond_t condition;
uint64_t currentToken;
public:
static EventQueue *instance;
};

View File

@@ -0,0 +1,30 @@
#include "main.h"
extern "C"
JNIEXPORT void JNICALL
Java_com_github_kr328_clash_core_bridge_Bridge_initialize(JNIEnv *env, jclass clazz,
jbyteArray database, jstring home,
jstring version) {
UNUSED(clazz);
Master::runWithContext<void>(env, [&](Master::Context *context) {
const_buffer_t databaseBuffer = context->createConstBufferFromByteArray(database);
const char *homeString = context->getString(home);
const char *versionString = context->getString(version);
initialize(&databaseBuffer, homeString, versionString);
context->releaseConstBufferFromByteArray(database, databaseBuffer);
context->releaseString(home, homeString);
context->releaseString(version, versionString);
});
}
extern "C"
JNIEXPORT void JNICALL
Java_com_github_kr328_clash_core_bridge_Bridge_reset(JNIEnv *env, jclass clazz) {
UNUSED(env);
UNUSED(clazz);
reset();
}

51
core/src/main/cpp/log.cpp Normal file
View File

@@ -0,0 +1,51 @@
#include "libclash.h"
#include "main.h"
static jobject logCallback;
extern "C"
JNIEXPORT void JNICALL
Java_com_github_kr328_clash_core_bridge_Bridge_setLogCallback(JNIEnv *env, jclass clazz,
jobject callback) {
UNUSED(clazz);
Master::runWithContext<void>(env, [&](Master::Context *context) {
if ( logCallback != nullptr )
context->removeGlobalReference(logCallback);
if ( callback == nullptr ) {
logCallback = nullptr;
return;
}
logCallback = context->newGlobalReference(callback);
EventQueue::getInstance()->registerHandler(LOG_RECEIVED, 0, [](const event_t *event) {
Master::runWithAttached<int>([&](JNIEnv *env) {
Master::runWithContext<void>(env, [&](Master::Context *context) {
context->logCallbackMessage(logCallback, event->payload);
});
return 0;
});
});
});
}
extern "C"
JNIEXPORT void JNICALL
Java_com_github_kr328_clash_core_bridge_Bridge_enableLogReport(JNIEnv *env, jclass clazz) {
UNUSED(env);
UNUSED(clazz);
enableLogReport();
}
extern "C"
JNIEXPORT void JNICALL
Java_com_github_kr328_clash_core_bridge_Bridge_disableLogReport(JNIEnv *env, jclass clazz) {
UNUSED(env);
UNUSED(clazz);
disableLogReport();
}

257
core/src/main/cpp/main.cpp Normal file
View File

@@ -0,0 +1,257 @@
#include "main.h"
#include <android/log.h>
Master *Master::master = nullptr;
template <class T>
inline T g(JNIEnv *env, T object) {
return reinterpret_cast<T>(env->NewGlobalRef(object));
}
Master::Master(JavaVM *vm, JNIEnv *env): vm(vm) {
master = this;
cClashException = g<jclass>(env, env->FindClass("com/github/kr328/clash/core/bridge/ClashException"));
cTraffic = g<jclass>(env, env->FindClass("com/github/kr328/clash/core/model/Traffic"));
cGeneral = g<jclass>(env, env->FindClass("com/github/kr328/clash/core/model/General"));
cCompletableFuture = g<jclass>(env, env->FindClass("java/util/concurrent/CompletableFuture"));
cProxyGroup = g<jclass>(env, env->FindClass("com/github/kr328/clash/core/model/ProxyGroup"));
cProxy = g<jclass>(env, env->FindClass("com/github/kr328/clash/core/model/Proxy"));
cLogEvent = g<jclass>(env, env->FindClass("com/github/kr328/clash/core/event/LogEvent"));
iTunCallback = g<jclass>(env, env->FindClass("com/github/kr328/clash/core/bridge/TunCallback"));
iLogCallback = g<jclass>(env, env->FindClass("com/github/kr328/clash/core/bridge/LogCallback"));
cClashExceptionConstructor = env->GetMethodID(cClashException, "<init>",
"(Ljava/lang/String;)V");
cTrafficConstructor = env->GetMethodID(cTraffic, "<init>", "(JJ)V");
cGeneralConstructor = env->GetMethodID(cGeneral, "<init>", "(Ljava/lang/String;IIII)V");
cCompletableFutureConstructor = env->GetMethodID(cCompletableFuture, "<init>", "()V");
cProxyGroupConstructor = env->GetMethodID(cProxyGroup, "<init>", "(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;[Lcom/github/kr328/clash/core/model/Proxy;)V");
cProxyConstructor = env->GetMethodID(cProxy, "<init>", "(Ljava/lang/String;Ljava/lang/String;J)V");
cLogEventConstructor = env->GetMethodID(cLogEvent, "<init>", "(Ljava/lang/String;)V");
mCompletableFutureComplete = env->GetMethodID(cCompletableFuture, "complete",
"(Ljava/lang/Object;)Z");
mCompletableFutureCompleteExceptionally = env->GetMethodID(cCompletableFuture,
"completeExceptionally",
"(Ljava/lang/Throwable;)Z");
mTunCallbackOnNewSocket = env->GetMethodID(iTunCallback, "onNewSocket", "(I)V");
mTunCallbackOnStop = env->GetMethodID(iTunCallback, "onStop", "()V");
mLogCallbackOnMessage = env->GetMethodID(iLogCallback, "onMessage", "(Lcom/github/kr328/clash/core/event/LogEvent;)V");
sDirect = g<jstring>(env, env->NewStringUTF("Direct"));
sReject = g<jstring>(env, env->NewStringUTF("Reject"));
sShadowsocks = g<jstring>(env, env->NewStringUTF("Shadowsocks"));
sSnell = g<jstring>(env, env->NewStringUTF("Snell"));
sSocks5 = g<jstring>(env, env->NewStringUTF("Socks5"));
sHttp = g<jstring>(env, env->NewStringUTF("Http"));
sVmess = g<jstring>(env, env->NewStringUTF("Vmess"));
sTrojan = g<jstring>(env, env->NewStringUTF("Trojan"));
sRelay = g<jstring>(env, env->NewStringUTF("Relay"));
sSelector = g<jstring>(env, env->NewStringUTF("Selector"));
sFallback = g<jstring>(env, env->NewStringUTF("Fallback"));
sURLTest = g<jstring>(env, env->NewStringUTF("URLTest"));
sLoadBalance = g<jstring>(env, env->NewStringUTF("LoadBalance"));
sUnknown = g<jstring>(env, env->NewStringUTF("Unknown"));
}
Master::Context::Context(JNIEnv *env) {
this->env = env;
}
jthrowable Master::Context::newClashException(const char *reason) {
return reinterpret_cast<jthrowable>(env->NewObject(master->cClashException,
master->cClashExceptionConstructor,
env->NewStringUTF(reason)));
}
void Master::Context::throwThrowable(jthrowable throwable) {
env->Throw(throwable);
}
jobject Master::Context::newTraffic(jlong upload, jlong download) {
return env->NewObject(master->cTraffic, master->cTrafficConstructor, upload, download);
}
jobject Master::Context::newGeneral(char const *mode, jint http, jint socks, jint redirect,
jint mixed) {
return env->NewObject(master->cGeneral, master->cGeneralConstructor,
env->NewStringUTF(mode), http, socks, redirect, mixed);
}
jobject Master::Context::newCompletableFuture() {
return env->NewObject(master->cCompletableFuture, master->cCompletableFutureConstructor);
}
jobject Master::Context::newGlobalReference(jobject obj) {
return env->NewGlobalRef(obj);
}
jobject Master::Context::removeGlobalReference(jobject obj) {
env->DeleteGlobalRef(obj);
return obj;
}
bool Master::Context::completeCompletableFuture(jobject completable, jobject object) {
return env->CallBooleanMethod(completable, master->mCompletableFutureComplete, object);
}
bool
Master::Context::completeExceptionallyCompletableFuture(jobject completable, jthrowable throwable) {
return env->CallBooleanMethod(completable, master->mCompletableFutureCompleteExceptionally,
throwable);
}
const_buffer_t Master::Context::createConstBufferFromByteArray(jbyteArray array) {
return {
.buffer = env->GetByteArrayElements(array, nullptr),
.length = env->GetArrayLength(array)
};
}
void Master::Context::releaseConstBufferFromByteArray(jbyteArray array, const_buffer_t &buffer) {
env->ReleaseByteArrayElements(array, const_cast<jbyte*>(reinterpret_cast<const jbyte*>(buffer.buffer)), JNI_ABORT);
}
const char *Master::Context::getString(jstring str) {
return env->GetStringUTFChars(str, nullptr);
}
void Master::Context::releaseString(jstring str, const char *c) {
env->ReleaseStringUTFChars(str, c);
}
void Master::Context::tunCallbackNewSocket(jobject callback, jint fd) {
env->CallVoidMethod(callback, master->mTunCallbackOnNewSocket, fd);
}
void Master::Context::tunCallbackStop(jobject callback) {
env->CallVoidMethod(callback, master->mTunCallbackOnStop);
}
jobjectArray Master::Context::createProxyGroupArray(int size, jobject elements[]) {
jobjectArray result = env->NewObjectArray(size, master->cProxyGroup, nullptr);
for ( int i = 0 ; i < size ; i++ )
env->SetObjectArrayElement(result, i, elements[i]);
return result;
}
jobjectArray Master::Context::createProxyArray(int size, jobject elements[]) {
jobjectArray result = env->NewObjectArray(size, master->cProxy, nullptr);
for ( int i = 0 ; i < size ; i++ )
env->SetObjectArrayElement(result, i, elements[i]);
return result;
}
jobject Master::Context::createProxy(char const *name, proxy_type_t type, jlong delay) {
jstring ts = nullptr;
switch (type) {
case Direct:
ts = master->sDirect;
break;
case Reject:
ts = master->sReject;
break;
case Socks5:
ts = master->sSocks5;
break;
case Http:
ts = master->sHttp;
break;
case Shadowsocks:
ts = master->sShadowsocks;
break;
case Vmess:
ts = master->sVmess;
break;
case Snell:
ts = master->sSnell;
break;
case Trojan:
ts = master->sTrojan;
break;
case Selector:
ts = master->sSelector;
break;
case Fallback:
ts = master->sFallback;
break;
case LoadBalance:
ts = master->sLoadBalance;
break;
case URLTest:
ts = master->sURLTest;
break;
case Relay:
ts = master->sRelay;
break;
case Unknown:
ts = master->sUnknown;
break;
default:
ts = master->sUnknown;
}
return env->NewObject(master->cProxy, master->cProxyConstructor, env->NewStringUTF(name), ts, delay);
}
jobject Master::Context::createProxyGroup(char const *name, proxy_type_t type,
char const *current, jobjectArray proxies) {
jstring ts = nullptr;
switch (type) {
case Selector:
ts = master->sSelector;
break;
case Fallback:
ts = master->sFallback;
break;
case LoadBalance:
ts = master->sLoadBalance;
break;
case URLTest:
ts = master->sURLTest;
break;
case Relay:
ts = master->sRelay;
break;
case Unknown:
ts = master->sUnknown;
break;
default:
ts = master->sUnknown;
}
return env->NewObject(master->cProxyGroup, master->cProxyGroupConstructor, env->NewStringUTF(name), ts, env->NewStringUTF(current), proxies);
}
void Master::Context::logCallbackMessage(jobject callback, const char *data) {
jobject event = env->NewObject(master->cLogEvent, master->cLogEventConstructor, env->NewStringUTF(data));
env->CallVoidMethod(callback, master->mLogCallbackOnMessage, event);
}
static void enqueue_event(const event_t *e) {
EventQueue::getInstance()->enqueueEvent(e);
}
JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM *vm, void *unused) {
UNUSED(unused);
JNIEnv *env = nullptr;
if (vm->GetEnv(reinterpret_cast<void**>(&env), JNI_VERSION_1_6) != JNI_OK)
return -1;
new Master(vm, env);
new EventQueue();
set_event_handler(&enqueue_event);
return JNI_VERSION_1_6;
}

129
core/src/main/cpp/main.h Normal file
View File

@@ -0,0 +1,129 @@
#include <jni.h>
#include <string>
#include <functional>
#include <cstdint>
#include "libclash.h"
#include "event_queue.h"
#define UNUSED(v) ((void)v)
class Master {
public:
class Context;
public:
Master(JavaVM *vm, JNIEnv *env);
public:
template <class R> static R runWithContext(JNIEnv *env, const std::function<R (Context *)>& func);
template <class R> static R runWithAttached(const std::function<R (JNIEnv *)> &func);
private:
jclass cClashException;
jclass cTraffic;
jclass cGeneral;
jclass cCompletableFuture;
jclass cProxyGroup;
jclass cProxy;
jclass cLogEvent;
jclass iTunCallback;
jclass iLogCallback;
jmethodID cClashExceptionConstructor;
jmethodID cTrafficConstructor;
jmethodID cGeneralConstructor;
jmethodID cCompletableFutureConstructor;
jmethodID cProxyGroupConstructor;
jmethodID cProxyConstructor;
jmethodID cLogEventConstructor;
jmethodID mCompletableFutureComplete;
jmethodID mCompletableFutureCompleteExceptionally;
jmethodID mTunCallbackOnNewSocket;
jmethodID mTunCallbackOnStop;
jmethodID mLogCallbackOnMessage;
private:
jstring sDirect;
jstring sReject;
jstring sShadowsocks;
jstring sSnell;
jstring sSocks5;
jstring sHttp;
jstring sVmess;
jstring sTrojan;
jstring sRelay;
jstring sSelector;
jstring sFallback;
jstring sURLTest;
jstring sLoadBalance;
jstring sUnknown;
private:
JavaVM *vm;
private:
static Master *master;
private:
friend class Context;
};
class Master::Context {
public:
public:
Context(JNIEnv *env);
public:
jthrowable newClashException(const char *message);
jobject newTraffic(jlong upload, jlong download);
jobject newGeneral(char const *mode, jint http, jint socks, jint redirect, jint mixed);
jobject newCompletableFuture();
public:
void throwThrowable(jthrowable throwable);
public:
jobject newGlobalReference(jobject obj);
jobject removeGlobalReference(jobject obj);
public:
bool completeCompletableFuture(jobject completable, jobject object);
bool completeExceptionallyCompletableFuture(jobject completable, jthrowable throwable);
void tunCallbackNewSocket(jobject callback, jint fd);
void tunCallbackStop(jobject callback);
void logCallbackMessage(jobject callback, const char *data);
public:
jobject createProxy(char const *name, proxy_type_t type, jlong delay);
jobject createProxyGroup(char const *name, proxy_type_t type, char const *current, jobjectArray proxies);
jobjectArray createProxyArray(int size, jobject elements[]);
jobjectArray createProxyGroupArray(int size, jobject elements[]);
const_buffer_t createConstBufferFromByteArray(jbyteArray array);
void releaseConstBufferFromByteArray(jbyteArray array, const_buffer_t &buffer);
const char *getString(jstring str);
void releaseString(jstring str, const char *c);
private:
JNIEnv *env;
};
template <class R>
R Master::runWithContext(JNIEnv *env, const std::function<R (Master::Context *)>& func) {
Master::Context context(env);
return func(&context);
}
template <class R> R Master::runWithAttached(const std::function<R (JNIEnv *)> &func) {
Master *m = Master::master;
JNIEnv *env;
m->vm->AttachCurrentThread(&env, nullptr);
R result = func(env);
m->vm->DetachCurrentThread();
return result;
}

View File

@@ -0,0 +1,70 @@
#include "main.h"
#include <cstring>
extern "C"
JNIEXPORT void JNICALL
Java_com_github_kr328_clash_core_bridge_Bridge_setProxyMode(JNIEnv *env, jclass clazz,
jstring proxy_mode) {
Master::runWithContext<void>(env, [&](Master::Context *context) {
const char *m = context->getString(proxy_mode);
int mode;
if ( strcmp(m, "Direct") == 0 )
mode = MODE_DIRECT;
else if ( strcmp(m, "Global") == 0 )
mode = MODE_GLOBAL;
else if ( strcmp(m, "Rule") == 0 )
mode = MODE_RULE;
else if ( strcmp(m, "Script") == 0 )
mode = MODE_SCRIPT;
else
mode = MODE_UNKNOWN;
setProxyMode(mode);
});
}
extern "C"
JNIEXPORT void JNICALL
Java_com_github_kr328_clash_core_bridge_Bridge_setDnsOverride(JNIEnv *env, jclass clazz,
jboolean override_dns,
jstring append_dns) {
Master::runWithContext<void>(env, [&](Master::Context *context) {
const char *appendDns = context->getString(append_dns);
int override = 1;
if ( override_dns )
override = 1;
else
override = 0;
dns_override_t dns = {
.override_dns = override,
.append_dns = appendDns
};
setDnsOverride(&dns);
context->releaseString(append_dns, appendDns);
});
}
extern "C"
JNIEXPORT jboolean JNICALL
Java_com_github_kr328_clash_core_bridge_Bridge_setSelector(JNIEnv *env, jclass clazz, jstring group,
jstring selected) {
UNUSED(clazz);
return Master::runWithContext<bool>(env, [&](Master::Context *context) -> bool {
const char *g = context->getString(group);
const char *s = context->getString(selected);
int r = setSelector(g, s);
context->releaseString(group, g);
context->releaseString(selected, s);
return r == 0;
});
}

109
core/src/main/cpp/query.cpp Normal file
View File

@@ -0,0 +1,109 @@
#include "main.h"
extern "C"
JNIEXPORT jobject JNICALL
Java_com_github_kr328_clash_core_bridge_Bridge_queryGeneral(JNIEnv *env, jclass clazz) {
UNUSED(clazz);
return Master::runWithContext<jobject>(env, [&](Master::Context *context) -> jobject {
general_t general;
queryGeneral(&general);
const char *mode = nullptr;
switch (general.mode) {
case MODE_DIRECT:
mode = "Direct";
break;
case MODE_GLOBAL:
mode = "Global";
break;
case MODE_RULE:
mode = "Rule";
break;
case MODE_SCRIPT:
mode = "Script";
break;
}
return context->newGeneral(mode,
general.http_port, general.socks_port,
general.redirect_port, general.mixed_port);
});
}
extern "C"
JNIEXPORT jobject JNICALL
Java_com_github_kr328_clash_core_bridge_Bridge_queryBandwidth(JNIEnv *env, jclass clazz) {
UNUSED(clazz);
return Master::runWithContext<jobject>(env, [&](Master::Context *context) -> jobject {
traffic_t traffic;
queryBandwidth(&traffic);
return context->newTraffic(traffic.upload, traffic.download);
});
}
extern "C"
JNIEXPORT jobject JNICALL
Java_com_github_kr328_clash_core_bridge_Bridge_querySpeed(JNIEnv *env, jclass clazz) {
UNUSED(clazz);
return Master::runWithContext<jobject>(env, [&](Master::Context *context) -> jobject {
traffic_t traffic;
querySpeed(&traffic);
return context->newTraffic(traffic.upload, traffic.download);
});
}
extern "C"
JNIEXPORT jobjectArray JNICALL
Java_com_github_kr328_clash_core_bridge_Bridge_queryProxyGroups(JNIEnv *env, jclass clazz) {
UNUSED(clazz);
return Master::runWithContext<jobjectArray>(env, [&](Master::Context *context) -> jobjectArray {
proxy_group_list_t *list = queryProxyGroups();
auto *jgroups = new jobject[list->size];
for (int group_index = 0 ; group_index < list->size ; group_index++ ) {
char const * now = "";
proxy_group_t *group = list->groups[group_index];
auto *jproxies = new jobject[group->proxies_size];
const char *group_name = &list->string_pool[group->base.name_index];
for ( int proxy_index = 0 ; proxy_index < group->proxies_size ; proxy_index++ ) {
proxy_t *proxy = &group->proxies[proxy_index];
const char *name = &list->string_pool[proxy->name_index];
jproxies[proxy_index] = context->createProxy(name, proxy->proxy_type, proxy->delay);
if ( proxy_index == group->now )
now = name;
}
jgroups[group_index] = context->createProxyGroup(
group_name,
group->base.proxy_type,
now,
context->createProxyArray(group->proxies_size, jproxies));
delete[] jproxies;
free(group);
}
jobjectArray result = context->createProxyGroupArray(list->size, jgroups);
delete[] jgroups;
free(list->string_pool);
free(list);
return result;
});
}

65
core/src/main/cpp/tun.cpp Normal file
View File

@@ -0,0 +1,65 @@
#include "main.h"
#include "event_queue.h"
extern "C"
JNIEXPORT void JNICALL
Java_com_github_kr328_clash_core_bridge_Bridge_startTunDevice(JNIEnv *env, jclass clazz, jint fd,
jint mtu, jstring gateway,
jstring mirror, jstring dns,
jobject callback) {
UNUSED(clazz);
Master::runWithContext<void>(env, [&](Master::Context *context) {
const char *gatewayString = context->getString(gateway);
const char *mirrorString = context->getString(mirror);
const char *dnsString = context->getString(dns);
jobject callbackGlobal = context->newGlobalReference(callback);
uint64_t token = EventQueue::getInstance()->obtainToken();
EventQueue::getInstance()->registerHandler(NEW_SOCKET, token, [callbackGlobal](const event_t *e) {
Master::runWithAttached<int>([&](JNIEnv *env) -> int {
Master::runWithContext<void>(env, [&](Master::Context *context) {
context->tunCallbackNewSocket(callbackGlobal, static_cast<int>(strtol(e->payload, nullptr, 10)));
});
return 0;
});
});
EventQueue::getInstance()->registerHandler(TUN_STOP, token, [callbackGlobal](const event_t *e) {
Master::runWithAttached<int>([&](JNIEnv *env) -> int {
Master::runWithContext<void>(env, [&](Master::Context *context) {
context->tunCallbackStop(callbackGlobal);
context->removeGlobalReference(callbackGlobal);
});
return 0;
});
auto queue = EventQueue::getInstance();
queue->unregisterHandler(NEW_SOCKET, e->token);
queue->unregisterHandler(TUN_STOP, e->token);
});
char *exception = startTunDevice(fd, mtu, gatewayString, mirrorString, dnsString, token);
context->releaseString(gateway, gatewayString);
context->releaseString(mirror, mirrorString);
context->releaseString(dns, dnsString);
if ( exception != nullptr ) {
context->throwThrowable(context->newClashException(exception));
context->removeGlobalReference(callbackGlobal);
free(exception);
}
});
}
extern "C"
JNIEXPORT void JNICALL
Java_com_github_kr328_clash_core_bridge_Bridge_stopTunDevice(JNIEnv *env, jclass clazz) {
stopTunDevice();
}

View File

@@ -1,6 +0,0 @@
package bridge
type DoneCallback interface {
Done()
DoneWithError(error)
}

View File

@@ -1,38 +0,0 @@
package bridge
import (
"github.com/Dreamacro/clash/hub/executor"
"github.com/Dreamacro/clash/tunnel"
)
type TunnelGeneral struct {
Mode string
HTTPPort int
SocksPort int
RedirectPort int
}
func QueryGeneral() *TunnelGeneral {
result := &TunnelGeneral{}
g := executor.GetGeneral()
m := tunnel.Mode()
result.Mode = m.String()
result.HTTPPort = g.Port
result.SocksPort = g.SocksPort
result.RedirectPort = g.RedirPort
return result
}
func SetProxyMode(mode string) {
switch mode {
case "Direct":
tunnel.SetMode(tunnel.Direct)
case "Global":
tunnel.SetMode(tunnel.Global)
case "Rule":
tunnel.SetMode(tunnel.Rule)
}
}

View File

@@ -1,61 +0,0 @@
package bridge
import (
"github.com/Dreamacro/clash/component/mmdb"
C "github.com/Dreamacro/clash/constant"
"github.com/Dreamacro/clash/log"
"github.com/Dreamacro/clash/tunnel"
"github.com/kr328/cfa/config"
"sync"
)
var (
logCallback LogCallback
logSubscribe sync.Once
)
type LogCallback interface {
OnLogEvent(level, payload string)
}
func InitCore(geoipDatabase[] byte, homeDir string, version string) {
dataClone := make([]byte, len(geoipDatabase))
copy(dataClone, geoipDatabase)
mmdb.LoadFromBytes(dataClone)
C.SetHomeDir(homeDir)
config.ApplicationVersion = version
Reset()
log.Infoln("Initialed")
}
func Reset() {
config.LoadDefault()
tunnel.DefaultManager.ResetStatistic()
}
func SetLogCallback(callback LogCallback) {
logSubscribe.Do(func() {
go func() {
sub := log.Subscribe()
defer log.UnSubscribe(sub)
for {
elm := <-sub
l := elm.(*log.Event)
if l.LogLevel < log.Level() {
continue
}
if cb := logCallback; cb != nil {
cb.OnLogEvent(l.LogLevel.String(), l.Payload)
}
}
}()
})
logCallback = callback
}

View File

@@ -1,49 +0,0 @@
package bridge
import (
"strings"
"github.com/kr328/cfa/config"
)
func ResetDnsAppend(dns string) {
if len(dns) == 0 {
config.NameServersAppend = make([]string, 0)
} else {
config.NameServersAppend = strings.Split(dns, ",")
}
}
func SetDnsOverrideEnabled(enabled bool) {
if enabled {
config.DnsPatch = config.OptionalDnsPatch
} else {
config.DnsPatch = nil
}
}
func LoadProfileFile(path, baseDir string, callback DoneCallback) {
go func() {
call(config.LoadFromFile(path, baseDir), callback)
}()
}
func DownloadProfileAndCheck(url, output, baseDir string, callback DoneCallback) {
go func() {
call(config.PullRemote(url, output, baseDir), callback)
}()
}
func ReadProfileAndCheck(fd int, output, baseDir string, callback DoneCallback) {
go func() {
call(config.PullLocal(fd, output, baseDir), callback)
}()
}
func call(err error, callback DoneCallback) {
if err != nil {
callback.DoneWithError(err)
} else {
callback.Done()
}
}

View File

@@ -1,129 +0,0 @@
package bridge
import (
"sync"
"github.com/Dreamacro/clash/adapters/outbound"
"github.com/Dreamacro/clash/adapters/outboundgroup"
"github.com/Dreamacro/clash/adapters/provider"
"github.com/Dreamacro/clash/log"
"github.com/Dreamacro/clash/tunnel"
)
type ProxyItem struct {
Name string
Type string
Delay int
}
type ProxyGroupItem struct {
Name string
Type string
Current string
Delay int
providers []provider.ProxyProvider
}
type ProxyGroupCollection interface {
Add(proxy *ProxyGroupItem) bool
}
type ProxyCollection interface {
Add(proxy *ProxyItem) bool
}
func (p *ProxyGroupItem) QueryAllProxies(collection ProxyCollection) {
for _, v := range p.providers {
for _, p := range v.Proxies() {
collection.Add(
&ProxyItem{
Name: p.Name(),
Type: p.Type().String(),
Delay: int(p.LastDelay()),
},
)
}
}
}
func StartUrlTest(group string, callback DoneCallback) {
go func() {
defer callback.Done()
p := tunnel.Proxies()[group]
pi, ok := p.(*outbound.Proxy)
if !ok {
return
}
group, ok := pi.ProxyAdapter.(outboundgroup.ProxyGroup)
if !ok {
return
}
providers := group.GetProxyProviders()
wg := &sync.WaitGroup{}
wg.Add(len(providers))
for _, v := range providers {
go func(p provider.ProxyProvider) {
p.HealthCheck()
wg.Done()
}(v)
}
wg.Wait()
}()
}
func QueryAllProxyGroups(collection ProxyGroupCollection) {
ps := tunnel.Proxies()
for _, p := range ps {
pi, ok := p.(*outbound.Proxy)
if !ok {
continue
}
group, ok := pi.ProxyAdapter.(outboundgroup.ProxyGroup)
if !ok {
continue
}
collection.Add(&ProxyGroupItem{
Name: group.Name(),
Type: group.Type().String(),
Current: group.Now(),
Delay: 0,
providers: group.GetProxyProviders(),
})
}
}
func SetSelectedProxy(name, proxy string) bool {
p := tunnel.Proxies()[name]
if p == nil {
return false
}
pb, ok := p.(*outbound.Proxy)
if !ok {
return false
}
selector, ok := pb.ProxyAdapter.(*outboundgroup.Selector)
if !ok {
return false
}
if err := selector.Set(proxy); err != nil {
return false
}
log.Infoln("Set " + name + " -> " + proxy)
return true
}

View File

@@ -1,47 +0,0 @@
package bridge
import (
"sync"
"github.com/Dreamacro/clash/tunnel"
)
type EventPoll struct {
stop sync.Once
onStop func()
}
func (e *EventPoll) Stop() {
e.stop.Do(func() {
e.onStop()
})
}
type Traffic struct {
Download int64
Upload int64
}
type Logs interface {
OnEvent(level, payload string)
}
func QueryBandwidth() *Traffic {
upload := tunnel.DefaultManager.UploadTotal()
download := tunnel.DefaultManager.DownloadTotal()
return &Traffic{
Upload: upload,
Download: download,
}
}
func QueryTraffic() *Traffic {
up, down := tunnel.DefaultManager.Now()
return &Traffic{
Upload: up,
Download: down,
}
}

View File

@@ -1,55 +0,0 @@
package bridge
import (
"net"
"syscall"
"github.com/Dreamacro/clash/component/dialer"
"github.com/kr328/cfa/tun"
)
type TunCallback interface {
OnCreateSocket(fd int)
OnStop()
}
var callback TunCallback
func init() {
dialer.DialerHook = onNewDialer
dialer.ListenConfigHook = onNewListenConfig
}
func onNewDialer(dialer *net.Dialer) {
dialer.Control = onNewSocket
}
func onNewListenConfig(listen *net.ListenConfig) {
listen.Control = onNewSocket
}
func onNewSocket(_, _ string, c syscall.RawConn) error {
if cb := callback; cb != nil {
_ = c.Control(func(fd uintptr) {
cb.OnCreateSocket(int(fd))
})
}
return nil
}
func StartTunDevice(fd, mtu int, gateway, mirror, dns string, cb TunCallback) error {
callback = cb
return tun.StartTunDevice(fd, mtu, gateway, mirror, dns)
}
func StopTunDevice() {
tun.StopTunDevice()
if c := callback; c != nil {
c.OnStop()
}
callback = nil
}

View File

@@ -0,0 +1,23 @@
#pragma once
#include <stdint.h>
#if __cplusplus
extern "C" {
#endif
typedef struct buffer_t {
void *buffer;
int length;
} buffer_t;
typedef struct const_buffer_t {
const void *buffer;
int length;
} const_buffer_t;
typedef const char *const_string_t;
#if __cplusplus
};
#endif

View File

@@ -0,0 +1,28 @@
package main
//#include "config.h"
//#include "buffer.h"
import "C"
import (
"github.com/kr328/cfa/config"
"strings"
)
//export setDnsOverride
func setDnsOverride(override *C.dns_override_t) {
overrideDns := override.override_dns != 0
appendDns := C.GoString(override.append_dns)
if overrideDns {
config.DnsPatch = config.OptionalDnsPatch
} else {
config.DnsPatch = nil
}
if len(appendDns) == 0 {
config.NameServersAppend = make([]string, 0)
} else {
config.NameServersAppend = strings.Split(appendDns, ",")
}
}

View File

@@ -0,0 +1,14 @@
#pragma once
#if __cplusplus
extern "C" {
#endif
typedef struct dns_override_t {
int override_dns;
const char *append_dns;
} dns_override_t;
#if __cplusplus
};
#endif

View File

@@ -0,0 +1,15 @@
package config
var defaultFakeIPFilter = []string{
// stun services
"+.stun.*.*",
"+.stun.*.*.*",
"+.stun.*.*.*.*",
// Google Voices
"lens.l.google.com",
"stun.l.google.com",
// Nintendo Switch
"*.n.n.srv.nintendo.net",
}

View File

@@ -7,6 +7,7 @@ import (
"io/ioutil"
"net"
"net/http"
"net/url"
"os"
"syscall"
@@ -29,20 +30,29 @@ var client = &http.Client{
client, server := net.Pipe()
tunnel.Add(inbound.NewSocket(socks5.ParseAddr(address), server, constant.HTTP, constant.TCP))
tunnel.Add(inbound.NewSocket(socks5.ParseAddr(address), server, constant.HTTP))
return client, nil
},
},
}
func fetchRemote(url string) ([]byte, error) {
request, err := http.NewRequest("GET", url, nil)
func fetchRemote(sUrl string) ([]byte, error) {
uri, err := url.Parse(sUrl)
if err != nil {
return nil, err
}
request, err := http.NewRequest("GET", uri.String(), nil)
if err != nil {
return nil, err
}
request.Header.Set("User-Agent", "ClashForAndroid/"+ApplicationVersion)
if user := uri.User; user != nil {
password, _ := user.Password()
request.SetBasicAuth(user.Username(), password)
}
response, err := client.Do(request)
if err != nil {
@@ -68,7 +78,7 @@ func fetchLocal(fd int) ([]byte, error) {
return ioutil.ReadAll(file)
}
func PullRemote(url, output, baseDir string) error {
func DownloadUrl(url, output, baseDir string) error {
data, err := fetchRemote(url)
if err != nil {
return err
@@ -77,7 +87,7 @@ func PullRemote(url, output, baseDir string) error {
return save(data, output, baseDir)
}
func PullLocal(fd int, output, baseDir string) error {
func DownloadFd(fd int, output, baseDir string) error {
data, err := fetchLocal(fd)
if err != nil {
return err
@@ -87,14 +97,10 @@ func PullLocal(fd int, output, baseDir string) error {
}
func save(data []byte, output, baseDir string) error {
cfg, err := parseConfig(data, baseDir)
_, err := parseConfig(data, baseDir)
if err != nil {
return err
}
for _, v := range cfg.Providers {
_ = v.Destroy()
}
return ioutil.WriteFile(output, data, defaultFileMode)
}

View File

@@ -35,7 +35,7 @@ func init() {
Listen: ":0",
EnhancedMode: dns.FAKEIP,
FakeIPRange: "198.18.0.0/16",
FakeIPFilter: []string{},
FakeIPFilter: defaultFakeIPFilter,
DefaultNameserver: defaultNameServers,
}
}

View File

@@ -0,0 +1,112 @@
package main
import (
"github.com/Dreamacro/clash/adapters/outbound"
"github.com/Dreamacro/clash/adapters/outboundgroup"
"github.com/Dreamacro/clash/adapters/provider"
"github.com/Dreamacro/clash/log"
"github.com/Dreamacro/clash/tunnel"
"github.com/kr328/cfa/config"
"sync"
)
//#include "buffer.h"
//#include "event.h"
import "C"
//export downloadProfileFromFd
func downloadProfileFromFd(fd int, base C.const_string_t, output C.const_string_t, callbackId uint64) {
b := C.GoString(base)
o := C.GoString(output)
go func() {
err := config.DownloadFd(fd, o, b)
if err != nil {
sendEvent(C.COMPLETE, callbackId, err.Error())
} else {
sendEvent(C.COMPLETE, callbackId, "")
}
}()
}
//export downloadProfileFromUrl
func downloadProfileFromUrl(url C.const_string_t, base C.const_string_t, output C.const_string_t, callbackId uint64) {
u := C.GoString(url)
b := C.GoString(base)
o := C.GoString(output)
go func() {
err := config.DownloadUrl(u, o, b)
if err != nil {
sendEvent(C.COMPLETE, callbackId, err.Error())
} else {
sendEvent(C.COMPLETE, callbackId, "")
}
}()
}
//export loadProfile
func loadProfile(path C.const_string_t, base C.const_string_t, callbackId uint64) {
p := C.GoString(path)
b := C.GoString(base)
go func() {
err := config.LoadFromFile(p, b)
if err != nil {
sendEvent(C.COMPLETE, callbackId, err.Error())
} else {
sendEvent(C.COMPLETE, callbackId, "")
}
}()
}
//export performHealthCheck
func performHealthCheck(group C.const_string_t, callbackId uint64) {
g := C.GoString(group)
go func() {
p := tunnel.Proxies()[g]
if p == nil {
sendEvent(C.COMPLETE, callbackId, "No such proxy group")
log.Warnln("Perform health check failure: %s not found", g)
return
}
pw, ok := p.(*outbound.Proxy)
if !ok {
sendEvent(C.COMPLETE, callbackId, "Invalid group")
log.Warnln("Perform health check failure: %s not valid group", g)
return
}
adapter, ok := pw.ProxyAdapter.(outboundgroup.ProxyGroup)
if !ok {
sendEvent(C.COMPLETE, callbackId, "Invalid group")
log.Warnln("Perform health check failure: %s not valid group", g)
return
}
providers := adapter.GetProxyProviders()
wg := &sync.WaitGroup{}
wg.Add(len(providers))
for _, p := range providers {
go func(p provider.ProxyProvider) {
p.HealthCheck()
wg.Done()
}(p)
}
wg.Wait()
sendEvent(C.COMPLETE, callbackId, "")
}()
}

View File

@@ -0,0 +1,29 @@
#include "event.h"
#include <malloc.h>
#include <string.h>
extern void answerEvent(int64_t id);
static event_handler_t event_handler;
void set_event_handler(event_handler_t handler) {
event_handler = handler;
}
void send_event(event_t *event, const void *payload, size_t payload_length) {
event_handler_t h = event_handler;
if ( h != NULL ) {
memcpy(event->payload, payload, payload_length);
h(event);
}
else {
answer_event(event);
}
}
void answer_event(const event_t *event) {
answerEvent(event->id);
free((void*)event);
}

View File

@@ -0,0 +1,56 @@
package main
//#include "event.h"
import "C"
import (
"sync"
"unsafe"
)
type EventWaiter struct {
result chan struct{}
}
var idLock = sync.Mutex{}
var currentId = int64(0)
var ids = map[int64]*EventWaiter{}
//export answerEvent
func answerEvent(id int64) {
idLock.Lock()
defer idLock.Unlock()
waiter, ok := ids[id]
if ok {
close(waiter.result)
delete(ids, id)
}
}
func sendEvent(t C.event_type_t, token uint64, payload string) *EventWaiter {
idLock.Lock()
defer idLock.Unlock()
currentId++
id := currentId
r := &EventWaiter{make(chan struct{})}
ids[id] = r
p := append([]byte(payload), 0)
e := allocCEvent(len(p))
e.id = C.int64_t(id)
e._type = t
e.token = C.int64_t(token)
C.send_event(e, unsafe.Pointer(&p[0]), C.size_t(len(p)))
return r
}
func allocCEvent(payloadLength int) *C.event_t {
return (*C.event_t)(C.malloc(C.sizeof_event_t + C.size_t(payloadLength)))
}

View File

@@ -0,0 +1,29 @@
#pragma once
#include <stdint.h>
#include <stddef.h>
#if __cplusplus
extern "C" {
#endif
typedef enum {
NEW_SOCKET, TUN_STOP, COMPLETE, LOG_RECEIVED
} event_type_t;
typedef struct event_t {
int64_t id;
int64_t token;
event_type_t type;
char payload[];
} event_t;
typedef void (*event_handler_t)(const event_t *event);
void set_event_handler(event_handler_t handler);
void send_event(event_t *event, const void *payload, size_t payload_length);
void answer_event(const event_t *event);
#if __cplusplus
};
#endif

View File

@@ -0,0 +1,42 @@
package main
//#include "buffer.h"
//#include "general.h"
import "C"
import (
"github.com/Dreamacro/clash/proxy"
"github.com/Dreamacro/clash/tunnel"
)
//export setProxyMode
func setProxyMode(mode C.int) {
switch mode {
case C.MODE_DIRECT:
tunnel.SetMode(tunnel.Direct)
case C.MODE_GLOBAL:
tunnel.SetMode(tunnel.Global)
case C.MODE_RULE:
tunnel.SetMode(tunnel.Rule)
}
}
//export queryGeneral
func queryGeneral(general *C.general_t) {
m := tunnel.Mode()
ports := proxy.GetPorts()
switch m {
case tunnel.Direct:
general.mode = C.MODE_DIRECT
case tunnel.Global:
general.mode = C.MODE_GLOBAL
case tunnel.Rule:
general.mode = C.MODE_RULE
}
general.http_port = C.int(ports.Port)
general.socks_port = C.int(ports.SocksPort)
general.mixed_port = C.int(ports.MixedPort)
general.redirect_port = C.int(ports.RedirPort)
}

View File

@@ -0,0 +1,23 @@
#pragma once
#if __cplusplus
extern "C" {
#endif
static const int MODE_UNKNOWN = -1;
static const int MODE_DIRECT = 0;
static const int MODE_GLOBAL = 1;
static const int MODE_RULE = 2;
static const int MODE_SCRIPT = 3;
typedef struct general_t {
int mode;
int http_port;
int socks_port;
int redirect_port;
int mixed_port;
} general_t;
#if __cplusplus
};
#endif

View File

@@ -1,10 +1,10 @@
module github.com/kr328/cfa
go 1.13
go 1.14
require (
github.com/Dreamacro/clash v0.0.0 // local
github.com/kr328/tun2socket v0.0.0-20200415021819-256b721ac9a4
github.com/kr328/tun2socket v0.0.0-20200613032901-7ffeefc227e3
github.com/miekg/dns v1.1.29
)

View File

@@ -2,32 +2,31 @@ github.com/Dreamacro/go-shadowsocks2 v0.1.5/go.mod h1:LSXCjyHesPY3pLjhwff1mQX72I
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/eapache/queue v1.1.0/go.mod h1:6eCeP0CKFpHLu8blIFXhExK/dRa7WDZfr6jVFPTqq+I=
github.com/go-chi/chi v4.0.3+incompatible/go.mod h1:eB3wogJHnLi3x/kFX2A+IbTBlXxmMeXJVKy9tTv1XzQ=
github.com/go-chi/cors v1.0.1/go.mod h1:K2Yje0VW/SJzxiyMYu6iPQYa7hMjQX2i/F491VChg1I=
github.com/go-chi/chi v4.1.2+incompatible/go.mod h1:eB3wogJHnLi3x/kFX2A+IbTBlXxmMeXJVKy9tTv1XzQ=
github.com/go-chi/cors v1.1.1/go.mod h1:K2Yje0VW/SJzxiyMYu6iPQYa7hMjQX2i/F491VChg1I=
github.com/go-chi/render v1.0.1/go.mod h1:pq4Rr7HbnsdaeHagklXub+p6Wd16Af5l9koip1OvJns=
github.com/gofrs/uuid v3.2.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
github.com/gofrs/uuid v3.3.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/kr328/tun2socket v0.0.0-20200415021819-256b721ac9a4/go.mod h1:FWfSixjrLgtK+dHkDoN6lHMNhvER24gnjUZd/wt8Z9o=
github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/kr328/tun2socket v0.0.0-20200613032901-7ffeefc227e3/go.mod h1:FWfSixjrLgtK+dHkDoN6lHMNhvER24gnjUZd/wt8Z9o=
github.com/miekg/dns v1.1.29/go.mod h1:KNUDUusw/aVsxyTYZM1oqvCicbwhgbNgztCETuNZ7xM=
github.com/oschwald/geoip2-golang v1.4.0/go.mod h1:8QwxJvRImBH+Zl6Aa6MaIcs5YdlZSTKtzmPGzQqi9ng=
github.com/oschwald/maxminddb-golang v1.6.0/go.mod h1:DUJFucBg2cvqx42YmDa/+xHvb0elJtOm3o4aFQ/nb/w=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20191206172530-e9b2fee46413/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20200320181102-891825fb96df/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20200604202706-70a84ac30bf9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190923162816-aa69164e4478/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200320220750-118fecf932d8/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200602114024-627f9648deb9/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
@@ -35,10 +34,12 @@ golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190924154521-2837fb4f24fe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191224085550-c709ea063b76/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/tools v0.0.0-20191216052735-49a3e744a425/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/eapache/channels.v1 v1.1.0/go.mod h1:BHIBujSvu9yMTrTYbTCjDD43gUhtmaOtTWDe7sTv1js=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

View File

@@ -0,0 +1,58 @@
package main
//#include "event.h"
import "C"
import (
"fmt"
"github.com/Dreamacro/clash/log"
"sync"
)
var logLocker sync.Mutex
var closeChan chan struct{}
//export enableLogReport
func enableLogReport() {
logLocker.Lock()
defer logLocker.Unlock()
if closeChan != nil {
close(closeChan)
}
closeChan = make(chan struct{})
go func(closed chan struct{}) {
subscriber := log.Subscribe()
defer log.UnSubscribe(subscriber)
defer log.Infoln("Log broadcast disabled")
for {
select {
case item := <-subscriber:
msg := item.(*log.Event)
if msg.LogLevel < log.Level() {
continue
}
<- sendEvent(C.LOG_RECEIVED, 0, fmt.Sprintf("%s:%s", msg.LogLevel.String(), msg.Payload)).result
case <-closeChan:
return
}
}
}(closeChan)
}
//export disableLogReport
func disableLogReport() {
logLocker.Lock()
defer logLocker.Unlock()
if closeChan != nil {
close(closeChan)
closeChan = nil
}
}

View File

@@ -0,0 +1,24 @@
#include <android/log.h>
#define TAG "ClashForAndroid"
void log_info(const char *msg) {
__android_log_write(ANDROID_LOG_INFO, TAG, msg);
}
void log_error(const char *msg) {
__android_log_write(ANDROID_LOG_ERROR, TAG, msg);
}
void log_warn(const char *msg) {
__android_log_write(ANDROID_LOG_WARN, TAG, msg);
}
void log_debug(const char *msg) {
__android_log_write(ANDROID_LOG_DEBUG, TAG, msg);
}
void log_verbose(const char *msg) {
__android_log_write(ANDROID_LOG_VERBOSE, TAG, msg);
}

View File

@@ -1,3 +1,83 @@
package main
func main() {}
import (
"github.com/Dreamacro/clash/component/mmdb"
"github.com/Dreamacro/clash/constant"
"github.com/Dreamacro/clash/log"
"github.com/Dreamacro/clash/tunnel"
"github.com/kr328/cfa/config"
"unsafe"
)
/*
#cgo CFLAGS: -O3
#cgo LDFLAGS: -llog
#include "buffer.h"
#include "malloc.h"
extern void log_info(const char *msg);
extern void log_error(const char *msg);
extern void log_warn(const char *msg);
extern void log_debug(const char *msg);
extern void log_verbose(const char *msg);
*/
import "C"
func main() {
panic("Only for linking")
}
func init() {
r := make(chan struct{})
go func() {
sub := log.Subscribe()
defer log.UnSubscribe(sub)
close(r)
for item := range sub {
msg := item.(*log.Event)
if msg.LogLevel < log.Level() {
continue
}
cPayload := C.CString(msg.Payload)
switch msg.LogLevel {
case log.INFO:
C.log_info(cPayload)
case log.ERROR:
C.log_error(cPayload)
case log.WARNING:
C.log_warn(cPayload)
case log.DEBUG:
C.log_debug(cPayload)
case log.SILENT:
C.log_verbose(cPayload)
}
C.free(unsafe.Pointer(cPayload))
}
}()
<- r
}
//export initialize
func initialize(database *C.const_buffer_t, home, version C.const_string_t) {
databaseData := C.GoBytes(database.buffer, database.length)
homeData := C.GoString(home)
versionData := C.GoString(version)
mmdb.LoadFromBytes(databaseData)
constant.SetHomeDir(homeData)
config.ApplicationVersion = versionData
}
//export reset
func reset() {
config.LoadDefault()
tunnel.DefaultManager.ResetStatistic()
}

View File

@@ -0,0 +1,198 @@
package main
import (
"github.com/Dreamacro/clash/adapters/outbound"
"github.com/Dreamacro/clash/adapters/outboundgroup"
"github.com/Dreamacro/clash/constant"
"github.com/Dreamacro/clash/log"
"github.com/Dreamacro/clash/tunnel"
"unsafe"
)
/*
#cgo CFLAGS: -Werror
#include "buffer.h"
#include "proxies.h"
*/
import "C"
//export setSelector
func setSelector(group C.const_string_t, selected C.const_string_t) C.int {
g := C.GoString(group)
s := C.GoString(selected)
p := tunnel.Proxies()[g]
if p == nil {
log.Warnln("Set selector failure: %s not found", g)
return -1
}
pw, ok := p.(*outbound.Proxy)
if !ok {
log.Warnln("Set selector failure: %s not valid group", g)
return -1
}
adapter, ok := pw.ProxyAdapter.(outboundgroup.ProxyGroup)
if !ok {
log.Warnln("Set selector failure: %s not valid group", g)
return -1
}
selector, ok := adapter.(*outboundgroup.Selector)
if !ok {
log.Warnln("Set selector failure: %s not selector", g)
return -1
}
if err := selector.Set(s); err != nil {
log.Warnln("Set selector failure: %s not in %s", s, g)
return -1
}
log.Infoln("Set %s -> %s", g, s)
return 0
}
//export queryProxyGroups
func queryProxyGroups() *C.proxy_group_list_t {
stringPool := make([]byte, 0, 4096)
proxies := tunnel.Proxies()
groups := make([]outboundgroup.ProxyGroup, 0, len(proxies))
for _, p := range proxies {
pw, ok := p.(*outbound.Proxy)
if !ok {
continue
}
adapter, ok := pw.ProxyAdapter.(outboundgroup.ProxyGroup)
if !ok {
continue
}
groups = append(groups, adapter)
}
result := allocCProxyGroupList(len(groups))
groupIndex := 0
for _, group := range groups {
ps := make([]constant.Proxy, 0)
for _, provider := range group.GetProxyProviders() {
ps = append(ps, provider.Proxies()...)
}
g := allocCProxyGroup(len(ps))
g.base.name_index = C.long(len(stringPool))
g.base.proxy_type = typeToProxyTypeC(group.Type())
g.base.delay = 0
stringPool = append(stringPool, group.Name()...)
stringPool = append(stringPool, 0)
proxyIndex := 0
for _, proxy := range ps {
p := indexCProxyGroupElement(g, proxyIndex)
p.name_index = C.long(len(stringPool))
p.proxy_type = typeToProxyTypeC(proxy.Type())
p.delay = C.long(proxy.LastDelay())
stringPool = append(stringPool, proxy.Name()...)
stringPool = append(stringPool, 0)
if proxy.Name() == group.Now() {
g.now = C.int(proxyIndex)
}
proxyIndex++
}
setCProxyGroupListElement(result, groupIndex, g)
groupIndex++
}
result.string_pool = (*C.char)(C.CBytes(stringPool))
return result
}
func typeToProxyTypeC(t constant.AdapterType) C.proxy_type_t {
switch t {
case constant.Direct:
return C.Direct
case constant.Reject:
return C.Reject
case constant.Shadowsocks:
return C.Shadowsocks
case constant.Snell:
return C.Snell
case constant.Socks5:
return C.Socks5
case constant.Http:
return C.Http
case constant.Vmess:
return C.Vmess
case constant.Trojan:
return C.Trojan
case constant.Relay:
return C.Relay
case constant.Selector:
return C.Selector
case constant.Fallback:
return C.Fallback
case constant.URLTest:
return C.URLTest
case constant.LoadBalance:
return C.LoadBalance
default:
return C.Unknown
}
}
func allocCProxyGroup(proxiesSize int) *C.proxy_group_t {
result := (*C.proxy_group_t)(C.malloc(C.sizeof_proxy_group_t + C.sizeof_proxy_t * C.size_t(proxiesSize)))
result.proxies_size = C.int(proxiesSize)
return result
}
func allocCProxyGroupList(groupSize int) *C.proxy_group_list_t {
result := (*C.proxy_group_list_t)(C.malloc(C.sizeof_proxy_group_list_t + C.sizeof_long * C.size_t(groupSize)))
result.size = C.int(groupSize)
return result
}
//noinspection GoVetUnsafePointer
func setCProxyGroupListElement(list *C.proxy_group_list_t, index int, element *C.proxy_group_t) {
address := uintptr(unsafe.Pointer(list))
offset := address + uintptr(C.sizeof_proxy_group_list_t) + uintptr(index) * uintptr(C.sizeof_long)
*(**C.proxy_group_t)(unsafe.Pointer(offset)) = element
}
//noinspection GoVetUnsafePointer
func indexCProxyGroupElement(group *C.proxy_group_t, index int) *C.proxy_t {
address := uintptr(unsafe.Pointer(group))
offset := address + uintptr(C.sizeof_proxy_group_t) + uintptr(index) * uintptr(C.sizeof_proxy_t)
return (*C.proxy_t)(unsafe.Pointer(offset))
}

View File

@@ -0,0 +1,33 @@
#pragma once
#if __cplusplus
extern "C" {
#endif
typedef enum proxy_type_t {
Direct, Reject, Socks5, Http, Shadowsocks, Vmess, Snell, Trojan, Selector, Fallback, LoadBalance, URLTest, Relay, Unknown
} proxy_type_t;
typedef struct proxy_t {
long name_index;
proxy_type_t proxy_type;
long delay;
} proxy_t;
typedef struct proxy_group_t {
proxy_t base;
int now;
int proxies_size;
proxy_t proxies[];
} proxy_group_t;
typedef struct proxy_group_list_t {
int size;
char *string_pool;
proxy_group_t *groups[];
} proxy_group_list_t;
#if __cplusplus
};
#endif

View File

@@ -0,0 +1,24 @@
package main
/*
#include "traffic.h"
*/
import "C"
import "github.com/Dreamacro/clash/tunnel"
//export querySpeed
func querySpeed(r *C.traffic_t) {
u, d := tunnel.DefaultManager.Now()
r.upload = C.int64_t(u)
r.download = C.int64_t(d)
}
//export queryBandwidth
func queryBandwidth(r *C.traffic_t) {
u := tunnel.DefaultManager.UploadTotal()
d := tunnel.DefaultManager.DownloadTotal()
r.upload = C.int64_t(u)
r.download = C.int64_t(d)
}

View File

@@ -0,0 +1,16 @@
#pragma once
#include <stdint.h>
#if __cplusplus
extern "C" {
#endif
typedef struct traffic_t {
int64_t upload;
int64_t download;
} traffic_t;
#if __cplusplus
};
#endif

View File

@@ -0,0 +1,62 @@
package main
import (
"github.com/Dreamacro/clash/component/dialer"
"github.com/kr328/cfa/tun"
"net"
"strconv"
"sync"
"syscall"
)
//#include "buffer.h"
//#include "event.h"
import "C"
var tunLock sync.Mutex
func init() {
c := func(_, _ string, conn syscall.RawConn) error {
return conn.Control(func(fd uintptr) {
<- sendEvent(C.NEW_SOCKET, 0, strconv.Itoa(int(fd))).result
})
}
dialer.DialerHook = func(d *net.Dialer) error {
d.Control = c
return nil
}
dialer.ListenConfigHook = func(l *net.ListenConfig) error {
l.Control = c
return nil
}
}
//export startTunDevice
func startTunDevice(fd, mtu int, gateway, mirror, dns C.const_string_t, callbackId uint64) *C.char {
stopTunDevice()
tunLock.Lock()
defer tunLock.Unlock()
g := C.GoString(gateway)
m := C.GoString(mirror)
d := C.GoString(dns)
err := tun.StartTunDevice(fd, mtu, g, m, d, func() {
sendEvent(C.TUN_STOP, callbackId, "")
})
if err != nil {
return C.CString(err.Error())
}
return nil
}
//export stopTunDevice
func stopTunDevice() {
tunLock.Lock()
defer tunLock.Unlock()
tun.StopTunDevice()
}

View File

@@ -56,7 +56,7 @@ func hijackTCPDNS(conn net.Conn, endpoint *binding.Endpoint) bool {
return false
}
if !hijackAddress.Equal(net.IPv4zero) && !hijackAddress.Equal(net.IPv6zero) && !hijackAddress.Equal(endpoint.Target.IP) {
if !hijackAddress.Equal(net.IPv4zero) && !hijackAddress.Equal(endpoint.Target.IP) {
return false
}
@@ -109,7 +109,7 @@ func hijackDNS(payload []byte, endpoint *binding.Endpoint, sender redirect.UDPSe
return false
}
if !hijackAddress.Equal(net.IPv4zero) && !hijackAddress.Equal(net.IPv6zero) && !hijackAddress.Equal(endpoint.Target.IP) {
if !hijackAddress.Equal(net.IPv4zero) && !hijackAddress.Equal(endpoint.Target.IP) {
return false
}

View File

@@ -2,21 +2,20 @@ package tun
import "github.com/Dreamacro/clash/log"
type ClashLogger struct {}
type ClashLogger struct{}
func (c *ClashLogger) D(format string, args ...interface{}) {
log.Debugln(format, args)
log.Debugln(format, args...)
}
func (c *ClashLogger) I(format string, args ...interface{}) {
log.Infoln(format, args)
log.Infoln(format, args...)
}
func (c *ClashLogger) W(format string, args ...interface{}) {
log.Warnln(format, args)
log.Warnln(format, args...)
}
func (c *ClashLogger) E(format string, args ...interface{}) {
log.Errorln(format, args)
log.Errorln(format, args...)
}

View File

@@ -24,7 +24,7 @@ const (
var adapter *tun2socket.Tun2Socket
var mutex sync.Mutex
func StartTunDevice(fd, mtu int, gateway, mirror, dnsAddress string) error {
func StartTunDevice(fd, mtu int, gateway, mirror, dnsAddress string, onStop func()) error {
mutex.Lock()
defer mutex.Unlock()
@@ -36,6 +36,7 @@ func StartTunDevice(fd, mtu int, gateway, mirror, dnsAddress string) error {
}
gatewayIP, gatewayNet, err := net.ParseCIDR(gateway)
_, ipv4Loopback, _ := net.ParseCIDR("127.0.0.0/8")
mirrorIP := net.ParseIP(mirror)
if err != nil || mirrorIP == nil || !gatewayNet.Contains(mirrorIP) {
@@ -59,6 +60,8 @@ func StartTunDevice(fd, mtu int, gateway, mirror, dnsAddress string) error {
adapter.SetLogger(&ClashLogger{})
adapter.SetClosedHandler(func() {
StopTunDevice()
onStop()
})
adapter.SetAllocator(func(length int) []byte {
if length <= maxUdpPacketSize {
@@ -67,7 +70,8 @@ func StartTunDevice(fd, mtu int, gateway, mirror, dnsAddress string) error {
return make([]byte, length)
})
adapter.SetTCPHandler(func(conn net.Conn, endpoint *binding.Endpoint) {
if gatewayNet.Contains(endpoint.Target.IP) {
if gatewayNet.Contains(endpoint.Target.IP) || ipv4Loopback.Contains(endpoint.Target.IP) {
_ = conn.Close()
return
}
@@ -81,10 +85,10 @@ func StartTunDevice(fd, mtu int, gateway, mirror, dnsAddress string) error {
Zone: "",
})
tunnel.Add(adapters.NewSocket(addr, conn, C.SOCKS, C.TCP))
tunnel.Add(adapters.NewSocket(addr, conn, C.SOCKS))
})
adapter.SetUDPHandler(func(payload []byte, endpoint *binding.Endpoint, sender redirect.UDPSender) {
if gatewayNet.Contains(endpoint.Target.IP) {
if gatewayNet.Contains(endpoint.Target.IP) || ipv4Loopback.Contains(endpoint.Target.IP) {
udpRecycle(payload)
return
}
@@ -101,7 +105,8 @@ func StartTunDevice(fd, mtu int, gateway, mirror, dnsAddress string) error {
pkt := &udpPacket{
payload: payload,
endpoint: endpoint,
sender: sender,
send: sender,
recycle: udpRecycle,
}
tunnel.AddPacket(adapters.NewPacket(addr, pkt, C.SOCKS))

View File

@@ -10,7 +10,8 @@ import (
type udpPacket struct {
payload []byte
endpoint *binding.Endpoint
sender redirect.UDPSender
send redirect.UDPSender
recycle func([]byte)
}
func (conn *udpPacket) Data() []byte {
@@ -39,11 +40,7 @@ func (conn *udpPacket) WriteBack(b []byte, addr net.Addr) (n int, err error) {
Target: conn.endpoint.Source,
}
return len(b), conn.sender(b, ep)
}
func (conn *udpPacket) Close() error {
return nil
return len(b), conn.send(b, ep)
}
func (conn *udpPacket) LocalAddr() net.Addr {
@@ -53,3 +50,7 @@ func (conn *udpPacket) LocalAddr() net.Addr {
Zone: "",
}
}
func (conn *udpPacket) Drop() {
conn.recycle(conn.payload)
}

View File

@@ -5,3 +5,4 @@ import "io"
func CloseSilent(closer io.Closer) {
_ = closer.Close()
}

View File

@@ -1,19 +1,17 @@
package com.github.kr328.clash.core
import bridge.Bridge
import bridge.TunCallback
import android.os.ParcelFileDescriptor
import com.github.kr328.clash.common.Global
import com.github.kr328.clash.common.utils.Log
import com.github.kr328.clash.core.bridge.Bridge
import com.github.kr328.clash.core.bridge.TunCallback
import com.github.kr328.clash.core.event.LogEvent
import com.github.kr328.clash.core.model.General
import com.github.kr328.clash.core.model.Proxy
import com.github.kr328.clash.core.model.ProxyGroup
import com.github.kr328.clash.core.model.Traffic
import com.github.kr328.clash.core.transact.DoneCallbackImpl
import com.github.kr328.clash.core.transact.ProxyCollectionImpl
import com.github.kr328.clash.core.transact.ProxyGroupCollectionImpl
import kotlinx.coroutines.CompletableDeferred
import java.io.File
import java.io.InputStream
import java.util.concurrent.CompletableFuture
object Clash {
private val logReceivers = mutableMapOf<String, (LogEvent) -> Unit>()
@@ -24,8 +22,16 @@ object Clash {
val bytes = context.assets.open("Country.mmdb")
.use(InputStream::readBytes)
Bridge.initCore(bytes, context.cacheDir.absolutePath, BuildConfig.VERSION_NAME)
Bridge.initialize(bytes, context.cacheDir.absolutePath, BuildConfig.VERSION_NAME)
Bridge.reset()
Bridge.setLogCallback {
synchronized(logReceivers) {
logReceivers.forEach { (_, e) -> e(it) }
}
}
Log.i("Clash core initialized")
}
fun start() {
@@ -45,11 +51,10 @@ object Clash {
onNewSocket: (Int) -> Boolean,
onTunStop: () -> Unit
) {
Bridge.startTunDevice(fd.toLong(), mtu.toLong(), gateway, mirror, dns, object: TunCallback {
override fun onCreateSocket(fd: Long) {
onNewSocket(fd.toInt())
Bridge.startTunDevice(fd, mtu, gateway, mirror, dns, object: TunCallback {
override fun onNewSocket(socket: Int) {
onNewSocket(socket)
}
override fun onStop() {
onTunStop()
}
@@ -60,56 +65,32 @@ object Clash {
Bridge.stopTunDevice()
}
fun appendDns(dns: List<String>) {
Bridge.resetDnsAppend(dns.joinToString(","))
fun setDnsOverride(dnsOverride: Boolean, appendNameservers: List<String>) {
Bridge.setDnsOverride(dnsOverride, appendNameservers.joinToString(","))
}
fun setDnsOverrideEnabled(enabled: Boolean) {
Bridge.setDnsOverrideEnabled(enabled)
fun loadProfile(path: File, baseDir: File): CompletableFuture<Unit> {
return Bridge.loadProfile(path.absolutePath, baseDir.absolutePath).thenApply { Unit }
}
fun loadProfile(path: File, baseDir: File): CompletableDeferred<Unit> {
return DoneCallbackImpl().apply {
Bridge.loadProfileFile(path.absolutePath, baseDir.absolutePath, this)
}
fun downloadProfile(url: String, output: File, baseDir: File): CompletableFuture<Unit> {
return Bridge.downloadProfile(url, baseDir.absolutePath, output.absolutePath).thenApply { Unit }
}
fun downloadProfile(url: String, output: File, baseDir: File): CompletableDeferred<Unit> {
return DoneCallbackImpl().apply {
Bridge.downloadProfileAndCheck(url, output.absolutePath, baseDir.absolutePath, this)
}
}
fun downloadProfile(fd: Int, output: File, baseDir: File): CompletableDeferred<Unit> {
return DoneCallbackImpl().apply {
Bridge.readProfileAndCheck(fd.toLong(), output.absolutePath, baseDir.absolutePath, this)
}
fun downloadProfile(fd: ParcelFileDescriptor, output: File, baseDir: File): CompletableFuture<Unit> {
return Bridge.downloadProfile(fd.detachFd(), baseDir.absolutePath, output.absolutePath).thenApply { Unit }
}
fun queryProxyGroups(): List<ProxyGroup> {
return ProxyGroupCollectionImpl().also { Bridge.queryAllProxyGroups(it) }
.filterNotNull()
.map { group ->
ProxyGroup(group.name,
Proxy.Type.fromString(group.type),
group.delay,
group.current,
ProxyCollectionImpl().also { pc ->
group.queryAllProxies(pc)
}.filterNotNull().map {
Proxy(it.name, Proxy.Type.fromString(it.type), it.delay)
})
}
return Bridge.queryProxyGroups().toList()
}
fun setSelectedProxy(name: String, selected: String): Boolean {
return Bridge.setSelectedProxy(name, selected)
fun setSelector(name: String, selected: String): Boolean {
return Bridge.setSelector(name, selected)
}
fun startHealthCheck(name: String): CompletableDeferred<Unit> {
return DoneCallbackImpl().apply {
Bridge.startUrlTest(name, this)
}
fun performHealthCheck(group: String): CompletableFuture<Unit> {
return Bridge.performHealthCheck(group).thenApply { Unit }
}
fun setProxyMode(mode: String) {
@@ -117,48 +98,30 @@ object Clash {
}
fun queryGeneral(): General {
val t = Bridge.queryGeneral()
return General(
General.Mode.fromString(t.mode),
t.httpPort.toInt(), t.socksPort.toInt(), t.redirectPort.toInt()
)
return Bridge.queryGeneral()
}
fun queryTraffic(): Traffic {
val data = Bridge.queryTraffic()
return Traffic(data.upload, data.download)
fun querySpeed(): Traffic {
return Bridge.querySpeed()
}
fun queryBandwidth(): Traffic {
val data = Bridge.queryBandwidth()
return Traffic(data.upload, data.download)
return Bridge.queryBandwidth()
}
fun registerLogReceiver(key: String, receiver: (LogEvent) -> Unit) {
synchronized(logReceivers) {
if ( logReceivers.isEmpty() )
Bridge.enableLogReport()
logReceivers[key] = receiver
Bridge.setLogCallback(this::onLogEvent)
}
}
fun unregisterLogReceiver(key: String) {
synchronized(logReceivers) {
logReceivers.remove(key)
if (logReceivers.isEmpty())
Bridge.setLogCallback(null)
}
}
private fun onLogEvent(level: String, payload: String) {
synchronized(logReceivers) {
logReceivers.forEach {
it.value(LogEvent(LogEvent.Level.fromString(level), payload))
}
if ( logReceivers.isEmpty() )
Bridge.disableLogReport();
}
}
}

View File

@@ -0,0 +1,44 @@
package com.github.kr328.clash.core.bridge;
import androidx.annotation.Keep;
import com.github.kr328.clash.core.model.General;
import com.github.kr328.clash.core.model.ProxyGroup;
import com.github.kr328.clash.core.model.Traffic;
import java.util.concurrent.CompletableFuture;
@Keep
public final class Bridge {
static {
System.loadLibrary("bridge");
}
private Bridge() {
}
public static native void initialize(byte[] database, String home, String version);
public static native void reset();
public static native General queryGeneral();
public static native Traffic querySpeed();
public static native Traffic queryBandwidth();
public static native ProxyGroup[] queryProxyGroups();
public static native void startTunDevice(int fd, int mtu, String gateway, String mirror, String dns, TunCallback callback) throws ClashException;
public static native void stopTunDevice();
public static native void setDnsOverride(boolean overrideDns, String appendNameservers);
public static native void setProxyMode(String mode);
public static native boolean setSelector(String group, String selected);
public static native CompletableFuture<Object> downloadProfile(String url, String base, String output);
public static native CompletableFuture<Object> downloadProfile(int fd, String base, String output);
public static native CompletableFuture<Object> loadProfile(String path, String base);
public static native CompletableFuture<Object> performHealthCheck(String group);
public static native void setLogCallback(LogCallback callback);
public static native void enableLogReport();
public static native void disableLogReport();
}

View File

@@ -0,0 +1,10 @@
package com.github.kr328.clash.core.bridge;
import androidx.annotation.Keep;
public class ClashException extends Exception {
@Keep
public ClashException(String msg) {
super(msg);
}
}

View File

@@ -0,0 +1,11 @@
package com.github.kr328.clash.core.bridge;
import androidx.annotation.Keep;
import com.github.kr328.clash.core.event.LogEvent;
@Keep
@SuppressWarnings("unused")
public interface LogCallback {
void onMessage(LogEvent event);
}

View File

@@ -0,0 +1,10 @@
package com.github.kr328.clash.core.bridge;
import androidx.annotation.Keep;
@Keep
@SuppressWarnings("unused")
public interface TunCallback {
void onNewSocket(int socket);
void onStop();
}

View File

@@ -2,6 +2,7 @@ package com.github.kr328.clash.core.event
import android.os.Parcel
import android.os.Parcelable
import androidx.annotation.Keep
import com.github.kr328.clash.common.serialization.Parcels
import kotlinx.serialization.Serializable
@@ -12,6 +13,10 @@ data class LogEvent(
val message: String,
val time: Long = System.currentTimeMillis()
) : Parcelable {
private constructor(data: List<String>): this(Level.fromString(data[0]), data[1])
@Keep
constructor(data: String) : this(data.split(":", limit = 2))
companion object {
const val DEBUG_VALUE = "debug"
const val INFO_VALUE = "info"

View File

@@ -2,11 +2,16 @@ package com.github.kr328.clash.core.model
import android.os.Parcel
import android.os.Parcelable
import androidx.annotation.Keep
import com.github.kr328.clash.common.serialization.Parcels
import kotlinx.serialization.*
import kotlinx.serialization.Serializable
@Serializable
data class General(val mode: Mode, val http: Int, val socks: Int, val redirect: Int) : Parcelable {
data class General(val mode: Mode, val http: Int, val socks: Int, val redirect: Int, val mixed: Int) : Parcelable {
@Keep
constructor(mode: String, http: Int, socks: Int, redirect: Int, mixed: Int) :
this(Mode.fromString(mode), http, socks, redirect, mixed)
@Serializable
enum class Mode(val string: String) {
DIRECT("Direct"), GLOBAL("Global"), RULE("Rule");

View File

@@ -1,13 +1,17 @@
package com.github.kr328.clash.core.model
import androidx.annotation.Keep
import kotlinx.serialization.Serializable
@Serializable
data class Proxy(
data class Proxy constructor(
val name: String,
val type: Type,
val delay: Long
) {
@Keep
constructor(name: String, type: String, delay: Long): this(name, Type.fromString(type), delay)
enum class Type(val text: String, val group: Boolean) {
DIRECT("Direct", false),
REJECT("Reject", false),

View File

@@ -1,12 +1,16 @@
package com.github.kr328.clash.core.model
import androidx.annotation.Keep
import kotlinx.serialization.Serializable
@Serializable
data class ProxyGroup(
val name: String,
val type: Proxy.Type,
val delay: Long,
val current: String,
val proxies: List<Proxy>
)
) {
@Keep
constructor(name: String, type: String, current: String, proxies: Array<Proxy>) :
this(name, Proxy.Type.fromString(type), current, proxies.toList())
}

View File

@@ -6,7 +6,7 @@ import com.github.kr328.clash.common.serialization.MergedParcels
import kotlinx.serialization.Serializable
@Serializable
data class ProxyGroupList(val list: List<ProxyGroup>) : Parcelable {
data class ProxyGroupWrapper(val list: List<ProxyGroup>) : Parcelable {
override fun writeToParcel(parcel: Parcel, flags: Int) {
MergedParcels.dump(serializer(), this, parcel)
}
@@ -15,12 +15,12 @@ data class ProxyGroupList(val list: List<ProxyGroup>) : Parcelable {
return 0
}
companion object CREATOR : Parcelable.Creator<ProxyGroupList> {
override fun createFromParcel(parcel: Parcel): ProxyGroupList {
companion object CREATOR : Parcelable.Creator<ProxyGroupWrapper> {
override fun createFromParcel(parcel: Parcel): ProxyGroupWrapper {
return MergedParcels.load(serializer(), parcel)
}
override fun newArray(size: Int): Array<ProxyGroupList?> {
override fun newArray(size: Int): Array<ProxyGroupWrapper?> {
return arrayOfNulls(size)
}
}

View File

@@ -1,3 +1,5 @@
package com.github.kr328.clash.core.model
data class Traffic(val upload: Long, val download: Long)
import androidx.annotation.Keep
data class Traffic @Keep constructor(val upload: Long, val download: Long)

View File

@@ -1,14 +0,0 @@
package com.github.kr328.clash.core.transact
import bridge.DoneCallback
import kotlinx.coroutines.CompletableDeferred
class DoneCallbackImpl : DoneCallback, CompletableDeferred<Unit> by CompletableDeferred() {
override fun doneWithError(e: Exception?) {
completeExceptionally(e ?: return done())
}
override fun done() {
complete(Unit)
}
}

View File

@@ -1,10 +0,0 @@
package com.github.kr328.clash.core.transact
import bridge.ProxyCollection
import bridge.ProxyGroupCollection
import bridge.ProxyGroupItem
import bridge.ProxyItem
import java.util.*
class ProxyCollectionImpl : LinkedList<ProxyItem?>(), ProxyCollection
class ProxyGroupCollectionImpl : LinkedList<ProxyGroupItem?>(), ProxyGroupCollection

View File

@@ -4,21 +4,19 @@ plugins {
id("kotlin-android-extensions")
}
val rootExtra = rootProject.extra
val gCompileSdkVersion: String by project
val gBuildToolsVersion: String by project
val gCompileSdkVersion: Int by rootExtra
val gBuildToolsVersion: String by rootExtra
val gMinSdkVersion: String by project
val gTargetSdkVersion: String by project
val gMinSdkVersion: Int by rootExtra
val gTargetSdkVersion: Int by rootExtra
val gVersionCode: String by project
val gVersionName: String by project
val gVersionCode: Int by rootExtra
val gVersionName: String by rootExtra
val gKotlinVersion: String by rootExtra
val gAndroidKtxVersion: String by rootExtra
val gAppCompatVersion: String by rootExtra
val gMaterialDesignVersion: String by rootExtra
val gKotlinVersion: String by project
val gAndroidKtxVersion: String by project
val gAppCompatVersion: String by project
val gMaterialDesignVersion: String by project
android {
compileSdkVersion(gCompileSdkVersion)
@@ -28,14 +26,14 @@ android {
minSdkVersion(gMinSdkVersion)
targetSdkVersion(gTargetSdkVersion)
versionCode = gVersionCode
versionCode = gVersionCode.toInt()
versionName = gVersionName
consumerProguardFiles("consumer-rules.pro")
}
buildTypes {
maybeCreate("release").apply {
named("release") {
isMinifyEnabled = false
proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro")
}

View File

@@ -19,5 +19,26 @@ android.useAndroidX=true
android.enableJetifier=true
# Kotlin code style for this project: "official" or "obsolete":
kotlin.code.style=official
kapt.incremental.apt=false
org.gradle.parallel=true
kapt.incremental.apt=false
# dependencies
gBuildToolsVersion=29.0.3
gCompileSdkVersion=android-29
gMinSdkVersion=24
gTargetSdkVersion=29
gVersionCode=10302
gVersionName=1.3.2
gKotlinVersion=1.3.72
gKotlinCoroutineVersion=1.3.7
gKotlinSerializationVersion=0.20.0
gRoomVersion=2.2.5
gAppCenterVersion=2.5.1
gAndroidKtxVersion=1.3.0
gRecyclerviewVersion=1.1.0
gAppCompatVersion=1.1.0
gMaterialDesignVersion=1.1.0
gShizukuPreferenceVersion=4.2.0
gMultiprocessPreferenceVersion=1.0.0

View File

@@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-6.1.1-all.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-6.5-all.zip

View File

@@ -6,23 +6,21 @@ plugins {
id("kotlinx-serialization")
}
val rootExtra = rootProject.extra
val gCompileSdkVersion: String by project
val gBuildToolsVersion: String by project
val gCompileSdkVersion: Int by rootExtra
val gBuildToolsVersion: String by rootExtra
val gMinSdkVersion: String by project
val gTargetSdkVersion: String by project
val gMinSdkVersion: Int by rootExtra
val gTargetSdkVersion: Int by rootExtra
val gVersionCode: String by project
val gVersionName: String by project
val gVersionCode: Int by rootExtra
val gVersionName: String by rootExtra
val gKotlinVersion: String by rootExtra
val gKotlinCoroutineVersion: String by rootExtra
val gKotlinSerializationVersion: String by rootExtra
val gRoomVersion: String by rootExtra
val gAndroidKtxVersion: String by rootExtra
val gMultiprocessPreferenceVersion: String by rootExtra
val gKotlinVersion: String by project
val gKotlinCoroutineVersion: String by project
val gKotlinSerializationVersion: String by project
val gRoomVersion: String by project
val gAndroidKtxVersion: String by project
val gMultiprocessPreferenceVersion: String by project
android {
compileSdkVersion(gCompileSdkVersion)
@@ -32,14 +30,14 @@ android {
minSdkVersion(gMinSdkVersion)
targetSdkVersion(gTargetSdkVersion)
versionCode = gVersionCode
versionCode = gVersionCode.toInt()
versionName = gVersionName
consumerProguardFiles("consumer-rules.pro")
}
buildTypes {
maybeCreate("release").apply {
named("release") {
isMinifyEnabled = false
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
@@ -66,6 +64,7 @@ dependencies {
implementation("org.jetbrains.kotlinx:kotlinx-serialization-runtime:$gKotlinSerializationVersion")
implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8:$gKotlinVersion")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:$gKotlinCoroutineVersion")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-jdk8:$gKotlinCoroutineVersion")
implementation("androidx.room:room-runtime:$gRoomVersion")
implementation("androidx.room:room-ktx:$gRoomVersion")
implementation("androidx.core:core-ktx:$gAndroidKtxVersion")

View File

@@ -1,4 +1,4 @@
package com.github.kr328.clash.core.model;
parcelable ProxyGroupList;
parcelable ProxyGroupWrapper;
parcelable General;

View File

@@ -5,12 +5,12 @@ import com.github.kr328.clash.core.model.Packet;
interface IClashManager {
// Control
boolean setSelectProxy(String proxy, String selected);
void startHealthCheck(String group, IStreamCallback callback);
void setSelector(String group, String selected);
void performHealthCheck(String group, IStreamCallback callback);
void setProxyMode(String mode);
// Query
ProxyGroupList queryAllProxies();
ProxyGroupWrapper queryProxyGroups();
General queryGeneral();
long queryBandwidth();

View File

@@ -2,7 +2,7 @@ package com.github.kr328.clash.service
import com.github.kr328.clash.core.Clash
import com.github.kr328.clash.core.model.General
import com.github.kr328.clash.core.model.ProxyGroupList
import com.github.kr328.clash.core.model.ProxyGroupWrapper
import com.github.kr328.clash.service.data.ProfileDao
import com.github.kr328.clash.service.data.SelectedProxyDao
import com.github.kr328.clash.service.data.SelectedProxyEntity
@@ -18,15 +18,15 @@ class ClashManager(parent: CoroutineScope) :
Clash.setProxyMode(requireNotNull(mode))
}
override fun queryAllProxies(): ProxyGroupList {
return ProxyGroupList(Clash.queryProxyGroups())
override fun queryProxyGroups(): ProxyGroupWrapper {
return ProxyGroupWrapper(Clash.queryProxyGroups())
}
override fun queryGeneral(): General {
return Clash.queryGeneral()
}
override fun setSelectProxy(proxy: String?, selected: String?): Boolean {
override fun setSelector(proxy: String?, selected: String?) {
require(proxy != null && selected != null)
launch {
@@ -35,7 +35,7 @@ class ClashManager(parent: CoroutineScope) :
SelectedProxyDao.setSelectedForProfile(SelectedProxyEntity(current.id, proxy, selected))
}
return Clash.setSelectedProxy(proxy, selected)
Clash.setSelector(proxy, selected)
}
override fun queryBandwidth(): Long {
@@ -44,10 +44,10 @@ class ClashManager(parent: CoroutineScope) :
return data.download + data.upload
}
override fun startHealthCheck(group: String?, callback: IStreamCallback?) {
override fun performHealthCheck(group: String?, callback: IStreamCallback?) {
require(group != null && callback != null)
Clash.startHealthCheck(group).invokeOnCompletion { u ->
Clash.performHealthCheck(group).whenComplete { _, u ->
if (u != null)
callback.completeExceptionally(u.message)
else

View File

@@ -4,10 +4,7 @@ import android.content.Intent
import android.os.Binder
import android.os.IBinder
import com.github.kr328.clash.service.clash.ClashRuntime
import com.github.kr328.clash.service.clash.module.CloseModule
import com.github.kr328.clash.service.clash.module.DynamicNotificationModule
import com.github.kr328.clash.service.clash.module.ReloadModule
import com.github.kr328.clash.service.clash.module.StaticNotificationModule
import com.github.kr328.clash.service.clash.module.*
import com.github.kr328.clash.service.settings.ServiceSettings
import com.github.kr328.clash.service.util.broadcastClashStarted
import com.github.kr328.clash.service.util.broadcastClashStopped
@@ -36,9 +33,7 @@ class ClashService : BaseService() {
runtime.install(ReloadModule(service)) {
onLoaded {
if (it != null) {
reason = it.message
stopSelf()
service.stopSelfForReason(it.message)
} else {
service.broadcastProfileLoaded()
}
@@ -46,9 +41,7 @@ class ClashService : BaseService() {
}
runtime.install(CloseModule()) {
onClosed {
reason = null
stopSelf()
service.stopSelfForReason(null)
}
}
@@ -78,4 +71,10 @@ class ClashService : BaseService() {
super.onDestroy()
}
private fun stopSelfForReason(reason: String?) {
this.reason = reason
stopSelf()
}
}

View File

@@ -13,10 +13,12 @@ import com.github.kr328.clash.service.model.asEntity
import com.github.kr328.clash.service.util.resolveBaseDir
import com.github.kr328.clash.service.util.resolveProfileFile
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.future.await
import kotlinx.coroutines.withContext
import java.io.File
import java.io.FileNotFoundException
import java.lang.Exception
import java.lang.NullPointerException
import java.util.*
object ProfileProcessor {
@@ -56,14 +58,17 @@ object ProfileProcessor {
source: Uri,
target: File,
baseDir: File
) = withContext(Dispatchers.IO) {
) {
when (source.scheme?.toLowerCase(Locale.getDefault())) {
"http", "https" ->
Clash.downloadProfile(source.toString(), target, baseDir)
"content", "file", "resource" -> {
val fd = context.contentResolver.openFileDescriptor(source, "r")
?: throw FileNotFoundException("$source not found")
Clash.downloadProfile(fd.detachFd(), target, baseDir)
val fd = withContext(Dispatchers.IO) {
@Suppress("BlockingMethodInNonBlockingContext")
context.contentResolver.openFileDescriptor(source, "r")
} ?: throw FileNotFoundException("$source not found")
Clash.downloadProfile(fd, target, baseDir)
}
else -> throw IllegalArgumentException("Invalid uri type")
}.await()

View File

@@ -45,11 +45,11 @@ class ProfileReceiver : BroadcastReceiver() {
val metadata = ProfileDao.queryById(id)?.asProfile(context) ?: return
val service = context.getSystemService<AlarmManager>() ?: return
val pendingIntent = cancelNextUpdate(context, id)
if (metadata.interval <= 0)
return
val pendingIntent = cancelNextUpdate(context, id)
service.set(
AlarmManager.RTC,
metadata.lastModified + metadata.interval,

Some files were not shown because too many files have changed in this diff Show More