Compare commits

...
53 Commits
Author SHA1 Message Date
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
kr328 ca85a688cc Chore: bump version 2021-05-24 19:45:09 +08:00
kr328 0e3ed548c9 Chore: update tun2socket 2021-05-24 19:44:41 +08:00
Kr328 27dc5395e0 Improve: migrate to latest dependencies 2021-05-24 16:15:06 +08:00
Kr328 4f05ba1ac6 Feature: Add workflow for push/PR checking (#1030)
* Chore: downgrade ndk version

* Fix: fix local.properties load

* Feature: add workflow for push/PR checking
2021-05-24 16:15:06 +08:00
kr328 1a36218c80 Feature: add nameserver-policy override 2021-05-24 16:15:06 +08:00
Goooler 3125b90efe Chore: update dependencies 2021-05-24 16:14:59 +08:00
Kr328 2a7425700a Chore: update tun2socket 2021-05-24 03:15:58 +08:00
kr328 f8a3308b46 Chore: bump version 2021-05-23 22:42:20 +08:00
kr328 93aed001af Chore: update clash core 2021-05-23 22:20:21 +08:00
kr328 44461b67ee Chore: update tun2socket 2021-05-23 18:45:15 +08:00
kr328 8a55dc1945 Fix: remove tun address from bypass_private_route 2021-05-22 19:59:12 +08:00
kr328 3af16bcf34 Chore: update tun2socket 2021-05-22 19:55:51 +08:00
kr328 9adf0f00ae Chore: update README.md 2021-05-22 19:54:34 +08:00
kr328 a2f3330455 Fix: force use cmake installed in system 2021-05-22 19:53:28 +08:00
kr328 7cfefbca19 Improve: use 'redir-host' instead of 'fake-ip' 2021-05-22 01:38:15 +08:00
kr328 e8eddb3917 Chore: remove unused logger 2021-05-22 01:30:53 +08:00
kr328 634c25894c Fix: should close all endpoint on exit 2021-05-22 01:29:25 +08:00
kr328 42f5331a0c Fix: should close TCP/UDP endpoint on exit 2021-05-22 01:26:02 +08:00
kr328 e2383df06b Improve: improve clash core build 2021-05-22 01:22:09 +08:00
Kr328 7381b787f3 Improve: use lwip stack instead of kernel 2021-05-21 21:02:22 +08:00
Kr328 eff310ac40 Chore: should ignore appcenter.key in foss build 2021-05-20 10:59:54 +08:00
kr328 c245fc6dfa Fix: use hardcoded components package name 2021-05-16 22:56:57 +08:00
kr328 b8f0c32ad5 Chore: remove migration error tracking 2021-05-16 20:00:47 +08:00
94 changed files with 1031 additions and 766 deletions

No files matched your search

+24
View File
@@ -0,0 +1,24 @@
name: Build Unsigned
on: [push, pull_request]
jobs:
BuildUnsigned:
runs-on: ubuntu-latest
steps:
- name: Checkout Repository
uses: actions/checkout@v2
with:
submodules: recursive
- name: Setup Java
uses: actions/setup-java@v1
with:
java-version: 11
- name: Setup Go
uses: actions/setup-go@v2
with:
go-version: 1.16
- name: Setup Android SDK
uses: android-actions/setup-android@v2
- name: Setup Cmake & Ninja
uses: lukka/get-cmake@latest
- name: Build
run: ./gradlew --no-daemon app:assembleRelease
+3
View File
@@ -4,3 +4,6 @@
[submodule "kaidl"]
path = kaidl
url = https://github.com/Kr328/kaidl.git
[submodule "core/src/main/golang/tun2socket"]
path = core/src/main/golang/tun2socket
url = https://github.com/Kr328/tun2socket-lwip.git
+2 -3
View File
@@ -36,13 +36,12 @@ See also [PRIVACY_POLICY.md](./PRIVACY_POLICY.md)
git submodule update --init --recursive
```
2. Install `JDK 1.8`, `Android SDK` and `Golang`
2. Install **OpenJDK 11**, **Android SDK**, **CMake** and **Golang**
3. Create `local.properties` in project root with
```properties
sdk.dir=/path/to/android-sdk
appcenter.key=<AppCenter Key> # Optional, from "appcenter.ms"
```
4. Create `keystore.properties` in project root with
@@ -57,7 +56,7 @@ See also [PRIVACY_POLICY.md](./PRIVACY_POLICY.md)
5. Build
```bash
./gradlew app:assembleRelease
./gradlew app:assembleFossRelease
```
6. Pick `app-release-<arch>.apk` in `app/build/outputs/apks`
+16 -7
View File
@@ -47,13 +47,22 @@ android {
dimension = "premium"
versionNameSuffix = ".premium"
val appCenterKey = rootProject.file("local.properties").inputStream()
.use { Properties().apply { load(it) } }
.getProperty("appcenter.key", null)
if (buildFlavor == "premium") {
val localFile = rootProject.file("local.properties")
if (localFile.exists()) {
val appCenterKey = localFile.inputStream()
.use { Properties().apply { load(it) } }
.getProperty("appcenter.key", null)
Objects.requireNonNull(appCenterKey)
buildConfigField("String", "APP_CENTER_KEY", "\"$appCenterKey\"")
if (appCenterKey != null) {
buildConfigField("String", "APP_CENTER_KEY", "\"$appCenterKey\"")
} else {
buildConfigField("String", "APP_CENTER_KEY", "null")
}
} else {
buildConfigField("String", "APP_CENTER_KEY", "null")
}
}
}
}
@@ -113,7 +122,7 @@ dependencies {
implementation(kotlin("stdlib-jdk7"))
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutineVersion")
implementation("androidx.core:core-ktx:$ktxVersion")
implementation("androidx.core:core-ktx:$coreVersion")
implementation("androidx.activity:activity:$activityVersion")
implementation("androidx.appcompat:appcompat:$appcompatVersion")
implementation("androidx.coordinatorlayout:coordinatorlayout:$coordinatorlayoutVersion")
+2 -1
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" />
@@ -24,8 +24,6 @@ class AppCrashedActivity : BaseActivity<AppCrashedDesign>() {
SystemLogcat.dumpCrash()
}
Tracker.uploadLogcat(logs)
design.setAppLogs(logs)
while (isActive) {
@@ -114,7 +114,7 @@ abstract class BaseActivity<D : Design<*>> :
Remote.broadcasts.addObserver(this)
events.offer(Event.ActivityStart)
events.trySend(Event.ActivityStart)
}
override fun onStop() {
@@ -124,7 +124,7 @@ abstract class BaseActivity<D : Design<*>> :
Remote.broadcasts.removeObserver(this)
events.offer(Event.ActivityStop)
events.trySend(Event.ActivityStop)
}
override fun onDestroy() {
@@ -174,23 +174,23 @@ abstract class BaseActivity<D : Design<*>> :
}
override fun onProfileChanged() {
events.offer(Event.ProfileChanged)
events.trySend(Event.ProfileChanged)
}
override fun onProfileLoaded() {
events.offer(Event.ProfileLoaded)
events.trySend(Event.ProfileLoaded)
}
override fun onServiceRecreated() {
events.offer(Event.ServiceRecreated)
events.trySend(Event.ServiceRecreated)
}
override fun onStarted() {
events.offer(Event.ClashStart)
events.trySend(Event.ClashStart)
}
override fun onStopped(cause: String?) {
events.offer(Event.ClashStop)
events.trySend(Event.ClashStop)
if (cause != null && activityStarted) {
launch {
@@ -141,7 +141,7 @@ class FilesActivity : BaseActivity<FilesDesign>() {
}
override fun onBackPressed() {
design?.requests?.offer(FilesDesign.Request.PopStack)
design?.requests?.trySend(FilesDesign.Request.PopStack)
}
private suspend fun FilesDesign.fetch(client: FilesClient, stack: Stack<String>, root: String) {
@@ -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 {
@@ -97,7 +97,7 @@ class LogcatService : Service(), CoroutineScope by CoroutineScope(Dispatchers.De
LogcatWriter(this@LogcatService).use {
val observer = object : ILogObserver {
override fun newItem(log: LogMessage) {
channel.offer(log)
channel.trySend(log)
}
}
@@ -47,7 +47,7 @@ class LogsActivity : BaseActivity<LogsDesign>() {
deleteAllLogs()
}
events.offer(Event.ActivityStart)
events.trySend(Event.ActivityStart)
}
}
is LogsDesign.Request.OpenFile -> {
@@ -32,4 +32,8 @@ class MainApplication : Application() {
sendServiceRecreated()
}
}
fun finalize() {
Global.destroy()
}
}
@@ -73,7 +73,7 @@ class ProxyActivity : BaseActivity<ProxyDesign>() {
}
ProxyDesign.Request.ReloadAll -> {
names.indices.forEach { idx ->
design.requests.offer(ProxyDesign.Request.Reload(idx))
design.requests.trySend(ProxyDesign.Request.Reload(idx))
}
}
is ProxyDesign.Request.Reload -> {
@@ -8,7 +8,8 @@ object SystemLogcat {
"Go",
"DEBUG",
"AndroidRuntime",
"ClashForAndroid"
"ClashForAndroid",
"LwIP",
)
fun dumpCrash(): String {
@@ -10,13 +10,12 @@ import com.github.kr328.clash.store.AppStore
import com.github.kr328.clash.util.ApplicationObserver
import com.github.kr328.clash.util.verifyApk
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.channels.Channel
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
@@ -30,9 +29,9 @@ object Remote {
fun launch() {
ApplicationObserver.attach(Global.application)
ApplicationObserver.onVisibleChanged(visible::offer)
ApplicationObserver.onVisibleChanged { visible.trySend(it) }
GlobalScope.launch(Dispatchers.IO) {
Global.launch(Dispatchers.IO) {
run()
}
}
@@ -57,10 +56,10 @@ object Remote {
while (true) {
if (visible.receive()) {
services.bind()
service.bind()
broadcasts.register()
} else {
services.unbind()
service.unbind()
broadcasts.unregister()
}
}
@@ -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)
}
}
@@ -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)
}
}
@@ -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)
}
}
}
+7 -17
View File
@@ -1,5 +1,5 @@
plugins {
kotlin("jvm") version "1.5.0"
kotlin("jvm") version "1.5.10"
`java-gradle-plugin`
}
@@ -9,27 +9,17 @@ 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 {
plugins {
create("golang") {
id = "library-golang"
implementationClass = "LibraryGolangPlugin"
id = "clash-build"
implementationClass = "com.github.kr328.clash.tools.ClashBuildPlugin"
}
}
}
+3 -3
View File
@@ -1,12 +1,12 @@
import org.gradle.api.Project
const val buildVersionCode = 204001
const val buildVersionName = "2.4.1"
const val buildVersionCode = 204007
const val buildVersionName = "2.4.7"
const val buildMinSdkVersion = 21
const val buildTargetSdkVersion = 30
const val buildNdkVersion = "23.0.7123448"
const val buildNdkVersion = "22.1.7171670"
val Project.buildFlavor: String
get() {
+4 -4
View File
@@ -1,13 +1,13 @@
const val activityVersion = "1.2.3"
const val coroutineVersion = "1.4.3"
const val coroutineVersion = "1.5.0"
const val roomVersion = "2.3.0"
const val ktxVersion = "1.3.2"
const val appcompatVersion = "1.2.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 serializationVersion = "1.2.1"
const val materialVersion = "1.3.0"
const val coordinatorlayoutVersion = "1.1.0"
const val recyclerviewVersion = "1.2.0"
const val fragmentVersion = "1.3.3"
const val fragmentVersion = "1.3.4"
const val viewpagerVersion = "1.0.0"
-121
View File
@@ -1,121 +0,0 @@
import org.apache.tools.ant.taskdefs.condition.Os
import org.gradle.api.DefaultTask
import org.gradle.api.GradleScriptException
import org.gradle.api.file.DirectoryProperty
import org.gradle.api.provider.Property
import org.gradle.api.provider.SetProperty
import org.gradle.api.tasks.*
import java.io.ByteArrayOutputStream
import java.io.File
import java.io.FileNotFoundException
import java.io.IOException
abstract class GolangBuildTask : DefaultTask() {
abstract val debug: Property<Boolean>
@Input get
abstract val premium: Property<Boolean>
@Input get
abstract val nativeAbis: SetProperty<String>
@Input get
abstract val minSdkVersion: Property<Int>
@Input get
abstract val cCompilerBasePath: DirectoryProperty
@InputDirectory get
abstract val inputDirectory: DirectoryProperty
@InputDirectory get
abstract val outputDirectory: DirectoryProperty
@OutputDirectory get
@TaskAction
fun build() {
val src = inputDirectory.get().asFile
val cmd = if (debug.get()) {
"""
go build --buildmode=c-shared -trimpath -o "%s" -tags "without_gvisor,without_system,debug${if (premium.get()) ",premium" else ""}"
""".trimIndent().trim()
} else {
"""
go build --buildmode=c-shared -trimpath -o "%s" -tags "without_gvisor,without_system${if (premium.get()) ",premium" else ""}" -ldflags "-s -w"
""".trimIndent().trim()
}
"go mod tidy".exec(pwd = src)
nativeAbis.get().parallelStream().forEach {
val out = outputDirectory.get().file("$it/libclash.so")
cmd.format(out).exec(pwd = src, env = generateGolangBuildEnvironment(it))
}
}
private fun generateGolangBuildEnvironment(abi: String): Map<String, String> {
val (goArch, goArm) = when (abi) {
"arm64-v8a" -> "arm64" to ""
"armeabi-v7a" -> "arm" to "7"
"x86" -> "386" to ""
"x86_64" -> "amd64" to ""
else -> throw UnsupportedOperationException("unsupported abi: $abi")
}
val compiler = when (abi) {
"armeabi-v7a" ->
"armv7a-linux-androideabi${minSdkVersion.get()}-clang"
"arm64-v8a" ->
"aarch64-linux-android${minSdkVersion.get()}-clang"
"x86" ->
"i686-linux-android${minSdkVersion.get()}-clang"
"x86_64" ->
"x86_64-linux-android${minSdkVersion.get()}-clang"
else ->
throw GradleScriptException(
"Unsupported abi $abi",
FileNotFoundException("Unsupported abi $abi")
)
}
return mapOf(
"CC" to cCompilerBasePath.get().asFile.resolve(compiler).absolutePath,
"GOOS" to "android",
"GOARCH" to goArch,
"GOARM" to goArm,
"CGO_ENABLED" to "1",
"CFLAGS" to "-O3 -Werror",
)
}
private fun String.exec(
pwd: File,
env: Map<String, String> = System.getenv()
): String {
val process = ProcessBuilder().run {
if (Os.isFamily(Os.FAMILY_WINDOWS))
command("cmd.exe", "/c", this@exec)
else
command("bash", "-c", this@exec)
environment().putAll(env)
directory(pwd)
redirectErrorStream(true)
start()
}
val outputStream = ByteArrayOutputStream()
process.inputStream.copyTo(outputStream)
if (process.waitFor() != 0) {
println(outputStream.toString("utf-8"))
throw GradleScriptException("Exec $this failure", IOException())
}
return outputStream.toString("utf-8")
}
}
@@ -1,69 +0,0 @@
import com.android.build.gradle.LibraryExtension
import org.apache.tools.ant.taskdefs.condition.Os
import org.gradle.api.GradleScriptException
import org.gradle.api.Plugin
import org.gradle.api.Project
import java.io.File
import java.io.FileNotFoundException
import java.util.*
class LibraryGolangPlugin : Plugin<Project> {
override fun apply(target: Project) {
target.extensions.getByType(LibraryExtension::class.java).apply {
target.afterEvaluate {
libraryVariants.forEach { variant ->
val abis = defaultConfig.externalNativeBuild.cmake.abiFilters +
defaultConfig.externalNativeBuild.ndkBuild.abiFilters
val nameCapitalize = variant.name.capitalize(Locale.getDefault())
val golangBuildDir = target.golangBuild.resolve(variant.name)
val task = target.tasks.register(
"externalGolangBuild$nameCapitalize",
GolangBuildTask::class.java
) {
it.premium.set(variant.flavorName == "premium")
it.debug.set(variant.name == "debug")
it.nativeAbis.set(abis)
it.minSdkVersion.set(defaultConfig.minSdk!!)
it.cCompilerBasePath.set(compilerBasePath)
it.inputDirectory.set(target.golangSource)
it.outputDirectory.set(golangBuildDir)
}
sourceSets.named(variant.name) {
it.jniLibs {
srcDir(golangBuildDir)
}
}
variant.externalNativeBuildProviders.forEach {
it.get().dependsOn(task)
}
target.tasks.filter { it.name.startsWith("buildCMake") }.forEach {
it.mustRunAfter(task)
}
}
}
}
}
private val LibraryExtension.compilerBasePath: File
get() {
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")
)
}
return ndkDirectory.resolve("toolchains/llvm/prebuilt/$host-x86_64/bin")
}
}
@@ -0,0 +1,23 @@
package com.github.kr328.clash.tools
import com.android.build.gradle.BaseExtension
import com.android.build.gradle.api.BaseVariant
import java.io.Serializable
data class BuildConfig(
val debug: Boolean,
val premium: Boolean,
val abis: List<NativeAbi>,
val minSdkVersion: Int,
) : Serializable {
companion object {
fun of(abis: List<NativeAbi>, minSdkVersion: Int, variant: BaseVariant): BuildConfig {
return BuildConfig(
debug = variant.buildType.isDebuggable,
premium = variant.flavorName == "premium",
abis = abis,
minSdkVersion = minSdkVersion
)
}
}
}
@@ -0,0 +1,56 @@
package com.github.kr328.clash.tools
import com.android.build.gradle.LibraryExtension
import golangBuild
import golangSource
import org.gradle.api.Plugin
import org.gradle.api.Project
import java.util.*
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(abis, minSdkVersion, variant)
val buildDir = target.golangBuild.resolve(variant.name)
val capitalize = variant.name.capitalize(Locale.getDefault())
val task = target.tasks.register(
"externalGolangBuild$capitalize",
ClashBuildTask::class.java
) {
it.config.set(config)
it.ndkDirectory.set(ndkDirectory)
it.inputDirectory.set(target.golangSource)
it.outputDirectory.set(buildDir)
}
sourceSets.named(variant.name) {
it.jniLibs {
srcDir(buildDir)
}
}
variant.externalNativeBuildProviders.forEach {
it.get().dependsOn(task)
}
target.tasks.filter { it.name.startsWith("buildCMake") }.forEach {
it.mustRunAfter(task)
}
}
}
}
}
}
@@ -0,0 +1,60 @@
package com.github.kr328.clash.tools
import org.gradle.api.DefaultTask
import org.gradle.api.file.DirectoryProperty
import org.gradle.api.provider.Property
import org.gradle.api.tasks.Input
import org.gradle.api.tasks.InputDirectory
import org.gradle.api.tasks.OutputDirectory
import org.gradle.api.tasks.TaskAction
import java.io.File
abstract class ClashBuildTask : DefaultTask() {
abstract val config: Property<BuildConfig>
@Input get
abstract val ndkDirectory: DirectoryProperty
@InputDirectory get
abstract val inputDirectory: DirectoryProperty
@InputDirectory get
abstract val outputDirectory: DirectoryProperty
@OutputDirectory get
@TaskAction
fun build() {
val input = inputDirectory.file
val output = outputDirectory.file
val config = config.get()
val environment = Environment(ndkDirectory.file, config.minSdkVersion)
val tags = listOf("without_gvisor", "without_system") +
(if (config.debug) listOf("debug") else emptyList()) +
(if (config.premium) listOf("premium") else emptyList())
Command.ofGoModuleTidy(input).exec()
config.abis.forEach {
Command.ofGoRun(
"make/make.go",
listOf("tun2socket", ".", "android", it.goArch),
input.resolve("tun2socket"),
environment.ofLwipBuild(it)
).exec()
Command.ofGoBuild(
"c-shared",
output.resolve("${it.value}/libclash.so"),
tags,
!config.debug,
input,
environment.ofCoreBuild(it)
).exec()
}
}
private val DirectoryProperty.file: File
get() = get().asFile
}
@@ -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"))
}
}
}
@@ -0,0 +1,84 @@
package com.github.kr328.clash.tools
import org.gradle.api.GradleException
import java.io.File
import kotlin.concurrent.thread
class Command(
private val command: Array<String>,
workingDir: File,
environments: Map<String, String>
) {
private val processBuilder: ProcessBuilder = ProcessBuilder(*command)
.redirectErrorStream(true)
.directory(workingDir)
.apply { environment().putAll(environments) }
fun exec() {
val process = processBuilder.start()
thread {
process.inputStream.copyTo(System.out)
}
val result = process.waitFor()
if (result != 0) {
throw GradleException("exec ${command.joinToString(" ")}: exit with $result")
}
}
companion object {
fun ofGoModuleTidy(workingDir: File): Command {
return Command(arrayOf("go", "mod", "tidy"), workingDir, System.getenv())
}
fun ofGoBuild(
mode: String,
output: File,
tags: List<String>,
strip: Boolean,
workingDir: File,
environments: Map<String, String>
): Command {
val command = mutableListOf("go", "build")
// go build mode
command += "-buildmode"
command += mode
// output file
command += "-o"
command += output.absolutePath
// trim path prefix
command += "-trimpath"
if (tags.isNotEmpty()) {
command += "-tags"
command += tags.joinToString(",")
}
if (strip) {
command += "-ldflags"
command += "-s -w"
}
return Command(command.toTypedArray(), workingDir, environments)
}
fun ofGoRun(
file: String,
args: List<String>,
workingDir: File,
environments: Map<String, String>
): Command {
val command = mutableListOf("go", "run")
command += file
command += args
return Command(command.toTypedArray(), workingDir, environments)
}
}
}
@@ -0,0 +1,58 @@
package com.github.kr328.clash.tools
import org.apache.tools.ant.taskdefs.condition.Os
import org.gradle.api.GradleException
import java.io.File
class Environment(
private val ndkDirectory: File,
private val minSdkVersion: Int,
) {
fun ofCoreBuild(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")
return mapOf(
"CC" to compiler.absolutePath,
"GOOS" to "android",
"GOARCH" to abi.goArch,
"GOARM" to abi.goArm,
"CGO_ENABLED" to "1",
"CFLAGS" to "-O3 -Werror",
)
}
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(
"CC" to compiler.absolutePath,
"AR" to ar.absolutePath,
)
}
}
@@ -0,0 +1,26 @@
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-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 {
return when (value) {
ArmeabiV7a.value -> ArmeabiV7a
Arm64V8a.value -> Arm64V8a
X86.value -> X86
X64.value -> X64
else -> throw IllegalArgumentException("unsupported abi $value")
}
}
}
}
+1 -1
View File
@@ -40,7 +40,7 @@ dependencies {
compileOnly(project(":hideapi"))
implementation(kotlin("stdlib-jdk7"))
implementation("androidx.core:core-ktx:$ktxVersion")
implementation("androidx.core:core-ktx:$coreVersion")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutineVersion")
}
@@ -1,8 +1,11 @@
package com.github.kr328.clash.common
import android.app.Application
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.cancel
object Global {
object Global : CoroutineScope by CoroutineScope(Dispatchers.IO) {
val application: Application
get() = application_
@@ -11,4 +14,8 @@ object Global {
fun init(application: Application) {
this.application_ = application
}
fun destroy() {
cancel()
}
}
@@ -4,6 +4,8 @@ import android.content.ComponentName
import com.github.kr328.clash.common.util.packageName
object Components {
val MAIN_ACTIVITY = ComponentName(packageName, "$packageName.MainActivity")
val PROPERTIES_ACTIVITY = ComponentName(packageName, "$packageName.PropertiesActivity")
private const val componentsPackageName = "com.github.kr328.clash"
val MAIN_ACTIVITY = ComponentName(packageName, "$componentsPackageName.MainActivity")
val PROPERTIES_ACTIVITY = ComponentName(packageName, "$componentsPackageName.PropertiesActivity")
}
+2 -2
View File
@@ -6,7 +6,7 @@ plugins {
id("com.android.library")
kotlin("android")
id("kotlinx-serialization")
id("library-golang")
id("clash-build")
}
val geoipDatabaseUrl =
@@ -81,7 +81,7 @@ dependencies {
api(project(":common"))
implementation(kotlin("stdlib-jdk7"))
implementation("androidx.core:core-ktx:$ktxVersion")
implementation("androidx.core:core-ktx:$coreVersion")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutineVersion")
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:$serializationVersion")
}
+4
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")
@@ -16,6 +19,7 @@ endif ()
include_directories("${GO_OUTPUT_BASE}/${CMAKE_ANDROID_ARCH_ABI}")
include_directories("${GO_SOURCE}")
include_directories("${GO_SOURCE}/tun2socket/bridge/native")
link_directories("${GO_OUTPUT_BASE}/${CMAKE_ANDROID_ARCH_ABI}")
+5 -6
View File
@@ -96,18 +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 gateway,
jstring mirror, 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 _gateway = get_string(gateway);
scoped_string _mirror = get_string(mirror);
scoped_string _blocking = get_string(blocking);
scoped_string _dns = get_string(dns);
jobject _interface = new_global(cb);
startTun(fd, mtu, _gateway, _mirror, _dns, _interface);
startTun(fd, mtu, _dns, _blocking, _interface);
}
JNIEXPORT void JNICALL
+5 -1
View File
@@ -7,6 +7,10 @@ var (
"8.8.8.8",
"1.1.1.1",
}
defaultFallback = []string{
"https://1.1.1.1/dns-query",
"https://doh.pub/dns-query",
}
defaultFakeIPFilter = []string{
// stun services
"+.stun.*.*",
@@ -21,7 +25,7 @@ var (
"*.n.n.srv.nintendo.net",
}
localNetwork = []string{
"0.0.0.0/32",
"0.0.0.0/8",
"127.0.0.0/8",
}
)
+2 -3
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) {
+2 -2
View File
@@ -58,10 +58,10 @@ func patchDns(cfg *config.RawConfig, _ string) error {
cfg.DNS.Enable = true
cfg.DNS.IPv6 = false
cfg.DNS.NameServer = defaultNameServers
cfg.DNS.Fallback = []string{}
cfg.DNS.Fallback = defaultFallback
cfg.DNS.FallbackFilter.GeoIP = false
cfg.DNS.FallbackFilter.IPCIDR = localNetwork
cfg.DNS.EnhancedMode = dns.FAKEIP
cfg.DNS.EnhancedMode = dns.MAPPING
cfg.DNS.FakeIPRange = "198.18.0.0/16"
cfg.DNS.DefaultNameserver = defaultNameServers
cfg.DNS.FakeIPFilter = defaultFakeIPFilter
+3 -1
View File
@@ -6,7 +6,7 @@ require (
cfa/blob v0.0.0 // local generated
github.com/Dreamacro/clash v0.0.0 // local
github.com/dlclark/regexp2 v1.4.0
github.com/kr328/tun2socket v0.0.0-20210412191540-3d56c47e2d99
github.com/kr328/tun2socket v0.0.0 // local
github.com/miekg/dns v1.1.42
github.com/oschwald/geoip2-golang v1.5.0
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c
@@ -15,4 +15,6 @@ require (
replace github.com/Dreamacro/clash => ./clash
replace github.com/kr328/tun2socket => ./tun2socket
replace cfa/blob => ../../../build/intermediates/golang_blob
+3 -1
View File
@@ -89,8 +89,10 @@ func (l *httpListener) handleConn(conn net.Conn) {
if err != nil || request.URL.Host == "" {
if err != nil {
log.Warnln("HTTP Connection closed: %s", err.Error())
} else {
log.Warnln("HTTP Connection closed: unknown host")
}
_ = conn.Close()
return
}
+10 -14
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,29 +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, gateway, mirror, 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)
mr := C.GoString(mirror)
d := C.GoString(dns)
remote := &remoteTun{callback: callback, closed: false, limit: semaphore.NewWeighted(4)}
if tun.Start(f, m, g, mr, d, remote.stop) != 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
}
+4 -73
View File
@@ -1,82 +1,21 @@
package tun
import (
"encoding/binary"
"io"
"net"
"time"
"github.com/Dreamacro/clash/component/resolver"
D "github.com/miekg/dns"
"github.com/kr328/tun2socket/binding"
"github.com/kr328/tun2socket/redirect"
)
const defaultDnsReadTimeout = time.Second * 30
func shouldHijackDns(dnsAddr binding.Address, targetAddr binding.Address) bool {
if targetAddr.Port != 53 {
func shouldHijackDns(dns net.IP, target net.IP, targetPort int) bool {
if targetPort != 53 {
return false
}
return dnsAddr.IP.Equal(net.IPv4zero) || dnsAddr.IP.Equal(targetAddr.IP)
return net.IPv4zero.Equal(dns) || target.Equal(dns)
}
func hijackUDPDns(pkt []byte, ep *binding.Endpoint, sender redirect.UDPSender) {
go func() {
answer, err := relayDnsPacket(pkt)
if err != nil {
return
}
_ = sender(answer, &binding.Endpoint{
Source: ep.Target,
Target: ep.Source,
})
}()
}
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
@@ -87,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()
+39
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])
}
}
-21
View File
@@ -1,21 +0,0 @@
package tun
import "github.com/Dreamacro/clash/log"
type logger struct{}
func (l *logger) D(format string, args ...interface{}) {
log.Debugln(format, args...)
}
func (l *logger) I(format string, args ...interface{}) {
log.Infoln(format, args...)
}
func (l *logger) W(format string, args ...interface{}) {
log.Warnln(format, args...)
}
func (l *logger) E(format string, args ...interface{}) {
log.Errorln(format, args...)
}
+91 -26
View File
@@ -1,40 +1,105 @@
package tun
import (
"encoding/binary"
"io"
"net"
"strconv"
"github.com/kr328/tun2socket/binding"
"time"
C "github.com/Dreamacro/clash/constant"
"github.com/Dreamacro/clash/context"
"github.com/Dreamacro/clash/log"
"github.com/Dreamacro/clash/tunnel"
)
func handleTCP(conn net.Conn, endpoint *binding.Endpoint) {
src := &net.TCPAddr{
IP: endpoint.Source.IP,
Port: int(endpoint.Source.Port),
Zone: "",
}
dst := &net.TCPAddr{
IP: endpoint.Target.IP,
Port: int(endpoint.Target.Port),
Zone: "",
}
const defaultDnsReadTimeout = time.Second * 30
metadata := &C.Metadata{
NetWork: C.TCP,
Type: C.SOCKS,
SrcIP: src.IP,
DstIP: dst.IP,
SrcPort: strconv.Itoa(src.Port),
DstPort: strconv.Itoa(dst.Port),
AddrType: C.AtypIPv4,
Host: "",
RawSrcAddr: src,
RawDstAddr: dst,
}
func (a *adapter) tcp() {
log.Infoln("[ATUN] TCP listener started")
defer log.Infoln("[ATUN] TCP listener exited")
defer a.stack.Close()
tunnel.Add(context.NewConnContext(conn, metadata))
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.Add(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
}
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
}
+54 -47
View File
@@ -3,67 +3,78 @@ package tun
import (
"net"
"os"
"strconv"
"strings"
"sync"
"github.com/kr328/tun2socket/binding"
"github.com/kr328/tun2socket/redirect"
"syscall"
"github.com/kr328/tun2socket"
)
var lock sync.Mutex
var tun *tun2socket.Tun2Socket
type adapter struct {
device *os.File
stack tun2socket.Stack
blocking []*net.IPNet
dns net.IP
mtu int
once sync.Once
stop func()
}
func Start(fd, mtu int, gateway, mirror, dns string, onStop func()) error {
var lock sync.Mutex
var instance *adapter
func (a *adapter) close() {
_ = a.stack.Close()
_ = a.device.Close()
}
func Start(fd, mtu int, dns string, blocking string, stop func()) error {
lock.Lock()
defer lock.Unlock()
stopLocked()
if instance != nil {
instance.close()
}
dnsHost, dnsPort, err := net.SplitHostPort(dns)
_ = syscall.SetNonblock(fd, true)
device := os.NewFile(uintptr(fd), "/dev/tun")
stack, err := tun2socket.NewStack(mtu)
if err != nil {
_ = device.Close()
return err
}
dnsP, err := strconv.Atoi(dnsPort)
if err != nil {
return err
}
dn := net.ParseIP(dns)
dnsAddr := binding.Address{
IP: net.ParseIP(dnsHost),
Port: uint16(dnsP),
}
var blk []*net.IPNet
t := tun2socket.NewTun2Socket(os.NewFile(uintptr(fd), "/dev/tun"), mtu, net.ParseIP(gateway), net.ParseIP(mirror))
for _, b := range strings.Split(blocking, ";") {
_, n, err := net.ParseCIDR(b)
if err != nil {
device.Close()
t.SetAllocator(allocUDP)
t.SetClosedHandler(onStop)
t.SetLogger(&logger{})
t.SetTCPHandler(func(conn net.Conn, endpoint *binding.Endpoint) {
if shouldHijackDns(dnsAddr, endpoint.Target) {
hijackTCPDns(conn)
return
return err
}
handleTCP(conn, endpoint)
})
t.SetUDPHandler(func(payload []byte, endpoint *binding.Endpoint, sender redirect.UDPSender) {
if shouldHijackDns(dnsAddr, endpoint.Target) {
hijackUDPDns(payload, endpoint, sender)
blk = append(blk, n)
}
return
}
instance = &adapter{
device: device,
stack: stack,
blocking: blk,
dns: dn,
mtu: mtu,
once: sync.Once{},
stop: stop,
}
handleUDP(payload, endpoint, sender)
})
t.Start()
tun = t
go instance.rx()
go instance.tx()
go instance.tcp()
go instance.udp()
return nil
}
@@ -72,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
}
+68 -43
View File
@@ -1,12 +1,11 @@
package tun
import (
"io"
"net"
"github.com/Dreamacro/clash/log"
"github.com/Dreamacro/clash/transport/socks5"
"github.com/kr328/tun2socket/binding"
"github.com/kr328/tun2socket/redirect"
"github.com/kr328/tun2socket"
adapters "github.com/Dreamacro/clash/adapters/inbound"
"github.com/Dreamacro/clash/common/pool"
@@ -14,63 +13,89 @@ import (
"github.com/Dreamacro/clash/tunnel"
)
type udpPacket struct {
metadata *C.Metadata
source binding.Address
data []byte
send redirect.UDPSender
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) {
uAddr, ok := addr.(*net.UDPAddr)
if !ok {
return 0, io.ErrClosedPipe
}
return len(b), u.send(b, &binding.Endpoint{
Source: binding.Address{IP: uAddr.IP, Port: uint16(uAddr.Port)},
Target: u.source,
})
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, endpoint *binding.Endpoint, sender redirect.UDPSender) {
pkt := &udpPacket{
source: endpoint.Source,
data: payload,
send: sender,
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],
}
adapter := adapters.NewPacket(socks5.ParseAddrToSocksAddr(tAddr), pkt, C.SOCKS)
tunnel.AddPacket(adapter)
}
}
func (a *adapter) hijackUDPDNS(pkt []byte, sAddr, tAddr *net.UDPAddr) bool {
if !shouldHijackDns(a.dns, tAddr.IP, tAddr.Port) {
return false
}
rAddr := &net.UDPAddr{
IP: endpoint.Target.IP,
Port: int(endpoint.Target.Port),
Zone: "",
}
go func() {
answer, err := relayDns(pkt)
adapter := adapters.NewPacket(socks5.ParseAddrToSocksAddr(rAddr), pkt, C.SOCKS)
if err != nil {
return
}
tunnel.AddPacket(adapter)
}
_, _ = a.stack.UDP().WriteTo(answer, sAddr, tAddr)
func allocUDP(size int) []byte {
return pool.Get(size)
}
pool.Put(pkt)
}()
func recycleUDP(payload []byte) {
_ = pool.Put(payload)
return true
}
@@ -61,13 +61,12 @@ object Clash {
fun startTun(
fd: Int,
mtu: Int,
gateway: String,
mirror: String,
dns: String,
blocking: String,
markSocket: (Int) -> Boolean,
querySocketUid: (protocol: Int, source: InetSocketAddress, target: InetSocketAddress) -> Int
) {
Bridge.nativeStartTun(fd, mtu, gateway, mirror, "$dns:53", object : TunInterface {
Bridge.nativeStartTun(fd, mtu, dns, blocking, object : TunInterface {
override fun markSocket(fd: Int) {
markSocket(fd)
}
@@ -220,7 +219,7 @@ object Clash {
return Channel<LogMessage>(32).apply {
Bridge.nativeSubscribeLogcat(object : LogcatInterface {
override fun received(jsonPayload: String) {
offer(Json.decodeFromString(LogMessage.serializer(), jsonPayload))
trySend(Json.decodeFromString(LogMessage.serializer(), jsonPayload))
}
})
}
@@ -17,15 +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,
gateway: String,
mirror: String,
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()
@@ -80,7 +80,10 @@ data class ConfigurationOverride(
var fakeIpFilter: List<String>? = null,
@SerialName("fallback-filter")
val fallbackFilter: DnsFallbackFilter = DnsFallbackFilter()
val fallbackFilter: DnsFallbackFilter = DnsFallbackFilter(),
@SerialName("nameserver-policy")
var nameserverPolicy: Map<String, String>? = null,
)
@Serializable
+1 -1
View File
@@ -59,7 +59,7 @@ dependencies {
implementation(kotlin("stdlib-jdk7"))
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutineVersion")
implementation("androidx.core:core-ktx:$ktxVersion")
implementation("androidx.core:core-ktx:$coreVersion")
implementation("androidx.appcompat:appcompat:$appcompatVersion")
implementation("androidx.activity:activity:$activityVersion")
implementation("com.google.android.material:material:$materialVersion")
@@ -236,7 +236,7 @@ class OverrideSettingsDesign(
summary = R.string.sideload_geoip_summary
) {
clicked {
requests.offer(Request.EditSideloadGeoip)
requests.trySend(Request.EditSideloadGeoip)
}
}
@@ -378,6 +378,15 @@ class OverrideSettingsDesign(
configure = dnsDependencies::add,
)
editableTextMap(
value = configuration.dns::nameserverPolicy,
keyAdapter = TextAdapter.String,
valueAdapter = TextAdapter.String,
title = R.string.name_server_policy,
placeholder = R.string.dont_modify,
configure = dnsDependencies::add,
)
dns.listener?.onChanged()
}
@@ -385,6 +394,6 @@ class OverrideSettingsDesign(
}
fun requestClear() {
requests.offer(Request.ResetOverride)
requests.trySend(Request.ResetOverride)
}
}
@@ -32,48 +32,48 @@ 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))
}
else -> return false
}
@@ -93,7 +93,7 @@ class AccessControlDesign(
binding.surface = dialog.surface
binding.mainList.applyLinearAdapter(context, adapter)
binding.keywordView.addTextChangedListener {
filter.offer(Unit)
filter.trySend(Unit)
}
binding.closeView.setOnClickListener {
dialog.dismiss()
@@ -38,7 +38,7 @@ class ApkBrokenDesign(context: Context) : Design<ApkBrokenDesign.Request>(contex
summary = R.string.google_play_url
) {
clicked {
requests.offer(Request(context.getString(R.string.google_play_url)))
requests.trySend(Request(context.getString(R.string.google_play_url)))
}
}
@@ -47,7 +47,7 @@ class ApkBrokenDesign(context: Context) : Design<ApkBrokenDesign.Request>(contex
summary = R.string.github_releases_url
) {
clicked {
requests.offer(Request(context.getString(R.string.github_releases_url)))
requests.trySend(Request(context.getString(R.string.github_releases_url)))
}
}
}
@@ -61,7 +61,7 @@ class AppSettingsDesign(
title = R.string.dark_mode
) {
listener = OnChangedListener {
requests.offer(Request.ReCreateAllActivities)
requests.trySend(Request.ReCreateAllActivities)
}
}
@@ -72,38 +72,38 @@ class FilesDesign(context: Context) : Design<FilesDesign.Request>(context) {
private fun requestOpen(file: File) {
if (file.isDirectory) {
requests.offer(Request.OpenDirectory(file))
requests.trySend(Request.OpenDirectory(file))
} else {
requests.offer(Request.OpenFile(file))
requests.trySend(Request.OpenFile(file))
}
}
fun requestRename(dialog: Dialog, file: File) {
requests.offer(Request.RenameFile(file))
requests.trySend(Request.RenameFile(file))
dialog.dismiss()
}
fun requestImport(dialog: Dialog, file: File) {
requests.offer(Request.ImportFile(file))
requests.trySend(Request.ImportFile(file))
dialog.dismiss()
}
fun requestExport(dialog: Dialog, file: File) {
requests.offer(Request.ExportFile(file))
requests.trySend(Request.ExportFile(file))
dialog.dismiss()
}
fun requestDelete(dialog: Dialog, file: File) {
requests.offer(Request.DeleteFile(file))
requests.trySend(Request.DeleteFile(file))
dialog.dismiss()
}
fun requestNew() {
requests.offer(Request.ImportFile(null))
requests.trySend(Request.ImportFile(null))
}
private fun requestMore(file: File) {
@@ -23,7 +23,7 @@ class LogsDesign(context: Context) : Design<LogsDesign.Request>(context) {
private val binding = DesignLogsBinding
.inflate(context.layoutInflater, context.root, false)
private val adapter = LogFileAdapter(context) {
requests.offer(Request.OpenFile(it))
requests.trySend(Request.OpenFile(it))
}
override val root: View
@@ -97,6 +97,6 @@ class MainDesign(context: Context) : Design<MainDesign.Request>(context) {
}
fun request(request: Request) {
requests.offer(request)
requests.trySend(request)
}
}
@@ -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,
@@ -96,7 +103,7 @@ class NetworkSettingsDesign(
summary = R.string.access_control_packages_summary,
) {
clicked {
requests.offer(Request.StartAccessControlList)
requests.trySend(Request.StartAccessControlList)
}
vpnDependencies.add(this)
@@ -38,13 +38,13 @@ class NewProfileDesign(context: Context) : Design<NewProfileDesign.Request>(cont
}
private fun requestCreate(provider: ProfileProvider) {
requests.offer(Request.Create(provider))
requests.trySend(Request.Create(provider))
}
private fun requestDetail(provider: ProfileProvider): Boolean {
if (provider !is ProfileProvider.External) return false
requests.offer(Request.OpenDetail(provider))
requests.trySend(Request.OpenDetail(provider))
return true
}
@@ -49,7 +49,7 @@ class ProfilesDesign(context: Context) : Design<ProfilesDesign.Request>(context)
suspend fun requestSave(profile: Profile) {
showToast(R.string.active_unsaved_tips, ToastDuration.Long) {
setAction(R.string.edit) {
requests.offer(Request.Edit(profile))
requests.trySend(Request.Edit(profile))
}
}
}
@@ -84,37 +84,37 @@ class ProfilesDesign(context: Context) : Design<ProfilesDesign.Request>(context)
}
fun requestUpdateAll() {
requests.offer(Request.UpdateAll)
requests.trySend(Request.UpdateAll)
}
fun requestCreate() {
requests.offer(Request.Create)
requests.trySend(Request.Create)
}
private fun requestActive(profile: Profile) {
requests.offer(Request.Active(profile))
requests.trySend(Request.Active(profile))
}
fun requestUpdate(dialog: Dialog, profile: Profile) {
requests.offer(Request.Update(profile))
requests.trySend(Request.Update(profile))
dialog.dismiss()
}
fun requestEdit(dialog: Dialog, profile: Profile) {
requests.offer(Request.Edit(profile))
requests.trySend(Request.Edit(profile))
dialog.dismiss()
}
fun requestDuplicate(dialog: Dialog, profile: Profile) {
requests.offer(Request.Duplicate(profile))
requests.trySend(Request.Duplicate(profile))
dialog.dismiss()
}
fun requestDelete(dialog: Dialog, profile: Profile) {
requests.offer(Request.Delete(profile))
requests.trySend(Request.Delete(profile))
dialog.dismiss()
}
@@ -142,11 +142,11 @@ class PropertiesDesign(context: Context) : Design<PropertiesDesign.Request>(cont
}
fun requestCommit() {
requests.offer(Request.Commit)
requests.trySend(Request.Commit)
}
fun requestBrowseFiles() {
requests.offer(Request.BrowseFiles)
requests.trySend(Request.BrowseFiles)
}
private fun ModelProgressBarConfigure.applyFrom(status: FetchStatus) {
@@ -24,7 +24,7 @@ class ProvidersDesign(
get() = binding.root
private val adapter = ProviderAdapter(context, providers) { index, provider ->
requests.offer(Request.Update(index, provider))
requests.trySend(Request.Update(index, provider))
}
fun updateElapsed() {
@@ -56,7 +56,7 @@ class ProvidersDesign(
adapter.states.filter { !it.updating }.forEachIndexed { index, state ->
state.updating = true
requests.offer(Request.Update(index, state.provider))
requests.trySend(Request.Update(index, state.provider))
}
}
}
@@ -134,7 +134,7 @@ class ProxyDesign(
config,
List(groupNames.size) { index ->
ProxyAdapter(config) { name ->
requests.offer(Request.Select(index, name))
requests.trySend(Request.Select(index, name))
}
}
) {
@@ -171,7 +171,7 @@ class ProxyDesign(
fun requestUrlTesting() {
urlTesting = true
requests.offer(Request.UrlTest(binding.pagesView.currentItem))
requests.trySend(Request.UrlTest(binding.pagesView.currentItem))
updateUrlTestButtonStatus()
}
@@ -28,6 +28,6 @@ class SettingsDesign(context: Context) : Design<SettingsDesign.Request>(context)
}
fun request(request: Request) {
requests.offer(request)
requests.trySend(request)
}
}
@@ -28,46 +28,46 @@ class AccessControlMenu(
when (item.itemId) {
R.id.select_all ->
requests.offer(Request.SelectAll)
requests.trySend(Request.SelectAll)
R.id.select_none ->
requests.offer(Request.SelectNone)
requests.trySend(Request.SelectNone)
R.id.select_invert ->
requests.offer(Request.SelectInvert)
requests.trySend(Request.SelectInvert)
R.id.system_apps -> {
uiStore.accessControlSystemApp = !item.isChecked
requests.offer(Request.ReloadApps)
requests.trySend(Request.ReloadApps)
}
R.id.name -> {
uiStore.accessControlSort = AppInfoSort.Label
requests.offer(Request.ReloadApps)
requests.trySend(Request.ReloadApps)
}
R.id.package_name -> {
uiStore.accessControlSort = AppInfoSort.PackageName
requests.offer(Request.ReloadApps)
requests.trySend(Request.ReloadApps)
}
R.id.install_time -> {
uiStore.accessControlSort = AppInfoSort.InstallTime
requests.offer(Request.ReloadApps)
requests.trySend(Request.ReloadApps)
}
R.id.update_time -> {
uiStore.accessControlSort = AppInfoSort.UpdateTime
requests.offer(Request.ReloadApps)
requests.trySend(Request.ReloadApps)
}
R.id.reverse -> {
uiStore.accessControlReverse = item.isChecked
requests.offer(Request.ReloadApps)
requests.trySend(Request.ReloadApps)
}
R.id.import_from_clipboard -> {
requests.offer(Request.Import)
requests.trySend(Request.Import)
}
R.id.export_to_clipboard -> {
requests.offer(Request.Export)
requests.trySend(Request.Export)
}
else -> return false
}
@@ -8,7 +8,7 @@ import com.github.kr328.clash.design.ui.Insets
fun View.setOnInsertsChangedListener(adaptLandscape: Boolean = true, listener: (Insets) -> Unit) {
setOnApplyWindowInsetsListener { v, ins ->
val compat = WindowInsetsCompat.toWindowInsetsCompat(ins)
val insets = compat.systemWindowInsets
val insets = compat.getInsets(WindowInsetsCompat.Type.systemBars())
val rInsets = if (ViewCompat.getLayoutDirection(v) == ViewCompat.LAYOUT_DIRECTION_LTR) {
Insets(
@@ -28,7 +28,7 @@ fun View.setOnInsertsChangedListener(adaptLandscape: Boolean = true, listener: (
listener(if (adaptLandscape) rInsets.landscape(v.context) else rInsets)
compat.consumeStableInsets().toWindowInsets()
compat.toWindowInsets()
}
requestApplyInsets()
@@ -208,4 +208,9 @@
<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>
<string name="block_loopback">阻止本地迴環</string>
<string name="block_loopback_summary">阻止本地迴環連接</string>
</resources>
@@ -208,4 +208,9 @@
<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>
<string name="block_loopback">阻止本地迴環</string>
<string name="block_loopback_summary">阻止本地迴環連接</string>
</resources>
@@ -210,4 +210,7 @@
<string name="import_">导入</string>
<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>
+3
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>
@@ -159,6 +161,7 @@
<string name="geoip_fallback">GeoIP Fallback</string>
<string name="ipcidr_fallback">IPCIDR Fallback</string>
<string name="domain_fallback">Domain Fallback</string>
<string name="name_server_policy">Name Server Policy</string>
<string name="dont_modify">Do not modify</string>
<string name="empty">Empty</string>
@@ -238,7 +238,7 @@ class OverrideSettingsDesign(
summary = R.string.sideload_geoip_summary
) {
clicked {
requests.offer(Request.EditSideloadGeoip)
requests.trySend(Request.EditSideloadGeoip)
}
}
@@ -380,6 +380,15 @@ class OverrideSettingsDesign(
configure = dnsDependencies::add,
)
editableTextMap(
value = configuration.dns::nameserverPolicy,
keyAdapter = TextAdapter.String,
valueAdapter = TextAdapter.String,
title = R.string.name_server_policy,
placeholder = R.string.dont_modify,
configure = dnsDependencies::add,
)
dns.listener?.onChanged()
}
@@ -387,6 +396,6 @@ class OverrideSettingsDesign(
}
fun requestClear() {
requests.offer(Request.ResetOverride)
requests.trySend(Request.ResetOverride)
}
}
}
@@ -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
}
+1 -1
Submodule kaidl updated: 963190ac8e...16da2e83b7.
+9 -12
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 {
@@ -71,9 +62,15 @@ dependencies {
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutineVersion")
implementation("androidx.room:room-runtime:$roomVersion")
implementation("androidx.room:room-ktx:$roomVersion")
implementation("androidx.core:core-ktx:$ktxVersion")
implementation("com.microsoft.appcenter:appcenter-analytics:$appcenterVersion")
implementation("com.microsoft.appcenter:appcenter-crashes:$appcenterVersion")
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"))
}
}
}
+1 -5
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
@@ -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) {
@@ -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)
}
}
}
@@ -6,6 +6,7 @@ import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import androidx.core.content.getSystemService
import com.github.kr328.clash.common.Global
import com.github.kr328.clash.common.compat.pendingIntentFlags
import com.github.kr328.clash.common.compat.startForegroundServiceCompat
import com.github.kr328.clash.common.constants.Intents
@@ -16,7 +17,6 @@ import com.github.kr328.clash.service.data.Imported
import com.github.kr328.clash.service.data.ImportedDao
import com.github.kr328.clash.service.model.Profile
import com.github.kr328.clash.service.util.importedDir
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
@@ -27,7 +27,7 @@ class ProfileReceiver : BroadcastReceiver() {
when (intent.action) {
Intent.ACTION_BOOT_COMPLETED, Intent.ACTION_MY_PACKAGE_REPLACED,
Intent.ACTION_TIMEZONE_CHANGED, Intent.ACTION_TIME_CHANGED -> {
GlobalScope.launch {
Global.launch {
reset()
val service = Intent(Intents.ACTION_PROFILE_SCHEDULE_UPDATES)
@@ -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 {
@@ -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!!
}
}
@@ -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,12 +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,
gateway = TUN_GATEWAY,
mirror = TUN_MIRROR,
blocking = blocking.joinToString(";"),
dns = if (store.dnsHijacking) NET_ANY else TUN_DNS,
)
}
@@ -229,9 +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_MIRROR = "172.31.255.254"
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"
}
}
@@ -28,7 +28,7 @@ class ConfigurationModule(service: Service) : Module<ConfigurationModule.LoadExc
var loaded: UUID? = null
reload.offer(Unit)
reload.trySend(Unit)
while (true) {
val changed: UUID? = select {
@@ -75,6 +75,6 @@ class ConfigurationModule(service: Service) : Module<ConfigurationModule.LoadExc
}
fun reload() {
reload.offer(Unit)
reload.trySend(Unit)
}
}
@@ -39,7 +39,7 @@ abstract class Module<E>(val service: Service) {
return
}
channel.offer(intent)
channel.trySend(intent)
}
}
@@ -36,7 +36,7 @@ class NetworkObserveModule(service: Service) :
override fun onAvailable(network: Network) {
this.network = network
networks.offer(network)
networks.trySend(network)
}
override fun onCapabilitiesChanged(
@@ -49,19 +49,19 @@ class NetworkObserveModule(service: Service) :
if (this.network == network && this.internet != internet) {
this.internet = internet
networks.offer(network)
networks.trySend(network)
}
}
override fun onLost(network: Network) {
if (this.network == network) {
networks.offer(null)
networks.trySend(null)
}
}
override fun onLinkPropertiesChanged(network: Network, linkProperties: LinkProperties) {
if (this.network == network) {
networks.offer(network)
networks.trySend(network)
}
}
}
@@ -16,9 +16,8 @@ class TunModule(private val vpn: VpnService) : Module<Unit>(vpn) {
data class TunDevice(
val fd: Int,
val mtu: Int,
val gateway: String,
val mirror: String,
val dns: String
val blocking: String,
val dns: String,
)
private val connectivity = service.getSystemService<ConnectivityManager>()!!
@@ -58,9 +57,8 @@ class TunModule(private val vpn: VpnService) : Module<Unit>(vpn) {
Clash.startTun(
fd = device.fd,
mtu = device.mtu,
gateway = device.gateway,
mirror = device.mirror,
dns = device.dns,
blocking = device.blocking,
markSocket = vpn::protect,
querySocketUid = this::queryUid
)
@@ -7,7 +7,6 @@ import com.github.kr328.clash.common.Global
import com.github.kr328.clash.service.data.migrations.LEGACY_MIGRATION
import com.github.kr328.clash.service.data.migrations.MIGRATIONS
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import java.lang.ref.SoftReference
import androidx.room.Database as DB
@@ -41,7 +40,7 @@ abstract class Database : RoomDatabase() {
}
init {
GlobalScope.launch(Dispatchers.IO) {
Global.launch(Dispatchers.IO) {
LEGACY_MIGRATION(Global.application)
}
}
@@ -12,7 +12,6 @@ import com.github.kr328.clash.service.model.Profile
import com.github.kr328.clash.service.util.generateProfileUUID
import com.github.kr328.clash.service.util.pendingDir
import com.github.kr328.clash.service.util.sendProfileChanged
import com.microsoft.appcenter.crashes.Crashes
import java.io.File
internal suspend fun migrationFromLegacy(context: Context) {
@@ -37,8 +36,6 @@ internal suspend fun migrationFromLegacy(context: Context) {
}
}
} catch (e: Exception) {
Crashes.trackError(e)
Log.w("Migration legacy database: $e", e)
}
@@ -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
}
@@ -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(
-1
View File
@@ -76,6 +76,5 @@
<item>255.255.255.248/30</item>
<item>255.255.255.252/31</item>
<item>255.255.255.254/32</item>
<item>172.31.255.252/30</item> <!-- tun device address -->
</string-array>
</resources>