Compare commits

...

43 Commits

Author SHA1 Message Date
kr328
1112534be6 Chore: bump version 2021-06-19 14:00:26 +08:00
kr328
784f249d54 Chore: migrate to new clash core 2021-06-19 13:59:58 +08:00
kr328
d6ad21b402 Chore: update clash core 2021-06-19 13:59:10 +08:00
kr328
bd03cac80d Chore: remove system proxy timeout 2021-06-19 13:39:29 +08:00
yi_Xu
da3ee71dfd Docs: update readme (#1044)
* docs: update README.md

- Fix the path and name of the release.
- Format the style with markdownlint.

* docs: fix apk name

* ci: add paths-ignore for workflow
2021-06-17 22:10:03 +08:00
kr328
62fb089e27 Fix: system proxy should handle connection in parallel 2021-06-13 18:21:51 +08:00
kr328
51dab21f6f Chore: bump version 2021-06-13 14:19:00 +08:00
kr328
9e89b3a201 Chore: update clash core 2021-06-13 14:19:00 +08:00
kr328
1c1e2b9f3d Chore: update dependencies 2021-06-13 14:19:00 +08:00
kr328
e2e0238dcd Chore: update tun2socket 2021-06-13 14:19:00 +08:00
Tragic Life
9745e11da4 Chore: update zh-TW translate (#1063) 2021-06-13 12:38:33 +08:00
kr328
8ae7ccbfc9 Chore: add coroutine-android proguard rules 2021-06-13 12:32:34 +08:00
kr328
5a2229596a Chore: use coroutine-android instead of core 2021-06-13 12:32:10 +08:00
kr328
a463d94480 Chore: bump version 2021-06-13 04:22:53 +08:00
kr328
750abc8c71 Improve: enable system proxy by default 2021-06-13 04:22:15 +08:00
kr328
8375fbd8b3 Fix: add http timeout & disable keep-alive 2021-06-13 02:45:20 +08:00
kr328
394e406a36 Chore: update tun2socket 2021-06-13 02:35:35 +08:00
kr328
2645af0d4c Chore: update tun2socket 2021-05-29 19:56:21 +08:00
Kr328
48222c22c8 Fix: should close connection if match blocking list 2021-05-29 00:35:04 +08:00
kr328
d6a71267c6 Chore: bump version 2021-05-28 23:05:09 +08:00
Kr328
0f4a46188c Chore: remove verbose logs 2021-05-28 11:36:29 +08:00
Kr328
5917b90837 Feature: add block loopback connections 2021-05-28 11:32:21 +08:00
Kr328
a222e90d1f Chore: update kaidl 2021-05-28 10:52:41 +08:00
Kr328
3f60d713f8 Chore: update dependencies 2021-05-28 10:52:35 +08:00
Kr328
9cb8433f3b Chore: update tun2socket 2021-05-28 10:43:00 +08:00
Goooler
428ca53532 Chore: use "implementation" & remove "exclude" for buildSrc dependencies (#1034) 2021-05-28 10:37:23 +08:00
kr328
6c4d7e537b Chore: bump version 2021-05-26 19:48:11 +08:00
kr328
58ab89736a Chore: change tun interface address/route 2021-05-26 16:03:43 +08:00
Kr328
5e34221a09 Fix: refactor tun implement 2021-05-26 15:33:59 +08:00
Kr328
ac35f2a5f4 Chore: update tun2socket 2021-05-26 03:17:30 +08:00
kr328
73992dca54 Chore: update tun2socket 2021-05-25 22:50:35 +08:00
kr328
53dc20109d Improve: migrate to latest dependencies 2021-05-25 22:49:03 +08:00
kr328
e7fef0a767 Chore: bump version 2021-05-25 22:30:38 +08:00
kr328
c73beabf7e Fix: launch mode of MainActivity should be singleTop 2021-05-25 20:30:07 +08:00
kr328
c7409d7ac6 Chore: cleanup code 2021-05-25 20:28:57 +08:00
kr328
5e238ab5d3 Chore: update tun2socket 2021-05-25 20:28:51 +08:00
kr328
a8f502ef4f Improve: merge ClashManager and ProfileService 2021-05-25 20:28:40 +08:00
kr328
c48ce82640 Fix: fix generated ksp search path 2021-05-25 19:40:44 +08:00
kr328
5594485bec Improve: enable -O3 for libbridge.so 2021-05-25 19:23:19 +08:00
kr328
f967bd299a Improve: enable CMAKE_POSITION_INDEPENDENT_CODE 2021-05-25 19:14:57 +08:00
kr328
a75c8f5dfc Chore: update tun2socket 2021-05-25 19:10:47 +08:00
kr328
271d56c01c Chore: update tun2socket 2021-05-25 14:11:17 +08:00
Kr328
12220789a3 Improve: clean tun2socket build on tasks["clean"] 2021-05-25 02:25:30 +08:00
64 changed files with 789 additions and 690 deletions

View File

@@ -1,5 +1,28 @@
name: Build Unsigned
on: [push, pull_request]
on:
workflow_dispatch:
push:
branches:
- main
paths-ignore:
- '.github/**'
- '.idea/**'
- '.gitignore'
- '.gitmodules'
- '**.md'
- 'LICENSE'
- 'NOTICE'
pull_request:
branches:
- main
paths-ignore:
- '.github/**'
- '.idea/**'
- '.gitignore'
- '.gitmodules'
- '**.md'
- 'LICENSE'
- 'NOTICE'
jobs:
BuildUnsigned:
runs-on: ubuntu-latest

View File

@@ -8,26 +8,20 @@ A Graphical user interface of [clash](https://github.com/Dreamacro/clash) for An
Fully feature of [clash](https://github.com/Dreamacro/clash) ~~(Exclude `external-controller`~~
### Requirement
* Android 5.0+ (minimum)
* Android 7.0+ (recommend)
* `armeabi-v7a` , `arm64-v8a`, `x86` or `x86_64` Architecture
- Android 5.0+ (minimum)
- Android 7.0+ (recommend)
- `armeabi-v7a` , `arm64-v8a`, `x86` or `x86_64` Architecture
### License
See also [LICENSE](./LICENSE) and [NOTICE](./NOTICE)
### Privacy Policy
### Privacy Policy
See also [PRIVACY_POLICY.md](./PRIVACY_POLICY.md)
### Build
1. Update submodules
@@ -38,7 +32,7 @@ See also [PRIVACY_POLICY.md](./PRIVACY_POLICY.md)
2. Install **OpenJDK 11**, **Android SDK**, **CMake** and **Golang**
3. Create `local.properties` in project root with
3. Create `local.properties` in project root with
```properties
sdk.dir=/path/to/android-sdk
@@ -51,7 +45,7 @@ See also [PRIVACY_POLICY.md](./PRIVACY_POLICY.md)
storePassword=<key store password>
keyAlias=<key alias>
keyPassword=<key password>
```
```
5. Build
@@ -59,4 +53,4 @@ See also [PRIVACY_POLICY.md](./PRIVACY_POLICY.md)
./gradlew app:assembleFossRelease
```
6. Pick `app-release-<arch>.apk` in `app/build/outputs/apks`
6. Pick `app-foss-<arch>-release-signed.apk` in `app/build/outputs/apk/foss/release/`

View File

@@ -26,6 +26,10 @@ android {
resValue("integer", "release_code", "$buildVersionCode")
}
packagingOptions {
exclude("DebugProbesKt.bin")
}
buildTypes {
named("release") {
isMinifyEnabled = true
@@ -121,7 +125,7 @@ dependencies {
premiumImplementation("com.microsoft.appcenter:appcenter-crashes:$appcenterVersion")
implementation(kotlin("stdlib-jdk7"))
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutineVersion")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutineVersion")
implementation("androidx.core:core-ktx:$coreVersion")
implementation("androidx.activity:activity:$activityVersion")
implementation("androidx.appcompat:appcompat:$appcompatVersion")

View File

@@ -31,3 +31,29 @@
public static void checkParameterIsNotNull(...);
public static void checkNotNullParameter(...);
}
# Kotlin Coroutine
# Allow R8 to optimize away the FastServiceLoader.
# Together with ServiceLoader optimization in R8
# this results in direct instantiation when loading Dispatchers.Main
-assumenosideeffects class kotlinx.coroutines.internal.MainDispatcherLoader {
boolean FAST_SERVICE_LOADER_ENABLED return false;
}
-assumenosideeffects class kotlinx.coroutines.internal.FastServiceLoaderKt {
boolean ANDROID_DETECTED return true;
}
-keep class kotlinx.coroutines.android.AndroidDispatcherFactory {*;}
# Disable support for "Missing Main Dispatcher", since we always have Android main dispatcher
-assumenosideeffects class kotlinx.coroutines.internal.MainDispatchersKt {
boolean SUPPORT_MISSING return false;
}
# Statically turn off all debugging facilities and assertions
-assumenosideeffects class kotlinx.coroutines.DebugKt {
boolean getASSERTIONS_ENABLED() return false;
boolean getDEBUG() return false;
boolean getRECOVER_STACK_TRACES() return false;
}

View File

@@ -41,7 +41,8 @@
android:name=".MainActivity"
android:configChanges="uiMode"
android:exported="true"
android:label="@string/launch_name">
android:label="@string/launch_name"
android:launchMode="singleTop">
<intent-filter>
<action android:name="android.intent.action.MAIN" />

View File

@@ -24,8 +24,6 @@ class AppCrashedActivity : BaseActivity<AppCrashedDesign>() {
SystemLogcat.dumpCrash()
}
Tracker.uploadLogcat(logs)
design.setAppLogs(logs)
while (isActive) {

View File

@@ -20,9 +20,9 @@ import com.github.kr328.clash.common.util.intent
import com.github.kr328.clash.core.model.LogMessage
import com.github.kr328.clash.log.LogcatCache
import com.github.kr328.clash.log.LogcatWriter
import com.github.kr328.clash.service.ClashManager
import com.github.kr328.clash.service.remote.IClashManager
import com.github.kr328.clash.service.RemoteService
import com.github.kr328.clash.service.remote.ILogObserver
import com.github.kr328.clash.service.remote.IRemoteService
import com.github.kr328.clash.service.remote.unwrap
import com.github.kr328.clash.util.logsDir
import kotlinx.coroutines.*
@@ -52,7 +52,7 @@ class LogcatService : Service(), CoroutineScope by CoroutineScope(Dispatchers.De
showNotification()
bindService(ClashManager::class.intent, connection, Context.BIND_AUTO_CREATE)
bindService(RemoteService::class.intent, connection, Context.BIND_AUTO_CREATE)
}
override fun onDestroy() {
@@ -88,7 +88,7 @@ class LogcatService : Service(), CoroutineScope by CoroutineScope(Dispatchers.De
return stopSelf()
launch(Dispatchers.IO) {
val service = binder.unwrap(IClashManager::class)
val service = binder.unwrap(IRemoteService::class).clash()
val channel = Channel<LogMessage>(CACHE_CAPACITY)
try {

View File

@@ -8,7 +8,8 @@ object SystemLogcat {
"Go",
"DEBUG",
"AndroidRuntime",
"ClashForAndroid"
"ClashForAndroid",
"LwIP",
)
fun dumpCrash(): String {

View File

@@ -15,7 +15,7 @@ import kotlinx.coroutines.launch
object Remote {
val broadcasts: Broadcasts = Broadcasts(Global.application)
val services: Services = Services(Global.application) {
val service: Service = Service(Global.application) {
ApplicationObserver.createdActivities.forEach { it.finish() }
val intent = AppCrashedActivity::class.intent
@@ -56,10 +56,10 @@ object Remote {
while (true) {
if (visible.receive()) {
services.bind()
service.bind()
broadcasts.register()
} else {
services.unbind()
service.unbind()
broadcasts.unregister()
}
}

View File

@@ -0,0 +1,64 @@
package com.github.kr328.clash.remote
import android.app.Application
import android.content.ComponentName
import android.content.Context
import android.content.ServiceConnection
import android.os.IBinder
import com.github.kr328.clash.Tracker
import com.github.kr328.clash.common.log.Log
import com.github.kr328.clash.common.util.intent
import com.github.kr328.clash.log.SystemLogcat
import com.github.kr328.clash.service.RemoteService
import com.github.kr328.clash.service.remote.IRemoteService
import com.github.kr328.clash.service.remote.unwrap
import com.github.kr328.clash.util.unbindServiceSilent
import java.util.concurrent.TimeUnit
class Service(private val context: Application, val crashed: () -> Unit) {
val remote = Resource<IRemoteService>()
private val connection = object : ServiceConnection {
private var lastCrashed: Long = -1
override fun onServiceConnected(name: ComponentName?, service: IBinder) {
remote.set(service.unwrap(IRemoteService::class))
}
override fun onServiceDisconnected(name: ComponentName?) {
remote.set(null)
Tracker.uploadLogcat(SystemLogcat.dumpCrash())
if (System.currentTimeMillis() - lastCrashed < TOGGLE_CRASHED_INTERVAL) {
unbind()
crashed()
}
lastCrashed = System.currentTimeMillis()
Log.w("RemoteManager crashed")
}
}
fun bind() {
try {
context.bindService(RemoteService::class.intent, connection, Context.BIND_AUTO_CREATE)
} catch (e: Exception) {
unbind()
crashed()
}
}
fun unbind() {
context.unbindServiceSilent(connection)
remote.set(null)
}
companion object {
private val TOGGLE_CRASHED_INTERVAL = TimeUnit.SECONDS.toMillis(10)
}
}

View File

@@ -1,88 +0,0 @@
package com.github.kr328.clash.remote
import android.app.Application
import android.content.ComponentName
import android.content.Context
import android.content.ServiceConnection
import android.os.IBinder
import com.github.kr328.clash.common.log.Log
import com.github.kr328.clash.common.util.intent
import com.github.kr328.clash.service.ClashManager
import com.github.kr328.clash.service.ProfileService
import com.github.kr328.clash.service.remote.IClashManager
import com.github.kr328.clash.service.remote.IProfileManager
import com.github.kr328.clash.service.remote.unwrap
import com.github.kr328.clash.util.unbindServiceSilent
import java.util.concurrent.TimeUnit
class Services(private val context: Application, val crashed: () -> Unit) {
val clash = Resource<IClashManager>()
val profile = Resource<IProfileManager>()
private val clashConnection = object : ServiceConnection {
private var lastCrashed: Long = -1
override fun onServiceConnected(name: ComponentName?, service: IBinder?) {
clash.set(service?.unwrap(IClashManager::class))
}
override fun onServiceDisconnected(name: ComponentName?) {
clash.set(null)
if (System.currentTimeMillis() - lastCrashed < TOGGLE_CRASHED_INTERVAL) {
unbind()
crashed()
}
lastCrashed = System.currentTimeMillis()
Log.w("ClashManager crashed")
}
}
private val profileConnection = object : ServiceConnection {
private var lastCrashed: Long = -1
override fun onServiceConnected(name: ComponentName?, service: IBinder?) {
profile.set(service?.unwrap(IProfileManager::class))
}
override fun onServiceDisconnected(name: ComponentName?) {
profile.set(null)
if (System.currentTimeMillis() - lastCrashed < TOGGLE_CRASHED_INTERVAL) {
unbind()
crashed()
}
lastCrashed = System.currentTimeMillis()
Log.w("ProfileService crashed")
}
}
fun bind() {
try {
context.bindService(ClashManager::class.intent, clashConnection, Context.BIND_AUTO_CREATE)
context.bindService(ProfileService::class.intent, profileConnection, Context.BIND_AUTO_CREATE)
} catch (e: Exception) {
unbind()
crashed()
}
}
fun unbind() {
context.unbindServiceSilent(clashConnection)
context.unbindServiceSilent(profileConnection)
clash.set(null)
profile.set(null)
}
companion object {
private val TOGGLE_CRASHED_INTERVAL = TimeUnit.SECONDS.toMillis(10)
}
}

View File

@@ -14,14 +14,15 @@ suspend fun <T> withClash(
block: suspend IClashManager.() -> T
): T {
while (true) {
val client = Remote.services.clash.get()
val remote = Remote.service.remote.get()
val client = remote.clash()
try {
return withContext(context) { client.block() }
} catch (e: DeadObjectException) {
Log.w("Remote services panic")
Remote.services.clash.reset(client)
Remote.service.remote.reset(remote)
}
}
}
@@ -31,14 +32,15 @@ suspend fun <T> withProfile(
block: suspend IProfileManager.() -> T
): T {
while (true) {
val client = Remote.services.profile.get()
val remote = Remote.service.remote.get()
val client = remote.profile()
try {
return withContext(context) { client.block() }
} catch (e: DeadObjectException) {
Log.w("Remote services panic")
Remote.services.profile.reset(client)
Remote.service.remote.reset(remote)
}
}
}

View File

@@ -1,5 +1,5 @@
plugins {
kotlin("jvm") version "1.5.0"
kotlin("jvm") version "1.5.10"
`java-gradle-plugin`
}
@@ -9,20 +9,10 @@ repositories {
}
dependencies {
implementation(kotlin("stdlib"))
compileOnly(gradleApi())
api(kotlin("gradle-plugin"))
api(kotlin("serialization"))
api("com.android.tools.build:gradle:4.2.1") {
exclude("org.jetbrains.kotlin", "kotlin-stdlib-jdk8")
exclude("org.jetbrains.kotlin", "kotlin-stdlib-jdk7")
exclude("org.jetbrains.kotlin", "kotlin-reflect")
}
api("com.google.devtools.ksp:symbol-processing-gradle-plugin:1.5.0-1.0.0-alpha10") {
exclude("com.android.tools.build", "gradle")
}
implementation(kotlin("gradle-plugin"))
implementation(kotlin("serialization"))
implementation("com.android.tools.build:gradle:4.2.1")
implementation("com.google.devtools.ksp:symbol-processing-gradle-plugin:1.5.10-1.0.0-beta01")
}
gradlePlugin {

View File

@@ -1,7 +1,7 @@
import org.gradle.api.Project
const val buildVersionCode = 204003
const val buildVersionName = "2.4.3"
const val buildVersionCode = 204009
const val buildVersionName = "2.4.9"
const val buildMinSdkVersion = 21
const val buildTargetSdkVersion = 30

View File

@@ -4,10 +4,10 @@ const val roomVersion = "2.3.0"
const val coreVersion = "1.5.0"
const val appcompatVersion = "1.3.0"
const val muiltprocessVersion = "1.0.0"
const val appcenterVersion = "4.1.1"
const val appcenterVersion = "4.2.0"
const val serializationVersion = "1.2.1"
const val materialVersion = "1.3.0"
const val coordinatorlayoutVersion = "1.1.0"
const val recyclerviewVersion = "1.2.0"
const val recyclerviewVersion = "1.2.1"
const val fragmentVersion = "1.3.4"
const val viewpagerVersion = "1.0.0"

View File

@@ -11,14 +11,12 @@ data class BuildConfig(
val minSdkVersion: Int,
) : Serializable {
companion object {
fun of(extension: BaseExtension, variant: BaseVariant): BuildConfig {
fun of(abis: List<NativeAbi>, minSdkVersion: Int, variant: BaseVariant): BuildConfig {
return BuildConfig(
debug = variant.buildType.isDebuggable,
premium = variant.flavorName == "premium",
abis = extension.defaultConfig.externalNativeBuild.cmake.abiFilters
.map { NativeAbi.parse(it) }
.distinct(),
minSdkVersion = extension.defaultConfig.minSdkVersion!!.apiLevel
abis = abis,
minSdkVersion = minSdkVersion
)
}
}

View File

@@ -11,8 +11,19 @@ class ClashBuildPlugin : Plugin<Project> {
override fun apply(target: Project) {
target.afterEvaluate {
target.extensions.getByType(LibraryExtension::class.java).apply {
val abis = defaultConfig.externalNativeBuild.cmake.abiFilters
.map { NativeAbi.parse(it) }
.distinct()
val minSdkVersion = defaultConfig.minSdkVersion!!.apiLevel
target.tasks.register("cleanGolang", ClashCleanTask::class.java) {
it.applyFrom(target, abis)
target.tasks.getByName("clean").dependsOn(it)
}
libraryVariants.forEach { variant ->
val config = BuildConfig.of(this, variant)
val config = BuildConfig.of(abis, minSdkVersion, variant)
val buildDir = target.golangBuild.resolve(variant.name)
val capitalize = variant.name.capitalize(Locale.getDefault())

View File

@@ -39,8 +39,8 @@ abstract class ClashBuildTask : DefaultTask() {
config.abis.forEach {
Command.ofGoRun(
"make/make.go",
listOf("bridge", "native", "build", "android", it.goArch),
input.resolve("tun2socket/bridge"),
listOf("tun2socket", ".", "android", it.goArch),
input.resolve("tun2socket"),
environment.ofLwipBuild(it)
).exec()

View File

@@ -0,0 +1,17 @@
package com.github.kr328.clash.tools
import org.gradle.api.Project
import org.gradle.api.tasks.Delete
import golangSource
abstract class ClashCleanTask : Delete() {
fun applyFrom(project: Project, abis: List<NativeAbi>) {
val bridge = project.golangSource.resolve("tun2socket")
delete(bridge.resolve("build"))
abis.forEach {
delete(bridge.resolve("build_android_${it.goArch}.go"))
}
}
}

View File

@@ -34,11 +34,25 @@ class Environment(
}
fun ofLwipBuild(abi: NativeAbi): Map<String, String> {
val host = when {
Os.isFamily(Os.FAMILY_WINDOWS) ->
"windows"
Os.isFamily(Os.FAMILY_MAC) ->
"darwin"
Os.isFamily(Os.FAMILY_UNIX) ->
"linux"
else ->
throw GradleException("Unsupported host: ${System.getProperty("os.name")}")
}
val compiler = ndkDirectory.resolve("toolchains/llvm/prebuilt/$host-x86_64/bin")
.resolve("${abi.compiler}${minSdkVersion}-clang")
val ar = ndkDirectory.resolve("toolchains/llvm/prebuilt/$host-x86_64/bin")
.resolve("${abi.archiver}-ar")
return mapOf(
"CMAKE_SYSTEM_NAME" to "Android",
"CMAKE_ANDROID_NDK" to ndkDirectory.absolutePath,
"CMAKE_ANDROID_ARCH_ABI" to abi.value,
"CMAKE_SYSTEM_VERSION" to minSdkVersion.toString()
"CC" to compiler.absolutePath,
"AR" to ar.absolutePath,
)
}
}

View File

@@ -3,13 +3,14 @@ package com.github.kr328.clash.tools
enum class NativeAbi(
val value: String,
val compiler: String,
val archiver: String,
val goArch: String,
val goArm: String
) {
ArmeabiV7a("armeabi-v7a", "armv7a-linux-androideabi", "arm", "7"),
Arm64V8a("arm64-v8a", "aarch64-linux-android", "arm64", ""),
X86("x86", "i686-linux-android", "386", ""),
X64("x86_64", "x86_64-linux-android", "amd64", "");
ArmeabiV7a("armeabi-v7a", "armv7a-linux-androideabi", "arm-linux-androideabi", "arm", "7"),
Arm64V8a("arm64-v8a", "aarch64-linux-android", "aarch64-linux-android", "arm64", ""),
X86("x86", "i686-linux-android", "i686-linux-android", "386", ""),
X64("x86_64", "x86_64-linux-android", "x86_64-linux-android", "amd64", "");
companion object {
fun parse(value: String): NativeAbi {

View File

@@ -41,7 +41,7 @@ dependencies {
implementation(kotlin("stdlib-jdk7"))
implementation("androidx.core:core-ktx:$coreVersion")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutineVersion")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutineVersion")
}
repositories {

View File

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="receive_clash_broadcasts">接收 Clash 廣播</string>
<string name="receive_broadcasts_of_clash">接收來自 Clash 內部的廣播</string>
</resources>

View File

@@ -82,7 +82,7 @@ dependencies {
implementation(kotlin("stdlib-jdk7"))
implementation("androidx.core:core-ktx:$coreVersion")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutineVersion")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutineVersion")
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:$serializationVersion")
}

View File

@@ -2,6 +2,9 @@ cmake_minimum_required(VERSION 3.0)
project(clash-bridge C)
set(CMAKE_POSITION_INDEPENDENT_CODE on)
set(CMAKE_C_FLAGS_RELEASE "${CMAKE_C_FLAGS_RELEASE} -O3")
set(GO_OUTPUT_BASE ${GO_OUTPUT}/${FLAVOR_NAME})
if ("${CMAKE_BUILD_TYPE}" STREQUAL "Debug")

View File

@@ -96,15 +96,17 @@ Java_com_github_kr328_clash_core_bridge_Bridge_nativeNotifyInstalledAppChanged(J
}
JNIEXPORT void JNICALL
Java_com_github_kr328_clash_core_bridge_Bridge_nativeStartTun(JNIEnv *env, jobject thiz, jint fd,
jint mtu, jstring dns,
Java_com_github_kr328_clash_core_bridge_Bridge_nativeStartTun(JNIEnv *env, jobject thiz,
jint fd, jint mtu,
jstring dns, jstring blocking,
jobject cb) {
TRACE_METHOD();
scoped_string _blocking = get_string(blocking);
scoped_string _dns = get_string(dns);
jobject _interface = new_global(cb);
startTun(fd, mtu, _dns, _interface);
startTun(fd, mtu, _dns, _blocking, _interface);
}
JNIEXPORT void JNICALL

View File

@@ -25,13 +25,12 @@ type Status struct {
var client = &http.Client{
Transport: &http.Transport{
// from http.DefaultTransport
MaxIdleConns: 100,
IdleConnTimeout: 90 * time.Second,
DisableKeepAlives: true,
TLSHandshakeTimeout: 10 * time.Second,
ExpectContinueTimeout: 1 * time.Second,
DialContext: dialer.DefaultTunnelDialer,
},
Timeout: 60 * time.Second,
}
func openUrl(url string) (io.ReadCloser, error) {

View File

@@ -5,6 +5,7 @@ import (
"errors"
"fmt"
"strings"
"time"
"github.com/Dreamacro/clash/log"
"github.com/dlclark/regexp2"
@@ -16,6 +17,11 @@ import (
"github.com/Dreamacro/clash/dns"
)
const (
defaultHealthCheckUrl = "https://www.gstatic.com/generate_204"
defaultHealthCheckInterval = time.Hour
)
var processors = []processor{
patchOverride,
patchGeneral,
@@ -88,7 +94,13 @@ func patchProviders(cfg *config.RawConfig, profileDir string) error {
func patchProxyGroup(cfg *config.RawConfig, _ string) error {
for _, g := range cfg.ProxyGroup {
g["lazy"] = false
if _, exist := g["url"]; !exist {
g["url"] = defaultHealthCheckUrl
}
if _, exist := g["interval"]; !exist {
g["interval"] = int(defaultHealthCheckInterval.Seconds())
}
}
return nil

View File

@@ -1,65 +1,27 @@
package proxy
import (
"bufio"
"net"
"net/http"
"sync"
"time"
adapters "github.com/Dreamacro/clash/adapters/inbound"
"github.com/Dreamacro/clash/log"
"github.com/Dreamacro/clash/listener/http"
"github.com/Dreamacro/clash/tunnel"
)
const (
LocalHttpTimeout = time.Millisecond * 100
)
var listener *httpListener
var listener *http.Listener
var lock sync.Mutex
type httpListener struct {
net.Listener
closed bool
}
func Start(listen string) (listenAt string, err error) {
lock.Lock()
defer lock.Unlock()
stopLocked()
l, err := net.Listen("tcp", listen)
if err != nil {
log.Errorln("Listen HTTP proxy at: %s: %s", listen, err.Error())
return
listener, err = http.NewWithAuthenticate(listen, tunnel.TCPIn(), false)
if err == nil {
listenAt = listener.Listener().Addr().String()
}
h := &httpListener{
Listener: l,
closed: false,
}
listener = h
go func() {
for !h.closed {
conn, err := h.Accept()
if err != nil {
log.Warnln("Accept connection: %s", err.Error())
continue
}
_ = conn.(*net.TCPConn).SetKeepAlive(false)
h.handleConn(conn)
}
}()
return h.Addr().String(), nil
return
}
func Stop() {
@@ -71,38 +33,8 @@ func Stop() {
func stopLocked() {
if listener != nil {
listener.closed = true
_ = listener.Close()
listener.Close()
}
listener = nil
}
func (l *httpListener) handleConn(conn net.Conn) {
_ = conn.SetReadDeadline(time.Now().Add(LocalHttpTimeout))
br := bufio.NewReader(conn)
request, err := http.ReadRequest(br)
_ = conn.SetReadDeadline(time.Time{})
if err != nil || request.URL.Host == "" {
if err != nil {
log.Warnln("HTTP Connection closed: %s", err.Error())
}
_ = conn.Close()
return
}
if request.Method == http.MethodConnect {
_, err := conn.Write([]byte("HTTP/1.1 200 Connection established\r\n\r\n"))
if err != nil {
return
}
tunnel.Add(adapters.NewHTTPS(request, conn))
return
}
tunnel.Add(adapters.NewHTTP(request, conn))
}

View File

@@ -11,8 +11,6 @@ import (
"cfa/tun"
"golang.org/x/sync/semaphore"
"github.com/Dreamacro/clash/log"
)
type remoteTun struct {
@@ -23,7 +21,7 @@ type remoteTun struct {
}
func (t *remoteTun) markSocket(fd int) {
_ = t.limit.Acquire(context.Background(), 1)
_ = t.limit.Acquire(context.TODO(), 1)
defer t.limit.Release(1)
if t.closed {
@@ -34,7 +32,7 @@ func (t *remoteTun) markSocket(fd int) {
}
func (t *remoteTun) querySocketUid(protocol int, source, target string) int {
_ = t.limit.Acquire(context.Background(), 1)
_ = t.limit.Acquire(context.TODO(), 1)
defer t.limit.Release(1)
if t.closed {
@@ -50,26 +48,27 @@ func (t *remoteTun) stop() {
t.closed = true
C.release_object(t.callback)
app.ApplyTunContext(nil, nil)
log.Infoln("Android tun device destroyed")
C.release_object(t.callback)
}
//export startTun
func startTun(fd, mtu C.int, dns C.c_string, callback unsafe.Pointer) C.int {
func startTun(fd, mtu C.int, gateway, dns C.c_string, callback unsafe.Pointer) C.int {
f := int(fd)
m := int(mtu)
g := C.GoString(gateway)
d := C.GoString(dns)
remote := &remoteTun{callback: callback, closed: false, limit: semaphore.NewWeighted(4)}
if tun.Start(f, m, d) != nil {
return 1
}
app.ApplyTunContext(remote.markSocket, remote.querySocketUid)
log.Infoln("Android tun device created")
if tun.Start(f, m, g, d, remote.stop) != nil {
app.ApplyTunContext(nil, nil)
return 1
}
return 0
}

View File

@@ -1,19 +1,12 @@
package tun
import (
"encoding/binary"
"io"
"net"
"time"
"github.com/Dreamacro/clash/component/resolver"
"github.com/kr328/tun2socket/bridge"
D "github.com/miekg/dns"
)
const defaultDnsReadTimeout = time.Second * 30
func shouldHijackDns(dns net.IP, target net.IP, targetPort int) bool {
if targetPort != 53 {
return false
@@ -22,58 +15,7 @@ func shouldHijackDns(dns net.IP, target net.IP, targetPort int) bool {
return net.IPv4zero.Equal(dns) || target.Equal(dns)
}
func hijackUDPDns(pkt []byte, lAddr, rAddr net.Addr, udp bridge.UDP) {
go func() {
answer, err := relayDnsPacket(pkt)
if err != nil {
return
}
_, _ = udp.WriteTo(answer, lAddr, rAddr)
recycleUDP(pkt)
}()
}
func hijackTCPDns(conn net.Conn) {
go func() {
defer conn.Close()
for {
if err := conn.SetReadDeadline(time.Now().Add(defaultDnsReadTimeout)); err != nil {
return
}
var length uint16
if binary.Read(conn, binary.BigEndian, &length) != nil {
return
}
data := make([]byte, length)
_, err := io.ReadFull(conn, data)
if err != nil {
return
}
rb, err := relayDnsPacket(data)
if err != nil {
continue
}
if binary.Write(conn, binary.BigEndian, uint16(len(rb))) != nil {
return
}
if _, err := conn.Write(rb); err != nil {
return
}
}
}()
}
func relayDnsPacket(payload []byte) ([]byte, error) {
func relayDns(payload []byte) ([]byte, error) {
msg := &D.Msg{}
if err := msg.Unpack(payload); err != nil {
return nil, err
@@ -84,14 +26,6 @@ func relayDnsPacket(payload []byte) ([]byte, error) {
return nil, err
}
for _, ans := range r.Answer {
header := ans.Header()
if header.Class == D.ClassINET && (header.Rrtype == D.TypeA || header.Rrtype == D.TypeAAAA) {
header.Ttl = 1
}
}
r.SetRcode(msg, r.Rcode)
return r.Pack()

View File

@@ -0,0 +1,39 @@
package tun
import "github.com/Dreamacro/clash/log"
func (a *adapter) rx() {
log.Infoln("[ATUN] Device rx started")
defer log.Infoln("[ATUN] Device rx exited")
defer a.once.Do(a.stop)
defer a.close()
buf := make([]byte, a.mtu)
for {
n, err := a.device.Read(buf)
if err != nil {
return
}
_, _ = a.stack.Link().Write(buf[:n])
}
}
func (a *adapter) tx() {
log.Infoln("[ATUN] Device tx started")
defer log.Infoln("[ATUN] Device tx exited")
defer a.once.Do(a.stop)
defer a.close()
buf := make([]byte, a.mtu)
for {
n, err := a.stack.Link().Read(buf)
if err != nil {
return
}
_, _ = a.device.Write(buf[:n])
}
}

View File

@@ -1,27 +1,105 @@
package tun
import (
"encoding/binary"
"io"
"net"
"strconv"
"time"
C "github.com/Dreamacro/clash/constant"
CTX "github.com/Dreamacro/clash/context"
"github.com/Dreamacro/clash/context"
"github.com/Dreamacro/clash/log"
"github.com/Dreamacro/clash/tunnel"
)
func handleTCP(conn net.Conn, source *net.TCPAddr, target *net.TCPAddr) {
metadata := &C.Metadata{
NetWork: C.TCP,
Type: C.SOCKS,
SrcIP: source.IP,
DstIP: target.IP,
SrcPort: strconv.Itoa(source.Port),
DstPort: strconv.Itoa(target.Port),
AddrType: C.AtypIPv4,
Host: "",
RawSrcAddr: source,
RawDstAddr: target,
const defaultDnsReadTimeout = time.Second * 30
func (a *adapter) tcp() {
log.Infoln("[ATUN] TCP listener started")
defer log.Infoln("[ATUN] TCP listener exited")
defer a.stack.Close()
accept:
for {
conn, err := a.stack.TCP().Accept()
if err != nil {
return
}
sAddr := conn.LocalAddr().(*net.TCPAddr)
tAddr := conn.RemoteAddr().(*net.TCPAddr)
// handle dns messages
if a.hijackTCPDNS(conn, tAddr) {
continue
}
// drop all connections connect to blocking list
for _, b := range a.blocking {
if b.Contains(tAddr.IP) {
_ = conn.Close()
continue accept
}
}
metadata := &C.Metadata{
NetWork: C.TCP,
Type: C.SOCKS,
SrcIP: sAddr.IP,
DstIP: tAddr.IP,
SrcPort: strconv.Itoa(sAddr.Port),
DstPort: strconv.Itoa(tAddr.Port),
AddrType: C.AtypIPv4,
Host: "",
RawSrcAddr: sAddr,
RawDstAddr: tAddr,
}
tunnel.TCPIn() <- context.NewConnContext(conn, metadata)
}
}
func (a *adapter) hijackTCPDNS(conn net.Conn, tAddr *net.TCPAddr) bool {
if !shouldHijackDns(a.dns, tAddr.IP, tAddr.Port) {
return false
}
tunnel.Add(CTX.NewConnContext(conn, metadata))
go func() {
defer conn.Close()
for {
if err := conn.SetReadDeadline(time.Now().Add(defaultDnsReadTimeout)); err != nil {
return
}
var length uint16
if binary.Read(conn, binary.BigEndian, &length) != nil {
return
}
data := make([]byte, length)
_, err := io.ReadFull(conn, data)
if err != nil {
return
}
rb, err := relayDns(data)
if err != nil {
continue
}
if binary.Write(conn, binary.BigEndian, uint16(len(rb))) != nil {
return
}
if _, err := conn.Write(rb); err != nil {
return
}
}
}()
return true
}

View File

@@ -3,34 +3,42 @@ package tun
import (
"net"
"os"
"strings"
"sync"
"syscall"
"github.com/kr328/tun2socket"
)
type context struct {
device *os.File
stack tun2socket.Stack
type adapter struct {
device *os.File
stack tun2socket.Stack
blocking []*net.IPNet
dns net.IP
mtu int
once sync.Once
stop func()
}
var lock sync.Mutex
var tun *context
var instance *adapter
func (ctx *context) close() {
_ = ctx.stack.Close()
_ = ctx.device.Close()
func (a *adapter) close() {
_ = a.stack.Close()
_ = a.device.Close()
}
func Start(fd, mtu int, dns string) error {
func Start(fd, mtu int, dns string, blocking string, stop func()) error {
lock.Lock()
defer lock.Unlock()
stopLocked()
if instance != nil {
instance.close()
}
dnsIP := net.ParseIP(dns)
_ = syscall.SetNonblock(fd, true)
device := os.NewFile(uintptr(fd), "/dev/tun")
stack, err := tun2socket.NewStack(mtu)
if err != nil {
_ = device.Close()
@@ -38,100 +46,35 @@ func Start(fd, mtu int, dns string) error {
return err
}
ctx := &context{
device: device,
stack: stack,
dn := net.ParseIP(dns)
var blk []*net.IPNet
for _, b := range strings.Split(blocking, ";") {
_, n, err := net.ParseCIDR(b)
if err != nil {
device.Close()
return err
}
blk = append(blk, n)
}
go func() {
// device -> lwip
instance = &adapter{
device: device,
stack: stack,
blocking: blk,
dns: dn,
mtu: mtu,
once: sync.Once{},
stop: stop,
}
defer ctx.close()
buf := make([]byte, mtu)
for {
n, err := device.Read(buf)
if err != nil {
return
}
_, _ = stack.Link().Write(buf[:n])
}
}()
go func() {
// lwip -> device
defer ctx.close()
buf := make([]byte, mtu)
for {
n, err := stack.Link().Read(buf)
if err != nil {
return
}
_, _ = device.Write(buf[:n])
}
}()
go func() {
// lwip tcp
defer ctx.close()
for {
conn, err := stack.TCP().Accept()
if err != nil {
return
}
source := conn.LocalAddr().(*net.TCPAddr)
target := conn.RemoteAddr().(*net.TCPAddr)
if shouldHijackDns(dnsIP, target.IP, target.Port) {
hijackTCPDns(conn)
continue
}
handleTCP(conn, source, target)
}
}()
go func() {
// lwip udp
defer ctx.close()
for {
buf := allocUDP(mtu)
n, lAddr, rAddr, err := stack.UDP().ReadFrom(buf)
if err != nil {
return
}
source := lAddr.(*net.UDPAddr)
target := rAddr.(*net.UDPAddr)
if n == 0 {
continue
}
if shouldHijackDns(dnsIP, target.IP, target.Port) {
hijackUDPDns(buf[:n], source, target, stack.UDP())
continue
}
handleUDP(buf[:n], source, target, stack.UDP())
}
}()
tun = ctx
go instance.rx()
go instance.tx()
go instance.tcp()
go instance.udp()
return nil
}
@@ -140,13 +83,9 @@ func Stop() {
lock.Lock()
defer lock.Unlock()
stopLocked()
}
func stopLocked() {
if tun != nil {
tun.close()
if instance != nil {
instance.close()
}
tun = nil
instance = nil
}

View File

@@ -3,57 +3,97 @@ package tun
import (
"net"
"github.com/Dreamacro/clash/log"
"github.com/Dreamacro/clash/transport/socks5"
"github.com/kr328/tun2socket/bridge"
"github.com/kr328/tun2socket"
adapters "github.com/Dreamacro/clash/adapters/inbound"
"github.com/Dreamacro/clash/adapter/inbound"
"github.com/Dreamacro/clash/common/pool"
C "github.com/Dreamacro/clash/constant"
"github.com/Dreamacro/clash/tunnel"
)
type udpPacket struct {
source *net.UDPAddr
data []byte
udp bridge.UDP
type packet struct {
stack tun2socket.Stack
local *net.UDPAddr
data []byte
}
func (u *udpPacket) Data() []byte {
return u.data
func (pkt *packet) Data() []byte {
return pkt.data
}
func (u *udpPacket) WriteBack(b []byte, addr net.Addr) (n int, err error) {
return u.udp.WriteTo(b, u.source, addr)
func (pkt *packet) WriteBack(b []byte, addr net.Addr) (n int, err error) {
return pkt.stack.UDP().WriteTo(b, pkt.local, addr)
}
func (u *udpPacket) Drop() {
recycleUDP(u.data)
func (pkt *packet) Drop() {
pool.Put(pkt.data)
}
func (u *udpPacket) LocalAddr() net.Addr {
func (pkt *packet) LocalAddr() net.Addr {
return &net.UDPAddr{
IP: u.source.IP,
Port: int(u.source.Port),
IP: pkt.local.IP,
Port: pkt.local.Port,
Zone: "",
}
}
func handleUDP(payload []byte, source *net.UDPAddr, target *net.UDPAddr, udp bridge.UDP) {
pkt := &udpPacket{
source: source,
data: payload,
udp: udp,
func (a *adapter) udp() {
log.Infoln("[ATUN] UDP receiver started")
defer log.Infoln("[ATUN] UDP receiver exited")
defer a.stack.Close()
read:
for {
buf := pool.Get(a.mtu)
n, lAddr, rAddr, err := a.stack.UDP().ReadFrom(buf)
if err != nil {
return
}
sAddr := lAddr.(*net.UDPAddr)
tAddr := rAddr.(*net.UDPAddr)
// handle dns messages
if a.hijackUDPDNS(buf[:n], sAddr, tAddr) {
continue
}
// drop all packet send to blocking list
for _, b := range a.blocking {
if b.Contains(tAddr.IP) {
continue read
}
}
pkt := &packet{
stack: a.stack,
local: sAddr,
data: buf[:n],
}
tunnel.UDPIn() <- inbound.NewPacket(socks5.ParseAddrToSocksAddr(tAddr), pkt, C.SOCKS)
}
}
func (a *adapter) hijackUDPDNS(pkt []byte, sAddr, tAddr *net.UDPAddr) bool {
if !shouldHijackDns(a.dns, tAddr.IP, tAddr.Port) {
return false
}
adapter := adapters.NewPacket(socks5.ParseAddrToSocksAddr(target), pkt, C.SOCKS)
go func() {
answer, err := relayDns(pkt)
tunnel.AddPacket(adapter)
}
if err != nil {
return
}
func allocUDP(size int) []byte {
return pool.Get(size)
}
_, _ = a.stack.UDP().WriteTo(answer, sAddr, tAddr)
func recycleUDP(payload []byte) {
_ = pool.Put(payload)
pool.Put(pkt)
}()
return true
}

View File

@@ -3,9 +3,9 @@ package tunnel
import (
"sync"
"github.com/Dreamacro/clash/adapters/outbound"
"github.com/Dreamacro/clash/adapters/outboundgroup"
"github.com/Dreamacro/clash/adapters/provider"
"github.com/Dreamacro/clash/adapter"
"github.com/Dreamacro/clash/adapter/outboundgroup"
"github.com/Dreamacro/clash/adapter/provider"
"github.com/Dreamacro/clash/log"
"github.com/Dreamacro/clash/tunnel"
)
@@ -19,7 +19,7 @@ func HealthCheck(name string) {
return
}
g, ok := p.(*outbound.Proxy).ProxyAdapter.(outboundgroup.ProxyGroup)
g, ok := p.(*adapter.Proxy).ProxyAdapter.(outboundgroup.ProxyGroup)
if !ok {
log.Warnln("Request health check for `%s`: invalid type %s", name, p.Type().String())

View File

@@ -7,7 +7,7 @@ import (
"fmt"
"time"
"github.com/Dreamacro/clash/adapters/provider"
"github.com/Dreamacro/clash/adapter/provider"
"github.com/Dreamacro/clash/tunnel"
)

View File

@@ -7,7 +7,7 @@ import (
"fmt"
"time"
"github.com/Dreamacro/clash/adapters/provider"
"github.com/Dreamacro/clash/adapter/provider"
"github.com/Dreamacro/clash/log"
"github.com/Dreamacro/clash/tunnel"
)

View File

@@ -4,11 +4,11 @@ import (
"sort"
"strings"
"github.com/Dreamacro/clash/adapter"
"github.com/dlclark/regexp2"
"github.com/Dreamacro/clash/adapters/outbound"
"github.com/Dreamacro/clash/adapters/outboundgroup"
"github.com/Dreamacro/clash/adapters/provider"
"github.com/Dreamacro/clash/adapter/outboundgroup"
"github.com/Dreamacro/clash/adapter/provider"
C "github.com/Dreamacro/clash/constant"
"github.com/Dreamacro/clash/log"
"github.com/Dreamacro/clash/tunnel"
@@ -60,7 +60,7 @@ func QueryProxyGroupNames(excludeNotSelectable bool) []string {
return []string{}
}
global := tunnel.Proxies()["GLOBAL"].(*outbound.Proxy).ProxyAdapter.(outboundgroup.ProxyGroup)
global := tunnel.Proxies()["GLOBAL"].(*adapter.Proxy).ProxyAdapter.(outboundgroup.ProxyGroup)
proxies := global.Providers()[0].Proxies()
result := make([]string, 0, len(proxies)+1)
@@ -69,7 +69,7 @@ func QueryProxyGroupNames(excludeNotSelectable bool) []string {
}
for _, p := range proxies {
if _, ok := p.(*outbound.Proxy).ProxyAdapter.(outboundgroup.ProxyGroup); ok {
if _, ok := p.(*adapter.Proxy).ProxyAdapter.(outboundgroup.ProxyGroup); ok {
if !excludeNotSelectable || p.Type() == C.Selector {
result = append(result, p.Name())
}
@@ -88,7 +88,7 @@ func QueryProxyGroup(name string, sortMode SortMode, uiSubtitlePattern *regexp2.
return nil
}
g, ok := p.(*outbound.Proxy).ProxyAdapter.(outboundgroup.ProxyGroup)
g, ok := p.(*adapter.Proxy).ProxyAdapter.(outboundgroup.ProxyGroup)
if !ok {
log.Warnln("Query group `%s`: invalid type %s", name, p.Type().String())
@@ -136,7 +136,7 @@ func PatchSelector(selector, name string) bool {
return false
}
g, ok := p.(*outbound.Proxy).ProxyAdapter.(outboundgroup.ProxyGroup)
g, ok := p.(*adapter.Proxy).ProxyAdapter.(outboundgroup.ProxyGroup)
if !ok {
log.Warnln("Patch selector `%s`: invalid type %s", selector, p.Type().String())
@@ -171,7 +171,7 @@ func collectProviders(providers []provider.ProxyProvider, uiSubtitlePattern *reg
subtitle := px.Type().String()
if uiSubtitlePattern != nil {
if _, ok := px.(*outbound.Proxy).ProxyAdapter.(outboundgroup.ProxyGroup); !ok {
if _, ok := px.(*adapter.Proxy).ProxyAdapter.(outboundgroup.ProxyGroup); !ok {
runes := []rune(name)
match, err := uiSubtitlePattern.FindRunesMatch(runes)
if err == nil && match != nil {

View File

@@ -1,6 +1,6 @@
package tunnel
import "github.com/Dreamacro/clash/adapters/provider"
import "github.com/Dreamacro/clash/adapter/provider"
func Suspend(s bool) {
provider.Suspend(s)

View File

@@ -62,10 +62,11 @@ object Clash {
fd: Int,
mtu: Int,
dns: String,
blocking: String,
markSocket: (Int) -> Boolean,
querySocketUid: (protocol: Int, source: InetSocketAddress, target: InetSocketAddress) -> Int
) {
Bridge.nativeStartTun(fd, mtu, dns, object : TunInterface {
Bridge.nativeStartTun(fd, mtu, dns, blocking, object : TunInterface {
override fun markSocket(fd: Int) {
markSocket(fd)
}

View File

@@ -17,7 +17,7 @@ object Bridge {
external fun nativeQueryTrafficTotal(): Long
external fun nativeNotifyDnsChanged(dnsList: String)
external fun nativeNotifyInstalledAppChanged(uidList: String)
external fun nativeStartTun(fd: Int, mtu: Int, dns: String, cb: TunInterface)
external fun nativeStartTun(fd: Int, mtu: Int, dns: String, blocking: String, cb: TunInterface)
external fun nativeStopTun()
external fun nativeStartHttp(listenAt: String): String?
external fun nativeStopHttp()

View File

@@ -58,7 +58,7 @@ dependencies {
api(project(":service"))
implementation(kotlin("stdlib-jdk7"))
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutineVersion")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutineVersion")
implementation("androidx.core:core-ktx:$coreVersion")
implementation("androidx.appcompat:appcompat:$appcompatVersion")
implementation("androidx.activity:activity:$activityVersion")

View File

@@ -70,6 +70,13 @@ class NetworkSettingsDesign(
configure = vpnDependencies::add,
)
switch(
value = srvStore::blockLoopback,
title = R.string.block_loopback,
summary = R.string.block_loopback_summary,
configure = vpnDependencies::add,
)
if (Build.VERSION.SDK_INT >= 29) {
switch(
value = srvStore::systemProxy,

View File

@@ -211,4 +211,6 @@
<string name="sources">源代碼</string>
<string name="clash_core">Clash 核心</string>
<string name="name_server_policy">Name Server 策略</string>
<string name="block_loopback">阻止本地迴環</string>
<string name="block_loopback_summary">阻止本地迴環連接</string>
</resources>

View File

@@ -1,95 +1,95 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="access_control_packages_summary">應用配置訪問權限</string>
<string name="access_control_packages_summary">軟體設定訪問權限</string>
<string name="about">關於</string>
<string name="access_control_mode">訪問控制模式</string>
<string name="access_control_packages">訪問控制應用包列表</string>
<string name="append_system_dns">追加系統 DNS</string>
<string name="application_broken">應用損壞</string>
<string name="access_control_packages">訪問控制軟體套件清單</string>
<string name="append_system_dns">追加作業系統 DNS</string>
<string name="application_broken">軟體損毀</string>
<string name="application_name">Clash for Android</string>
<string name="auto_update">自動更新</string>
<string name="behavior">行為</string>
<string name="bypass_private_network">繞過私有網</string>
<string name="bypass_private_network_summary">繞過私有網絡地</string>
<string name="bypass_private_network">繞過私有網</string>
<string name="bypass_private_network_summary">繞過私有網路位</string>
<string name="cancel">取消</string>
<string name="clash_logcat">Clash 日誌捕捉工具</string>
<string name="create_profile">創建配置</string>
<string name="dark_mode">暗黑模式</string>
<string name="default_">默認</string>
<string name="delay"></string>
<string name="clash_logcat">Clash 日誌檔採集工具</string>
<string name="create_profile">建立設定檔</string>
<string name="dark_mode">深色模式</string>
<string name="default_">預設</string>
<string name="delay"></string>
<string name="delete">刪除</string>
<string name="delete_all_logs">刪除所有日誌</string>
<string name="delete_all_logs_warn">所有歷史日誌將丟失</string>
<string name="detail">詳情</string>
<string name="delete_all_logs">刪除所有日誌</string>
<string name="delete_all_logs_warn">所有歷史日誌將丟失</string>
<string name="detail">內容</string>
<string name="direct_mode">直連模式</string>
<string name="disabled">已禁用</string>
<string name="dns_hijacking">DNS 劫持</string>
<string name="dns_hijacking_summary">處理所有 DNS 數據</string>
<string name="dns_hijacking_summary">處理所有 DNS </string>
<string name="duplicate">複製</string>
<string name="edit">編輯</string>
<string name="empty_name">空名稱</string>
<string name="exit_without_save">退出而不</string>
<string name="empty_name">名稱</string>
<string name="exit_without_save">退出而不</string>
<string name="exit_without_save_warning">所有變更將會丟失</string>
<string name="export"></string>
<string name="export"></string>
<string name="external">外部</string>
<string name="file">文件</string>
<string name="file_exported">文件已導</string>
<string name="file">檔案</string>
<string name="file_exported">檔案已匯</string>
<string name="format_minutes">%d 分鐘</string>
<string name="format_profile_activated">%s 已激活</string>
<string name="format_profile_activated">%s 已啟用</string>
<string name="format_traffic_forwarded">%s 已轉發</string>
<string name="global_mode">模式</string>
<string name="global_mode">域性模式</string>
<string name="history">歷史</string>
<string name="import_from_file">文件導</string>
<string name="import_from_url">從 URL </string>
<string name="interface_"></string>
<string name="invalid_url">無效 URL</string>
<string name="import_from_file">檔案匯</string>
<string name="import_from_url">從 URL </string>
<string name="interface_"></string>
<string name="invalid_url">無效 URL</string>
<string name="launch_name">Clash</string>
<string name="logcat">Logcat</string>
<string name="logs">日誌</string>
<string name="mode">模式</string>
<string name="name">名稱</string>
<string name="network"></string>
<string name="new_profile">配置</string>
<string name="not_selected">未選</string>
<string name="network"></string>
<string name="new_profile">設定檔</string>
<string name="not_selected">未選</string>
<string name="ok">確認</string>
<string name="profile">配置</string>
<string name="profile_name">配置名稱</string>
<string name="profiles">配置</string>
<string name="profile">設定檔</string>
<string name="profile_name">設定檔名稱</string>
<string name="profiles">設定檔</string>
<string name="properties">參數</string>
<string name="proxy">代理</string>
<string name="recently"></string>
<string name="route_system_traffic">自動路由系統流量</string>
<string name="routing_via_vpn_service">通過 VpnService 自動路由所有系統流量</string>
<string name="proxy">Proxy</string>
<string name="recently"></string>
<string name="route_system_traffic">自動轉發作業系統流量</string>
<string name="routing_via_vpn_service">通過 VpnService 自動轉發所有作業系統流量</string>
<string name="rule_mode">規則模式</string>
<string name="running"></string>
<string name="settings"></string>
<string name="running"></string>
<string name="settings"></string>
<string name="show_traffic">顯示流量</string>
<string name="show_traffic_summary">在通知中自動刷新流量</string>
<string name="allow_clash_auto_restart">允許 Clash 自動重</string>
<string name="auto_restart">自動重</string>
<string name="show_traffic_summary">在通知中自動重新整理流量</string>
<string name="allow_clash_auto_restart">允許 Clash 自動重新啟動</string>
<string name="auto_restart">自動重新啟動</string>
<string name="stopped">已停止</string>
<string name="help">幫助</string>
<string name="tap_to_start">點此啟動</string>
<string name="update">更新</string>
<string name="url">URL</string>
<string name="vpn_service_options">VpnService 選項</string>
<string name="options_unavailable">選項在 Clash 運時不可用</string>
<string name="search"></string>
<string name="system_apps">系統應用</string>
<string name="options_unavailable">選項在 Clash 運時不可用</string>
<string name="search"></string>
<string name="system_apps">作業系統軟體</string>
<string name="update_time">更新時間</string>
<string name="package_name">應用包名稱</string>
<string name="package_name">軟體套件名稱</string>
<string name="install_time">安裝時間</string>
<string name="clash_for_android">Clash for Android</string>
<string name="feedback"></string>
<string name="feedback"></string>
<string name="github_issues">Github Issues</string>
<string name="tips_properties"><![CDATA[僅接受 <strong>Clash 配置文件</strong>(包含<strong>代理</strong>/<strong>規則</strong>)]]></string>
<string name="tips_properties"><![CDATA[僅接受 <strong>Clash 設定檔</strong>(包含<strong>Proxy</strong>/<strong>規則</strong>)]]></string>
<string name="loading">載入中</string>
<string name="tips_help"><![CDATA[Clash for Android 是一個<strong>免費軟</strong>並且我們<strong>不</strong>為其提供任何服務, <strong>請務必不要反饋非應用自身引起的問題</strong>]]></string>
<string name="donate">捐贈</string>
<string name="allow_all_apps">允許所有應用</string>
<string name="allow_selected_apps">僅允許已選擇的應用</string>
<string name="deny_selected_apps">不允許已選擇的應用</string>
<string name="no_profile_selected">沒有選擇配置文件</string>
<string name="tips_help"><![CDATA[Clash for Android 是一個<strong>免費軟</strong>並且我們<strong>不</strong>為其提供任何服務, <strong>請務必不要回報非軟體自身引起的問題</strong>]]></string>
<string name="donate">抖內</string>
<string name="allow_all_apps">允許所有軟體</string>
<string name="allow_selected_apps">僅允許已選取的軟體</string>
<string name="deny_selected_apps">不允許已選取的軟體</string>
<string name="no_profile_selected">沒有選取設定檔</string>
<string name="copied">已複製</string>
<string name="script_mode">腳本模式</string>
<string name="google_play">Google Play</string>
@@ -97,22 +97,22 @@
<string name="select_all">全選</string>
<string name="select_invert">反選</string>
<string name="select_none">清除</string>
<string name="app">應用</string>
<string name="follow_system_android_10">跟隨系統 (Android 10+)</string>
<string name="always_dark">總是暗黑模式</string>
<string name="always_light">總是明亮模式</string>
<string name="app">軟體</string>
<string name="follow_system_android_10">跟隨作業系統 (Android 10+)</string>
<string name="always_dark">總是深色模式</string>
<string name="always_light">總是淺色模式</string>
<string name="service">服務</string>
<string name="accept_http_content">僅接受 http(s) 和 content 類型</string>
<string name="at_least_15_minutes">至少 15 分鐘</string>
<string name="override">覆寫</string>
<string name="general">常規</string>
<string name="general">一般</string>
<string name="dns">DNS</string>
<string name="http_port">HTTP 端口</string>
<string name="socks_port">Socks 端口</string>
<string name="mixed_port">複合端口</string>
<string name="allow_lan">允許來自域網的連</string>
<string name="bind_address">監聽</string>
<string name="log_level">日誌級別</string>
<string name="http_port">HTTP </string>
<string name="socks_port">Socks </string>
<string name="mixed_port">複合</string>
<string name="allow_lan">允許來自域網的連</string>
<string name="bind_address">監聽</string>
<string name="log_level">日誌級別</string>
<string name="ipv6">IPv6</string>
<string name="hosts">Hosts</string>
<string name="enabled">已啟用</string>
@@ -130,85 +130,87 @@
<string name="fakeip_filter">FakeIP 過濾器</string>
<string name="geoip_fallback">GeoIP Fallback</string>
<string name="ipcidr_fallback">IPCIDR Fallback</string>
<string name="use_built_in">使用內</string>
<string name="use_built_in">使用內</string>
<string name="mapping">Real-IP 至 域名映射</string>
<string name="fakeip">Fake-IP 至 域名映射</string>
<string name="sort">排序</string>
<string name="layout">佈局</string>
<string name="single"></string>
<string name="multiple"></string>
<string name="not_selectable">不可選</string>
<string name="single"></string>
<string name="multiple"></string>
<string name="not_selectable">不可選</string>
<string name="providers">外部資源</string>
<string name="unavailable">不可用</string>
<string name="_new"></string>
<string name="_new"></string>
<string name="value"></string>
<string name="listen">監聽</string>
<string name="files">文件</string>
<string name="browse_files">瀏覽文件</string>
<string name="browse_configuration_providers">瀏覽配置文件和外部資源</string>
<string name="rename">重命名</string>
<string name="file_name">文件</string>
<string name="format_type_unsaved">%s (未存)</string>
<string name="files">檔案</string>
<string name="browse_files">瀏覽檔案</string>
<string name="browse_configuration_providers">瀏覽設定檔和外部資源</string>
<string name="rename">命名</string>
<string name="file_name"></string>
<string name="format_type_unsaved">%s (未存)</string>
<string name="format_minutes_ago">%d 分鐘前</string>
<string name="format_hours_ago">%d 小時前</string>
<string name="format_days_ago">%d 天前</string>
<string name="format_months_ago">%d 月前</string>
<string name="format_years_ago">%d 年前</string>
<string name="system_proxy">系統代理</string>
<string name="system_proxy_summary">為 VpnService 附加 HTTP 代理</string>
<string name="system_proxy">作業系統 Proxy</string>
<string name="system_proxy_summary">為 VpnService 附加 HTTP Proxy</string>
<string name="dont_modify">不修改</string>
<string name="redirect_port">Redirect 端口</string>
<string name="tproxy_port">TProxy 端口</string>
<string name="reset"></string>
<string name="redirect_port">Redirect </string>
<string name="tproxy_port">TProxy </string>
<string name="reset"></string>
<string name="use_hosts">使用 Hosts</string>
<string name="authentication">認證</string>
<string name="domain_fallback">域名 Fallback</string>
<string name="empty">置空</string>
<string name="import_from_clipboard">從剪切板導</string>
<string name="export_to_clipboard">出至剪切板</string>
<string name="import_from_clipboard">從剪貼簿匯</string>
<string name="export_to_clipboard">出至剪貼簿</string>
<string name="auto_update_minutes">自動更新 (分鐘)</string>
<string name="profile_url">配置 URL</string>
<string name="profile_url">設定檔 URL</string>
<string name="should_not_be_blank">不能為空</string>
<string name="format_fetching_configuration">正在從 %s 下載配置文件</string>
<string name="format_fetching_configuration">正在從 %s 下載設定檔</string>
<string name="format_fetching_provider">正在下載外部資源 %s</string>
<string name="initializing">正在初始化</string>
<string name="verifying">正在校驗</string>
<string name="sideload_geoip">載 GEOIP</string>
<string name="sideload_geoip_summary">外部 GEOIP 數據</string>
<string name="sideload_geoip">旁載 GEOIP</string>
<string name="sideload_geoip_summary">外部 GEOIP 資料</string>
<string name="force_enable">強制啟用</string>
<string name="document"></string>
<string name="document"></string>
<string name="clash_wiki">Clash Wiki</string>
<string name="invalid_file_name">非法的文件名稱</string>
<string name="reset_override_settings">重置覆寫設</string>
<string name="reset_override_settings_message">所有的覆寫設將會被</string>
<string name="invalid_file_name">不規範檔案名稱</string>
<string name="reset_override_settings">重置覆寫設</string>
<string name="reset_override_settings_message">所有的覆寫設將會被</string>
<string name="key"></string>
<string name="more">更多</string>
<string name="save"></string>
<string name="delay_test">測試</string>
<string name="save"></string>
<string name="delay_test">測試</string>
<string name="format_provider_type">%1$s(%2$s)</string>
<string name="rule">規則</string>
<string name="http">HTTP</string>
<string name="compatible"></string>
<string name="compatible"></string>
<string name="format_update_provider_failure">更新 %1$s: %2$s</string>
<string name="update_all">更新全部</string>
<string name="proxy_empty_tips">沒有可以顯示的組</string>
<string name="proxy_empty_tips">沒有可以顯示的</string>
<string name="reverse">反轉</string>
<string name="close">關閉</string>
<string name="keyword">關鍵</string>
<string name="invalid_log_file">無效的日誌文件</string>
<string name="application_crashed">應用崩潰</string>
<string name="application_broken_tips">應用缺少必要的運行組件,這通常由於下載不完整的 apk 導致。</string>
<string name="keyword">關鍵</string>
<string name="invalid_log_file">無效的日誌</string>
<string name="application_crashed">軟體崩潰</string>
<string name="application_broken_tips">軟體缺少必要的運作元件,這通常由於下載不完整的 apk 導致。</string>
<string name="reinstall">重新安裝</string>
<string name="github_releases">Github Releases</string>
<string name="unable_to_start_vpn">無法啟動 VPN </string>
<string name="request_donate_tips">如果您覺得本應用對您有幫助歡迎在 [幫助] 中給予開發者一點捐贈</string>
<string name="request_donate">捐贈</string>
<string name="version_updated">應用已更新</string>
<string name="version_updated_tips">已被清除,舊的配置文件需要再次存。</string>
<string name="active_unsaved_tips">配置文件需要在激活之前</string>
<string name="mode_switch_tips">僅在本次會話中有效</string>
<string name="import_"></string>
<string name="sources">源代</string>
<string name="unable_to_start_vpn">無法啟動 VPN </string>
<string name="request_donate_tips">如果您覺得本軟體對您有幫助歡迎在 [幫助] 中給予開發者一點抖內</string>
<string name="request_donate">抖內</string>
<string name="version_updated">軟體已更新</string>
<string name="version_updated_tips">已被清除,舊版設定檔需要再次存。</string>
<string name="active_unsaved_tips">設定檔需要在啟用之前</string>
<string name="mode_switch_tips">僅在本次工作階段中有效</string>
<string name="import_"></string>
<string name="sources">原始</string>
<string name="clash_core">Clash 核心</string>
<string name="name_server_policy">Name Server 策略</string>
</resources>
<string name="block_loopback">阻止本地迴環</string>
<string name="block_loopback_summary">阻止本地迴環連結</string>
</resources>

View File

@@ -211,4 +211,6 @@
<string name="sources">源代码</string>
<string name="clash_core">Clash 核心</string>
<string name="name_server_policy">Name Server 策略</string>
<string name="block_loopback">阻止本地回环</string>
<string name="block_loopback_summary">阻止本地回环连接</string>
</resources>

View File

@@ -119,6 +119,8 @@
<string name="bypass_private_network_summary">Bypass private network addresses</string>
<string name="dns_hijacking">DNS Hijacking</string>
<string name="dns_hijacking_summary">Handle all dns packet</string>
<string name="block_loopback">Block Loopback</string>
<string name="block_loopback_summary">Block loopback connections</string>
<string name="system_proxy">System Proxy</string>
<string name="system_proxy_summary">Attach http proxy to VpnService</string>
<string name="access_control_mode">Access Control Mode</string>

View File

@@ -238,7 +238,7 @@ class OverrideSettingsDesign(
summary = R.string.sideload_geoip_summary
) {
clicked {
requests.offer(Request.EditSideloadGeoip)
requests.trySend(Request.EditSideloadGeoip)
}
}
@@ -379,7 +379,7 @@ class OverrideSettingsDesign(
placeholder = R.string.dont_modify,
configure = dnsDependencies::add,
)
editableTextMap(
value = configuration.dns::nameserverPolicy,
keyAdapter = TextAdapter.String,
@@ -396,6 +396,6 @@ class OverrideSettingsDesign(
}
fun requestClear() {
requests.offer(Request.ResetOverride)
requests.trySend(Request.ResetOverride)
}
}

View File

@@ -32,51 +32,51 @@ class ProxyMenu(
R.id.not_selectable -> {
uiStore.proxyExcludeNotSelectable = item.isChecked
requests.offer(ProxyDesign.Request.ReLaunch)
requests.trySend(ProxyDesign.Request.ReLaunch)
}
R.id.single -> {
uiStore.proxySingleLine = true
updateConfig()
requests.offer(ProxyDesign.Request.ReloadAll)
requests.trySend(ProxyDesign.Request.ReloadAll)
}
R.id.multiple -> {
uiStore.proxySingleLine = false
updateConfig()
requests.offer(ProxyDesign.Request.ReloadAll)
requests.trySend(ProxyDesign.Request.ReloadAll)
}
R.id.default_ -> {
uiStore.proxySort = ProxySort.Default
requests.offer(ProxyDesign.Request.ReloadAll)
requests.trySend(ProxyDesign.Request.ReloadAll)
}
R.id.name -> {
uiStore.proxySort = ProxySort.Title
requests.offer(ProxyDesign.Request.ReloadAll)
requests.trySend(ProxyDesign.Request.ReloadAll)
}
R.id.delay -> {
uiStore.proxySort = ProxySort.Delay
requests.offer(ProxyDesign.Request.ReloadAll)
requests.trySend(ProxyDesign.Request.ReloadAll)
}
R.id.dont_modify -> {
requests.offer(ProxyDesign.Request.PatchMode(null))
requests.trySend(ProxyDesign.Request.PatchMode(null))
}
R.id.direct_mode -> {
requests.offer(ProxyDesign.Request.PatchMode(TunnelState.Mode.Direct))
requests.trySend(ProxyDesign.Request.PatchMode(TunnelState.Mode.Direct))
}
R.id.global_mode -> {
requests.offer(ProxyDesign.Request.PatchMode(TunnelState.Mode.Global))
requests.trySend(ProxyDesign.Request.PatchMode(TunnelState.Mode.Global))
}
R.id.rule_mode -> {
requests.offer(ProxyDesign.Request.PatchMode(TunnelState.Mode.Rule))
requests.trySend(ProxyDesign.Request.PatchMode(TunnelState.Mode.Rule))
}
R.id.script_mode -> {
requests.offer(ProxyDesign.Request.PatchMode(TunnelState.Mode.Script))
requests.trySend(ProxyDesign.Request.PatchMode(TunnelState.Mode.Script))
}
else -> return false
}

2
kaidl

Submodule kaidl updated: 963190ac8e...16da2e83b7

View File

@@ -48,15 +48,6 @@ android {
kotlinOptions {
jvmTarget = "1.8"
}
sourceSets {
named("debug") {
java.srcDir(buildDir.resolve("generated/ksp/debug/kotlin"))
}
named("release") {
java.srcDir(buildDir.resolve("generated/ksp/release/kotlin"))
}
}
}
dependencies {
@@ -66,12 +57,23 @@ dependencies {
api(project(":core"))
api(project(":common"))
implementation(project(":kaidl:kaidl-runtime")) {
exclude(group = "org.jetbrains.kotlinx", module = "kotlinx-coroutines-core")
}
implementation(kotlin("stdlib-jdk7"))
implementation(project(":kaidl:kaidl-runtime"))
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutineVersion")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutineVersion")
implementation("androidx.room:room-runtime:$roomVersion")
implementation("androidx.room:room-ktx:$roomVersion")
implementation("androidx.core:core-ktx:$coreVersion")
implementation("dev.rikka.rikkax.preference:multiprocess:$muiltprocessVersion")
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:$serializationVersion")
}
afterEvaluate {
android {
libraryVariants.forEach {
sourceSets[it.name].java.srcDir(buildDir.resolve("generated/ksp/${it.name}/kotlin"))
}
}
}

View File

@@ -25,11 +25,7 @@
</intent-filter>
</service>
<service
android:name=".ClashManager"
android:exported="false"
android:process=":background" />
<service
android:name=".ProfileService"
android:name=".RemoteService"
android:exported="false"
android:process=":background" />
<service

View File

@@ -1,7 +1,6 @@
package com.github.kr328.clash.service
import android.content.Intent
import android.os.IBinder
import android.content.Context
import com.github.kr328.clash.common.log.Log
import com.github.kr328.clash.core.Clash
import com.github.kr328.clash.core.model.*
@@ -9,22 +8,16 @@ import com.github.kr328.clash.service.data.Selection
import com.github.kr328.clash.service.data.SelectionDao
import com.github.kr328.clash.service.remote.IClashManager
import com.github.kr328.clash.service.remote.ILogObserver
import com.github.kr328.clash.service.remote.wrap
import com.github.kr328.clash.service.store.ServiceStore
import com.github.kr328.clash.service.util.sendOverrideChanged
import kotlinx.coroutines.*
import kotlinx.coroutines.channels.ReceiveChannel
import java.util.*
class ClashManager : BaseService(), IClashManager {
private val store by lazy { ServiceStore(this) }
private val binder = this.wrap()
class ClashManager(private val context: Context) : IClashManager,
CoroutineScope by CoroutineScope(Dispatchers.IO) {
private val store = ServiceStore(context)
private var logReceiver: ReceiveChannel<LogMessage>? = null
override fun onBind(intent: Intent?): IBinder {
return binder
}
override fun queryTunnelState(): TunnelState {
return Clash.queryTunnelState()
}
@@ -68,7 +61,7 @@ class ClashManager : BaseService(), IClashManager {
override fun patchOverride(slot: Clash.OverrideSlot, configuration: ConfigurationOverride) {
Clash.patchOverride(slot, configuration)
sendOverrideChanged()
context.sendOverrideChanged()
}
override fun clearOverride(slot: Clash.OverrideSlot) {

View File

@@ -1,7 +1,6 @@
package com.github.kr328.clash.service
import android.content.Intent
import android.os.IBinder
import android.content.Context
import com.github.kr328.clash.service.data.Database
import com.github.kr328.clash.service.data.ImportedDao
import com.github.kr328.clash.service.data.Pending
@@ -9,34 +8,27 @@ import com.github.kr328.clash.service.data.PendingDao
import com.github.kr328.clash.service.model.Profile
import com.github.kr328.clash.service.remote.IFetchObserver
import com.github.kr328.clash.service.remote.IProfileManager
import com.github.kr328.clash.service.remote.wrap
import com.github.kr328.clash.service.store.ServiceStore
import com.github.kr328.clash.service.util.directoryLastModified
import com.github.kr328.clash.service.util.generateProfileUUID
import com.github.kr328.clash.service.util.importedDir
import com.github.kr328.clash.service.util.pendingDir
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.io.FileNotFoundException
import java.util.*
class ProfileService : BaseService(), IProfileManager {
private val service = this
private val store by lazy { ServiceStore(this) }
private val binder = this.wrap()
override fun onBind(intent: Intent?): IBinder {
return binder
}
override fun onCreate() {
super.onCreate()
Database.database //.init
class ProfileManager(private val context: Context) : IProfileManager,
CoroutineScope by CoroutineScope(Dispatchers.IO) {
private val store = ServiceStore(context)
init {
launch {
ProfileReceiver.rescheduleAll(service)
Database.database //.init
ProfileReceiver.rescheduleAll(context)
}
}
@@ -52,7 +44,7 @@ class ProfileService : BaseService(), IProfileManager {
PendingDao().insert(pending)
pendingDir.resolve(uuid.toString()).apply {
context.pendingDir.resolve(uuid.toString()).apply {
deleteRecursively()
mkdirs()
@@ -119,21 +111,21 @@ class ProfileService : BaseService(), IProfileManager {
}
override suspend fun commit(uuid: UUID, callback: IFetchObserver?) {
ProfileProcessor.apply(service, uuid, callback)
ProfileProcessor.apply(context, uuid, callback)
scheduleUpdate(uuid, false)
}
override suspend fun release(uuid: UUID) {
ProfileProcessor.release(this, uuid)
ProfileProcessor.release(context, uuid)
}
override suspend fun delete(uuid: UUID) {
ImportedDao().queryByUUID(uuid)?.also {
ProfileReceiver.cancelNext(service, it)
ProfileReceiver.cancelNext(context, it)
}
ProfileProcessor.delete(service, uuid)
ProfileProcessor.delete(context, uuid)
}
override suspend fun queryByUUID(uuid: UUID): Profile? {
@@ -159,7 +151,7 @@ class ProfileService : BaseService(), IProfileManager {
}
override suspend fun setActive(profile: Profile) {
ProfileProcessor.active(this, profile.uuid)
ProfileProcessor.active(context, profile.uuid)
}
private suspend fun resolveProfile(uuid: UUID): Profile? {
@@ -186,14 +178,14 @@ class ProfileService : BaseService(), IProfileManager {
}
private fun resolveUpdatedAt(uuid: UUID): Long {
return pendingDir.resolve(uuid.toString()).directoryLastModified
?: importedDir.resolve(uuid.toString()).directoryLastModified
return context.pendingDir.resolve(uuid.toString()).directoryLastModified
?: context.importedDir.resolve(uuid.toString()).directoryLastModified
?: -1
}
private fun cloneImportedFiles(source: UUID, target: UUID = source) {
val s = importedDir.resolve(source.toString())
val t = pendingDir.resolve(target.toString())
val s = context.importedDir.resolve(source.toString())
val t = context.pendingDir.resolve(target.toString())
if (!s.exists())
throw FileNotFoundException("profile $source not found")
@@ -207,9 +199,9 @@ class ProfileService : BaseService(), IProfileManager {
val imported = ImportedDao().queryByUUID(uuid) ?: return
if (startImmediately) {
ProfileReceiver.schedule(service, imported)
ProfileReceiver.schedule(context, imported)
} else {
ProfileReceiver.scheduleNext(service, imported)
ProfileReceiver.scheduleNext(context, imported)
}
}
}

View File

@@ -14,7 +14,6 @@ import com.github.kr328.clash.common.compat.pendingIntentFlags
import com.github.kr328.clash.common.constants.Components
import com.github.kr328.clash.common.constants.Intents
import com.github.kr328.clash.common.id.UndefinedIds
import com.github.kr328.clash.common.log.Log
import com.github.kr328.clash.common.util.setUUID
import com.github.kr328.clash.common.util.uuid
import com.github.kr328.clash.service.data.ImportedDao
@@ -148,17 +147,12 @@ class ProfileWorker : BaseService() {
NotificationManagerCompat.from(applicationContext)
.notify(id, notification)
Log.d("notify processing $name: id = $id")
try {
block()
} finally {
withContext(NonCancellable) {
NotificationManagerCompat.from(applicationContext)
.cancel(id)
Log.d("notify processed $name: id = $id")
}
}
}
@@ -190,8 +184,6 @@ class ProfileWorker : BaseService() {
NotificationManagerCompat.from(this)
.notify(id, notification)
Log.d("notify completed $name: id = $id")
}
private fun failed(uuid: UUID, name: String, reason: String) {
@@ -207,8 +199,6 @@ class ProfileWorker : BaseService() {
NotificationManagerCompat.from(this)
.notify(id, notification)
Log.d("notify failed $name: id = $id")
}
companion object {

View File

@@ -0,0 +1,46 @@
package com.github.kr328.clash.service
import android.content.Intent
import android.os.IBinder
import com.github.kr328.clash.service.remote.IClashManager
import com.github.kr328.clash.service.remote.IRemoteService
import com.github.kr328.clash.service.remote.IProfileManager
import com.github.kr328.clash.service.remote.wrap
import com.github.kr328.clash.service.util.cancelAndJoinBlocking
class RemoteService : BaseService(), IRemoteService {
private val binder = this.wrap()
private var clash: ClashManager? = null
private var profile: ProfileManager? = null
private var clashBinder: IClashManager? = null
private var profileBinder: IProfileManager? = null
override fun onCreate() {
super.onCreate()
clash = ClashManager(this)
profile = ProfileManager(this)
clashBinder = clash?.wrap() as IClashManager?
profileBinder = profile?.wrap() as IProfileManager?
}
override fun onDestroy() {
super.onDestroy()
clash?.cancelAndJoinBlocking()
profile?.cancelAndJoinBlocking()
}
override fun onBind(intent: Intent?): IBinder {
return binder
}
override fun clash(): IClashManager {
return clashBinder!!
}
override fun profile(): IProfileManager {
return profileBinder!!
}
}

View File

@@ -138,6 +138,9 @@ class TunService : VpnService(), CoroutineScope by CoroutineScope(Dispatchers.De
resources.getStringArray(R.array.bypass_private_route).map(::parseCIDR).forEach {
addRoute(it.ip, it.prefix)
}
// Route of virtual DNS
addRoute(TUN_DNS, 32)
} else {
addRoute(NET_ANY, 0)
}
@@ -213,10 +216,16 @@ class TunService : VpnService(), CoroutineScope by CoroutineScope(Dispatchers.De
}
}
val blocking = mutableListOf("$TUN_GATEWAY/$TUN_SUBNET_PREFIX")
if (store.blockLoopback) {
blocking.add(NET_SUBNET_LOOPBACK)
}
TunModule.TunDevice(
fd = establish()?.detachFd()
?: throw NullPointerException("Establish VPN rejected by system"),
mtu = TUN_MTU,
blocking = blocking.joinToString(";"),
dns = if (store.dnsHijacking) NET_ANY else TUN_DNS,
)
}
@@ -227,8 +236,9 @@ class TunService : VpnService(), CoroutineScope by CoroutineScope(Dispatchers.De
companion object {
private const val TUN_MTU = 9000
private const val TUN_SUBNET_PREFIX = 30
private const val TUN_GATEWAY = "172.31.255.253"
private const val TUN_DNS = "198.18.0.1"
private const val TUN_GATEWAY = "172.19.0.1"
private const val TUN_DNS = "172.19.0.2"
private const val NET_ANY = "0.0.0.0"
private const val NET_SUBNET_LOOPBACK = "127.0.0.0/8"
}
}

View File

@@ -16,7 +16,8 @@ class TunModule(private val vpn: VpnService) : Module<Unit>(vpn) {
data class TunDevice(
val fd: Int,
val mtu: Int,
val dns: String
val blocking: String,
val dns: String,
)
private val connectivity = service.getSystemService<ConnectivityManager>()!!
@@ -57,6 +58,7 @@ class TunModule(private val vpn: VpnService) : Module<Unit>(vpn) {
fd = device.fd,
mtu = device.mtu,
dns = device.dns,
blocking = device.blocking,
markSocket = vpn::protect,
querySocketUid = this::queryUid
)

View File

@@ -0,0 +1,9 @@
package com.github.kr328.clash.service.remote
import com.github.kr328.kaidl.BinderInterface
@BinderInterface
interface IRemoteService {
fun clash(): IClashManager
fun profile(): IProfileManager
}

View File

@@ -43,7 +43,12 @@ class ServiceStore(context: Context) {
var systemProxy by store.boolean(
key = "system_proxy",
defaultValue = false
defaultValue = true
)
var blockLoopback by store.boolean(
key = "block_loopback",
defaultValue = true
)
var dynamicNotification by store.boolean(

View File

@@ -1,19 +1,19 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="clash_service_status_channel">Clash 狀態</string>
<string name="running">正在運</string>
<string name="running">正在運</string>
<string name="format_update_complete">更新 %s 成功</string>
<string name="format_update_failure">"更新 %1$s: %2$s "</string>
<string name="clash_for_android">Clash for Android</string>
<string name="profiles_and_providers">配置文件和外部資源</string>
<string name="configuration_yaml">配置文件.yaml</string>
<string name="profiles_and_providers">設定檔和外部資源</string>
<string name="configuration_yaml">設定檔.yaml</string>
<string name="provider_files">外部資源文件列表</string>
<string name="loading">載入中</string>
<string name="profile_process_status">配置文件處理狀態</string>
<string name="profile_process_status">設定檔處理狀態</string>
<string name="update_successfully">更新成功</string>
<string name="update_failure">更新失敗</string>
<string name="profile_updater">配置更新服務</string>
<string name="profile_updating">配置更新中</string>
<string name="profile_service_status">配置文件服務狀態</string>
<string name="profile_process_result">配置文件處理結果</string>
</resources>
<string name="profile_updater">設定檔更新服務</string>
<string name="profile_updating">設定檔更新中</string>
<string name="profile_service_status">設定檔服務狀態</string>
<string name="profile_process_result">設定檔處理結果</string>
</resources>