Unverified Commit daad494b authored by Mygod's avatar Mygod Committed by GitHub

Merge pull request #2096 from Mygod/dns-server

New local DNS server implemented with Kotlin coroutines with support for private DNS (DNS over TLS) on Android 9+.

YOLO!
parents c2357c8a e6433c28
...@@ -3,7 +3,7 @@ jobs: ...@@ -3,7 +3,7 @@ jobs:
build: build:
working_directory: ~/code working_directory: ~/code
docker: docker:
- image: shadowsocks/android-ndk-go - image: circleci/android:api-28-ndk
environment: environment:
JVM_OPTS: -Xmx3500m JVM_OPTS: -Xmx3500m
GRADLE_OPTS: -Dorg.gradle.workers.max=1 -Dorg.gradle.daemon=false -Dkotlin.compiler.execution.strategy="in-process" GRADLE_OPTS: -Dorg.gradle.workers.max=1 -Dorg.gradle.daemon=false -Dkotlin.compiler.execution.strategy="in-process"
......
...@@ -30,7 +30,3 @@ ...@@ -30,7 +30,3 @@
[submodule "core/src/main/jni/libev"] [submodule "core/src/main/jni/libev"]
path = core/src/main/jni/libev path = core/src/main/jni/libev
url = https://github.com/shadowsocks/libev.git url = https://github.com/shadowsocks/libev.git
[submodule "core/src/main/jni/overture"]
path = core/src/main/jni/overture
url = https://github.com/shadowsocks/overture.git
branch = shadowsocks
...@@ -7,8 +7,8 @@ services: ...@@ -7,8 +7,8 @@ services:
- docker - docker
before_install: before_install:
- docker pull shadowsocks/android-ndk-go - docker pull circleci/android:api-28-ndk
- chmod -R a+wrx . - chmod -R a+wrx .
script: script:
- docker run --rm -v ${PWD}:/build shadowsocks/android-ndk-go bash -c "cd /build; ./gradlew assembleDebug check" - docker run --rm -v ${PWD}:/build circleci/android:api-28-ndk bash -c "cd /build; ./gradlew assembleDebug check"
## [Shadowsocks](https://shadowsocks.org) for Android ## [Shadowsocks](https://shadowsocks.org) for Android
[![Build Status](https://travis-ci.com/shadowsocks/shadowsocks-android.svg)](https://travis-ci.com/shadowsocks/shadowsocks-android) [![CircleCI](https://circleci.com/gh/shadowsocks/shadowsocks-android.svg?style=shield)](https://circleci.com/gh/shadowsocks/shadowsocks-android)
[![API](https://img.shields.io/badge/API-21%2B-brightgreen.svg?style=flat)](https://android-arsenal.com/api?level=21) [![API](https://img.shields.io/badge/API-21%2B-brightgreen.svg?style=flat)](https://android-arsenal.com/api?level=21)
[![Releases](https://img.shields.io/github/downloads/shadowsocks/shadowsocks-android/total.svg)](https://github.com/shadowsocks/shadowsocks-android/releases) [![Releases](https://img.shields.io/github/downloads/shadowsocks/shadowsocks-android/total.svg)](https://github.com/shadowsocks/shadowsocks-android/releases)
[![Language: Kotlin](https://img.shields.io/github/languages/top/shadowsocks/shadowsocks-android.svg)](https://github.com/shadowsocks/shadowsocks-android/search?l=kotlin) [![Language: Kotlin](https://img.shields.io/github/languages/top/shadowsocks/shadowsocks-android.svg)](https://github.com/shadowsocks/shadowsocks-android/search?l=kotlin)
...@@ -16,9 +16,8 @@ for Android TV ([beta](https://play.google.com/apps/testing/com.github.shadowsoc ...@@ -16,9 +16,8 @@ for Android TV ([beta](https://play.google.com/apps/testing/com.github.shadowsoc
### PREREQUISITES ### PREREQUISITES
* JDK 1.8 * JDK 1.8
* Go 1.11+
* Android SDK * Android SDK
- Android NDK r19+ - Android NDK
### BUILD ### BUILD
...@@ -32,7 +31,7 @@ You can check whether the latest commit builds under UNIX environment by checkin ...@@ -32,7 +31,7 @@ You can check whether the latest commit builds under UNIX environment by checkin
```bash ```bash
mkdir build mkdir build
sudo chown 3434:3434 build sudo chown 3434:3434 build
docker run --rm -v ${PWD}/build:/build shadowsocks/android-ndk-go bash -c "cd /build; git clone https://github.com/shadowsocks/shadowsocks-android; cd shadowsocks-android; git submodule update --init --recursive; ./gradlew assembleDebug" docker run --rm -v ${PWD}/build:/build circleci/android:api-28-ndk bash -c "cd /build; git clone https://github.com/shadowsocks/shadowsocks-android; cd shadowsocks-android; git submodule update --init --recursive; ./gradlew assembleDebug"
``` ```
### [TRANSLATE](https://discourse.shadowsocks.org/t/poeditor-translation-main-thread/30) ### [TRANSLATE](https://discourse.shadowsocks.org/t/poeditor-translation-main-thread/30)
...@@ -47,7 +46,6 @@ docker run --rm -v ${PWD}/build:/build shadowsocks/android-ndk-go bash -c "cd /b ...@@ -47,7 +46,6 @@ docker run --rm -v ${PWD}/build:/build shadowsocks/android-ndk-go bash -c "cd /b
<li>pcre: <a href="https://android.googlesource.com/platform/external/pcre/+/master/dist2/LICENCE">BSD</a></li> <li>pcre: <a href="https://android.googlesource.com/platform/external/pcre/+/master/dist2/LICENCE">BSD</a></li>
<li>libancillary: <a href="https://github.com/shadowsocks/libancillary/blob/shadowsocks-android/COPYING">BSD</a></li> <li>libancillary: <a href="https://github.com/shadowsocks/libancillary/blob/shadowsocks-android/COPYING">BSD</a></li>
<li>shadowsocks-libev: <a href="https://github.com/shadowsocks/shadowsocks-libev/blob/master/LICENSE">GPLv3</a></li> <li>shadowsocks-libev: <a href="https://github.com/shadowsocks/shadowsocks-libev/blob/master/LICENSE">GPLv3</a></li>
<li>overture: <a href="https://github.com/shawn1m/overture/blob/master/LICENSE">MIT</a></li>
<li>libev: <a href="https://github.com/shadowsocks/libev/blob/master/LICENSE">GPLv2</a></li> <li>libev: <a href="https://github.com/shadowsocks/libev/blob/master/LICENSE">GPLv2</a></li>
<li>libsodium: <a href="https://github.com/jedisct1/libsodium/blob/master/LICENSE">ISC</a></li> <li>libsodium: <a href="https://github.com/jedisct1/libsodium/blob/master/LICENSE">ISC</a></li>
</ul> </ul>
......
...@@ -4,14 +4,14 @@ apply plugin: 'com.github.ben-manes.versions' ...@@ -4,14 +4,14 @@ apply plugin: 'com.github.ben-manes.versions'
buildscript { buildscript {
ext { ext {
kotlinVersion = '1.3.20' kotlinVersion = '1.3.21'
minSdkVersion = 21 minSdkVersion = 21
sdkVersion = 28 sdkVersion = 28
compileSdkVersion = 28 compileSdkVersion = 28
buildToolsVersion = '28.0.3' buildToolsVersion = '28.0.3'
lifecycleVersion = '2.0.0' lifecycleVersion = '2.0.0'
roomVersion = '2.0.0' roomVersion = '2.0.0'
workVersion = '1.0.0-beta03' workVersion = '1.0.0-beta05'
preferencexVersion = '1.0.0' preferencexVersion = '1.0.0'
junitVersion = '4.12' junitVersion = '4.12'
androidTestVersion = '1.1.1' androidTestVersion = '1.1.1'
...@@ -29,7 +29,7 @@ buildscript { ...@@ -29,7 +29,7 @@ buildscript {
} }
dependencies { dependencies {
classpath 'com.android.tools.build:gradle:3.3.0' classpath 'com.android.tools.build:gradle:3.3.1'
classpath 'com.github.ben-manes:gradle-versions-plugin:0.20.0' classpath 'com.github.ben-manes:gradle-versions-plugin:0.20.0'
classpath 'com.google.gms:google-services:4.2.0' classpath 'com.google.gms:google-services:4.2.0'
classpath 'io.fabric.tools:gradle:1.27.1' classpath 'io.fabric.tools:gradle:1.27.1'
......
import org.apache.tools.ant.taskdefs.condition.Os
apply plugin: 'com.android.library' apply plugin: 'com.android.library'
apply plugin: 'kotlin-android' apply plugin: 'kotlin-android'
apply plugin: 'kotlin-kapt' apply plugin: 'kotlin-kapt'
...@@ -40,23 +38,6 @@ android { ...@@ -40,23 +38,6 @@ android {
} }
} }
task goBuild(type: Exec) {
if (Os.isFamily(Os.FAMILY_WINDOWS)) {
println "Warning: Building on Windows is not supported"
} else {
executable "/bin/bash"
args "go-build.bash", minSdkVersion
environment "ANDROID_HOME", android.sdkDirectory
environment "ANDROID_NDK_HOME", android.ndkDirectory
}
}
tasks.whenTaskAdded { task ->
if ((task.name == 'generateJsonModelDebug' || task.name == 'generateJsonModelRelease')) {
task.dependsOn(goBuild)
}
}
dependencies { dependencies {
api project(':plugin') api project(':plugin')
api "android.arch.work:work-runtime-ktx:$workVersion" api "android.arch.work:work-runtime-ktx:$workVersion"
...@@ -66,9 +47,11 @@ dependencies { ...@@ -66,9 +47,11 @@ dependencies {
api "androidx.room:room-runtime:$roomVersion" api "androidx.room:room-runtime:$roomVersion"
api 'com.crashlytics.sdk.android:crashlytics:2.9.8' api 'com.crashlytics.sdk.android:crashlytics:2.9.8'
api 'com.google.firebase:firebase-config:16.1.3' api 'com.google.firebase:firebase-config:16.1.3'
api 'com.google.firebase:firebase-core:16.0.6' api 'com.google.firebase:firebase-core:16.0.7'
api "com.takisoft.preferencex:preferencex:$preferencexVersion" api "com.takisoft.preferencex:preferencex:$preferencexVersion"
api 'dnsjava:dnsjava:2.1.8'
api 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.1.1' api 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.1.1'
api 'org.connectbot.jsocks:jsocks:1.0.0'
kapt "androidx.lifecycle:lifecycle-compiler:$lifecycleVersion" kapt "androidx.lifecycle:lifecycle-compiler:$lifecycleVersion"
kapt "androidx.room:room-compiler:$roomVersion" kapt "androidx.room:room-compiler:$roomVersion"
testImplementation "androidx.arch.core:core-testing:$lifecycleVersion" testImplementation "androidx.arch.core:core-testing:$lifecycleVersion"
......
#!/bin/bash
[[ -z "${ANDROID_NDK_HOME}" ]] && ANDROID_NDK_HOME="${ANDROID_HOME}/ndk-bundle"
TOOLCHAIN="$(find ${ANDROID_NDK_HOME}/toolchains/llvm/prebuilt/* -maxdepth 1 -type d -print -quit)/bin"
ABIS=(armeabi-v7a arm64-v8a x86 x86_64)
GO_ARCHS=('arm GOARM=7' arm64 386 amd64)
CLANG_ARCHS=(armv7a-linux-androideabi aarch64-linux-android i686-linux-android x86_64-linux-android)
STRIP_ARCHS=(arm-linux-androideabi aarch64-linux-android i686-linux-android x86_64-linux-android)
MIN_API="$1"
ROOT="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
OUT_DIR="$ROOT/build/go"
cd "$ROOT/src/main/jni/overture/main"
BIN="liboverture.so"
for i in "${!ABIS[@]}"; do
ABI="${ABIS[$i]}"
[[ -f "${OUT_DIR}/${ABI}/${BIN}" ]] && continue
echo "Build ${BIN} ${ABI}"
mkdir -p ${OUT_DIR}/${ABI} \
&& env \
CGO_ENABLED=1 CC="${TOOLCHAIN}/${CLANG_ARCHS[$i]}${MIN_API}-clang" \
GOOS=android GOARCH=${GO_ARCHS[$i]} \
go build -v -ldflags='-s -w' -o "${OUT_DIR}/unstripped" \
&& "${TOOLCHAIN}/${STRIP_ARCHS[$i]}-strip" "${OUT_DIR}/unstripped" -o "${OUT_DIR}/${ABI}/${BIN}" \
|| exit -1
rm "${OUT_DIR}/unstripped"
done
cd "$ROOT"
(^|\.)googleapis(\.[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?){1,2}$
...@@ -107,14 +107,13 @@ object Core { ...@@ -107,14 +107,13 @@ object Core {
if (DataStore.tcpFastOpen && !TcpFastOpen.sendEnabled) TcpFastOpen.enableTimeout() if (DataStore.tcpFastOpen && !TcpFastOpen.sendEnabled) TcpFastOpen.enableTimeout()
if (DataStore.publicStore.getLong(Key.assetUpdateTime, -1) != packageInfo.lastUpdateTime) { if (DataStore.publicStore.getLong(Key.assetUpdateTime, -1) != packageInfo.lastUpdateTime) {
val assetManager = app.assets val assetManager = app.assets
for (dir in arrayOf("acl", "overture")) try {
try { for (file in assetManager.list("acl")!!) assetManager.open("acl/$file").use { input ->
for (file in assetManager.list(dir)!!) assetManager.open("$dir/$file").use { input -> File(deviceStorage.noBackupFilesDir, file).outputStream().use { output -> input.copyTo(output) }
File(deviceStorage.noBackupFilesDir, file).outputStream().use { output -> input.copyTo(output) }
}
} catch (e: IOException) {
printLog(e)
} }
} catch (e: IOException) {
printLog(e)
}
DataStore.publicStore.putLong(Key.assetUpdateTime, packageInfo.lastUpdateTime) DataStore.publicStore.putLong(Key.assetUpdateTime, packageInfo.lastUpdateTime)
} }
updateNotificationChannels() updateNotificationChannels()
......
...@@ -31,15 +31,16 @@ class AclSyncer(context: Context, workerParams: WorkerParameters) : CoroutineWor ...@@ -31,15 +31,16 @@ class AclSyncer(context: Context, workerParams: WorkerParameters) : CoroutineWor
companion object { companion object {
private const val KEY_ROUTE = "route" private const val KEY_ROUTE = "route"
fun schedule(route: String) = WorkManager.getInstance().enqueue(OneTimeWorkRequestBuilder<AclSyncer>().run { fun schedule(route: String) = WorkManager.getInstance().enqueueUniqueWork(route, ExistingWorkPolicy.REPLACE,
setInputData(Data.Builder().putString(KEY_ROUTE, route).build()) OneTimeWorkRequestBuilder<AclSyncer>().run {
setConstraints(Constraints.Builder() setInputData(Data.Builder().putString(KEY_ROUTE, route).build())
.setRequiredNetworkType(NetworkType.UNMETERED) setConstraints(Constraints.Builder()
.setRequiresCharging(true) .setRequiredNetworkType(NetworkType.UNMETERED)
.build()) .setRequiresCharging(true)
setInitialDelay(10, TimeUnit.SECONDS) .build())
build() setInitialDelay(10, TimeUnit.SECONDS)
}) build()
})
} }
override val coroutineContext get() = Dispatchers.IO override val coroutineContext get() = Dispatchers.IO
......
...@@ -43,6 +43,7 @@ import com.google.firebase.analytics.FirebaseAnalytics ...@@ -43,6 +43,7 @@ import com.google.firebase.analytics.FirebaseAnalytics
import kotlinx.coroutines.* import kotlinx.coroutines.*
import java.io.File import java.io.File
import java.net.InetAddress import java.net.InetAddress
import java.net.URL
import java.net.UnknownHostException import java.net.UnknownHostException
import java.util.* import java.util.*
...@@ -233,9 +234,9 @@ object BaseService { ...@@ -233,9 +234,9 @@ object BaseService {
else startService(Intent(this, javaClass)) else startService(Intent(this, javaClass))
} }
suspend fun killProcesses() { fun killProcesses(scope: CoroutineScope) {
data.processes?.run { data.processes?.run {
close() close(scope)
data.processes = null data.processes = null
} }
} }
...@@ -246,26 +247,28 @@ object BaseService { ...@@ -246,26 +247,28 @@ object BaseService {
data.changeState(STOPPING) data.changeState(STOPPING)
GlobalScope.launch(Dispatchers.Main, CoroutineStart.UNDISPATCHED) { GlobalScope.launch(Dispatchers.Main, CoroutineStart.UNDISPATCHED) {
Core.analytics.logEvent("stop", bundleOf(Pair(FirebaseAnalytics.Param.METHOD, tag))) Core.analytics.logEvent("stop", bundleOf(Pair(FirebaseAnalytics.Param.METHOD, tag)))
killProcesses()
// clean up recevier
this@Interface as Service this@Interface as Service
val data = data // we use a coroutineScope here to allow clean-up in parallel
if (data.closeReceiverRegistered) { coroutineScope {
unregisterReceiver(data.closeReceiver) killProcesses(this)
data.closeReceiverRegistered = false // clean up receivers
} val data = data
if (data.closeReceiverRegistered) {
unregisterReceiver(data.closeReceiver)
data.closeReceiverRegistered = false
}
data.notification?.destroy() data.notification?.destroy()
data.notification = null data.notification = null
val ids = listOfNotNull(data.proxy, data.udpFallback).map { val ids = listOfNotNull(data.proxy, data.udpFallback).map {
it.close() it.shutdown(this)
it.profile.id it.profile.id
}
data.proxy = null
data.udpFallback = null
data.binder.trafficPersisted(ids)
} }
data.proxy = null
data.binder.trafficPersisted(ids)
// change the state // change the state
data.changeState(STOPPED, msg) data.changeState(STOPPED, msg)
...@@ -276,7 +279,8 @@ object BaseService { ...@@ -276,7 +279,8 @@ object BaseService {
} }
suspend fun preInit() { } suspend fun preInit() { }
suspend fun resolver(host: String) = InetAddress.getByName(host) suspend fun resolver(host: String) = InetAddress.getAllByName(host)
suspend fun openConnection(url: URL) = url.openConnection()
fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
val data = data val data = data
...@@ -293,7 +297,7 @@ object BaseService { ...@@ -293,7 +297,7 @@ object BaseService {
profile.name = profile.formattedName // save name for later queries profile.name = profile.formattedName // save name for later queries
val proxy = ProxyInstance(profile) val proxy = ProxyInstance(profile)
data.proxy = proxy data.proxy = proxy
if (fallback != null) data.udpFallback = ProxyInstance(fallback, profile.route) data.udpFallback = if (fallback == null) null else ProxyInstance(fallback, profile.route)
if (!data.closeReceiverRegistered) { if (!data.closeReceiverRegistered) {
registerReceiver(data.closeReceiver, IntentFilter().apply { registerReceiver(data.closeReceiver, IntentFilter().apply {
...@@ -310,14 +314,14 @@ object BaseService { ...@@ -310,14 +314,14 @@ object BaseService {
data.changeState(CONNECTING) data.changeState(CONNECTING)
data.connectingJob = GlobalScope.launch(Dispatchers.Main) { data.connectingJob = GlobalScope.launch(Dispatchers.Main) {
try { try {
killProcesses() Executable.killAll() // clean up old processes
preInit() preInit()
proxy.init(this@Interface::resolver) proxy.init(this@Interface)
data.udpFallback?.init(this@Interface::resolver) data.udpFallback?.init(this@Interface)
data.processes = GuardedProcessPool { data.processes = GuardedProcessPool {
printLog(it) printLog(it)
data.connectingJob?.apply { runBlocking { cancelAndJoin() } } data.connectingJob?.cancelAndJoin()
stopRunner(false, it.localizedMessage) stopRunner(false, it.localizedMessage)
} }
startProcesses() startProcesses()
...@@ -327,6 +331,8 @@ object BaseService { ...@@ -327,6 +331,8 @@ object BaseService {
RemoteConfig.fetch() RemoteConfig.fetch()
data.changeState(CONNECTED) data.changeState(CONNECTED)
} catch (_: CancellationException) {
// if the job was cancelled, it is canceller's responsibility to call stopRunner
} catch (_: UnknownHostException) { } catch (_: UnknownHostException) {
stopRunner(false, getString(R.string.invalid_server)) stopRunner(false, getString(R.string.invalid_server))
} catch (exc: Throwable) { } catch (exc: Throwable) {
......
...@@ -33,11 +33,9 @@ import java.io.FileNotFoundException ...@@ -33,11 +33,9 @@ import java.io.FileNotFoundException
object Executable { object Executable {
const val REDSOCKS = "libredsocks.so" const val REDSOCKS = "libredsocks.so"
const val SS_LOCAL = "libss-local.so" const val SS_LOCAL = "libss-local.so"
const val SS_TUNNEL = "libss-tunnel.so"
const val TUN2SOCKS = "libtun2socks.so" const val TUN2SOCKS = "libtun2socks.so"
const val OVERTURE = "liboverture.so"
private val EXECUTABLES = setOf(SS_LOCAL, SS_TUNNEL, REDSOCKS, TUN2SOCKS, OVERTURE) private val EXECUTABLES = setOf(SS_LOCAL, REDSOCKS, TUN2SOCKS)
fun killAll() { fun killAll() {
for (process in File("/proc").listFiles { _, name -> TextUtils.isDigitsOnly(name) }) { for (process in File("/proc").listFiles { _, name -> TextUtils.isDigitsOnly(name) }) {
......
...@@ -32,13 +32,12 @@ import com.github.shadowsocks.Core ...@@ -32,13 +32,12 @@ import com.github.shadowsocks.Core
import com.github.shadowsocks.utils.Commandline import com.github.shadowsocks.utils.Commandline
import kotlinx.coroutines.* import kotlinx.coroutines.*
import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.selects.select
import java.io.File import java.io.File
import java.io.IOException import java.io.IOException
import java.io.InputStream import java.io.InputStream
import kotlin.concurrent.thread import kotlin.concurrent.thread
class GuardedProcessPool(private val onFatal: (IOException) -> Unit) : CoroutineScope { class GuardedProcessPool(private val onFatal: suspend (IOException) -> Unit) : CoroutineScope {
companion object { companion object {
private const val TAG = "GuardedProcessPool" private const val TAG = "GuardedProcessPool"
private val pid by lazy { private val pid by lazy {
...@@ -47,7 +46,6 @@ class GuardedProcessPool(private val onFatal: (IOException) -> Unit) : Coroutine ...@@ -47,7 +46,6 @@ class GuardedProcessPool(private val onFatal: (IOException) -> Unit) : Coroutine
} }
private inner class Guard(private val cmd: List<String>) { private inner class Guard(private val cmd: List<String>) {
private val abortChannel = Channel<Unit>()
private lateinit var process: Process private lateinit var process: Process
private fun streamLogger(input: InputStream, logger: (String) -> Unit) = try { private fun streamLogger(input: InputStream, logger: (String) -> Unit) = try {
...@@ -58,10 +56,6 @@ class GuardedProcessPool(private val onFatal: (IOException) -> Unit) : Coroutine ...@@ -58,10 +56,6 @@ class GuardedProcessPool(private val onFatal: (IOException) -> Unit) : Coroutine
process = ProcessBuilder(cmd).directory(Core.deviceStorage.noBackupFilesDir).start() process = ProcessBuilder(cmd).directory(Core.deviceStorage.noBackupFilesDir).start()
} }
suspend fun abort() {
if (!abortChannel.isClosedForSend) abortChannel.send(Unit)
}
suspend fun looper(onRestartCallback: (suspend () -> Unit)?) { suspend fun looper(onRestartCallback: (suspend () -> Unit)?) {
var running = true var running = true
val cmdName = File(cmd.first()).nameWithoutExtension val cmdName = File(cmd.first()).nameWithoutExtension
...@@ -75,58 +69,56 @@ class GuardedProcessPool(private val onFatal: (IOException) -> Unit) : Coroutine ...@@ -75,58 +69,56 @@ class GuardedProcessPool(private val onFatal: (IOException) -> Unit) : Coroutine
runBlocking { exitChannel.send(process.waitFor()) } runBlocking { exitChannel.send(process.waitFor()) }
} }
val startTime = SystemClock.elapsedRealtime() val startTime = SystemClock.elapsedRealtime()
if (select { val exitCode = exitChannel.receive()
abortChannel.onReceive { true } // prefer abort to save work
exitChannel.onReceive { false }
}) break
running = false running = false
if (SystemClock.elapsedRealtime() - startTime < 1000) throw IOException("$cmdName exits too fast") if (SystemClock.elapsedRealtime() - startTime < 1000) {
Crashlytics.log(Log.DEBUG, TAG, "restart process: " + Commandline.toString(cmd)) throw IOException("$cmdName exits too fast (exit code: $exitCode)")
}
Crashlytics.log(Log.DEBUG, TAG,
"restart process: ${Commandline.toString(cmd)} (last exit code: $exitCode)")
start() start()
running = true running = true
onRestartCallback?.invoke() onRestartCallback?.invoke()
} }
} catch (e: IOException) { } catch (e: IOException) {
Crashlytics.log(Log.WARN, TAG, "error occurred. stop guard: " + Commandline.toString(cmd)) Crashlytics.log(Log.WARN, TAG, "error occurred. stop guard: " + Commandline.toString(cmd))
// calling callback without closing channel first will cause deadlock, therefore we defer it
GlobalScope.launch(Dispatchers.Main) { onFatal(e) } GlobalScope.launch(Dispatchers.Main) { onFatal(e) }
} finally { } finally {
abortChannel.close() if (running) withContext(NonCancellable) { // clean-up cannot be cancelled
} if (Build.VERSION.SDK_INT < 24) {
if (!running) return // process already exited, nothing to be done try {
if (Build.VERSION.SDK_INT < 24) { Os.kill(pid.get(process) as Int, OsConstants.SIGTERM)
try { } catch (e: ErrnoException) {
Os.kill(pid.get(process) as Int, OsConstants.SIGTERM) if (e.errno != OsConstants.ESRCH) throw e
} catch (e: ErrnoException) { }
if (e.errno != OsConstants.ESRCH) throw e if (withTimeoutOrNull(500) { exitChannel.receive() } != null) return@withContext
} }
if (withTimeoutOrNull(500) { exitChannel.receive() } != null) return process.destroy() // kill the process
} if (Build.VERSION.SDK_INT >= 26) {
process.destroy() // kill the process if (withTimeoutOrNull(1000) { exitChannel.receive() } != null) return@withContext
if (Build.VERSION.SDK_INT >= 26) { process.destroyForcibly() // Force to kill the process if it's still alive
if (withTimeoutOrNull(1000) { exitChannel.receive() } != null) return }
process.destroyForcibly() // Force to kill the process if it's still alive exitChannel.receive()
} // otherwise process already exited, nothing to be done
} }
exitChannel.receive()
} }
} }
private val supervisor = SupervisorJob() private val job = Job()
override val coroutineContext get() = Dispatchers.Main + supervisor override val coroutineContext get() = Dispatchers.Main + job
private val guards = ArrayList<Guard>()
@MainThread @MainThread
fun start(cmd: List<String>, onRestartCallback: (suspend () -> Unit)? = null) { fun start(cmd: List<String>, onRestartCallback: (suspend () -> Unit)? = null) {
Crashlytics.log(Log.DEBUG, TAG, "start process: " + Commandline.toString(cmd)) Crashlytics.log(Log.DEBUG, TAG, "start process: " + Commandline.toString(cmd))
val guard = Guard(cmd) Guard(cmd).apply {
guard.start() start() // if start fails, IOException will be thrown directly
guards += guard launch { looper(onRestartCallback) }
launch(start = CoroutineStart.UNDISPATCHED) { guard.looper(onRestartCallback) } }
} }
@MainThread @MainThread
suspend fun close() { fun close(scope: CoroutineScope) {
guards.forEach { it.abort() } job.cancel()
supervisor.children.forEach { it.join() } // we can't cancel the supervisor as we need it to do clean up scope.launch { job.join() }
} }
} }
...@@ -20,75 +20,51 @@ ...@@ -20,75 +20,51 @@
package com.github.shadowsocks.bg package com.github.shadowsocks.bg
import com.github.shadowsocks.Core
import com.github.shadowsocks.Core.app import com.github.shadowsocks.Core.app
import com.github.shadowsocks.acl.Acl import com.github.shadowsocks.acl.Acl
import com.github.shadowsocks.core.R
import com.github.shadowsocks.net.LocalDnsServer
import com.github.shadowsocks.net.Socks5Endpoint
import com.github.shadowsocks.net.Subnet
import com.github.shadowsocks.preference.DataStore import com.github.shadowsocks.preference.DataStore
import com.github.shadowsocks.utils.parseNumericAddress import kotlinx.coroutines.CoroutineScope
import java.io.File import java.net.InetSocketAddress
import java.net.Inet6Address import java.net.URI
import org.json.JSONArray import java.util.*
import org.json.JSONObject
object LocalDnsService { object LocalDnsService {
private val googleApisTester =
"(^|\\.)googleapis(\\.[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?){1,2}\$".toRegex()
private val chinaIpList by lazy {
app.resources.openRawResource(R.raw.china_ip_list).bufferedReader()
.lineSequence().map(Subnet.Companion::fromString).filterNotNull().toList()
}
private val servers = WeakHashMap<LocalDnsService.Interface, LocalDnsServer>()
interface Interface : BaseService.Interface { interface Interface : BaseService.Interface {
override suspend fun startProcesses() { override suspend fun startProcesses() {
super.startProcesses() super.startProcesses()
val data = data
val profile = data.proxy!!.profile val profile = data.proxy!!.profile
val dns = URI("dns://${profile.remoteDns}")
fun makeDns(name: String, address: String, timeout: Int, edns: Boolean = true) = JSONObject().apply { LocalDnsServer(this::resolver,
put("Name", name) Socks5Endpoint(dns.host, if (dns.port < 0) 53 else dns.port),
put("Address", when (address.parseNumericAddress()) { DataStore.proxyAddress).apply {
is Inet6Address -> "[$address]" tcp = !profile.udpdns
else -> address when (profile.route) {
}) Acl.BYPASS_CHN, Acl.BYPASS_LAN_CHN, Acl.GFWLIST, Acl.CUSTOM_RULES -> {
put("Timeout", timeout) remoteDomainMatcher = googleApisTester
put("EDNSClientSubnet", JSONObject().put("Policy", "disable")) localIpMatcher = chinaIpList
put("Protocol", if (edns) {
put("Socks5Address", "127.0.0.1:${DataStore.portProxy}")
"tcp"
} else "udp")
}
fun buildOvertureConfig(file: String) = file.also {
File(Core.deviceStorage.noBackupFilesDir, it).writeText(JSONObject().run {
put("BindAddress", "${DataStore.listenAddress}:${DataStore.portLocalDns}")
put("RedirectIPv6Record", true)
put("DomainBase64Decode", false)
put("HostsFile", "hosts")
put("MinimumTTL", 120)
put("CacheSize", 4096)
val remoteDns = JSONArray(profile.remoteDns.split(",")
.mapIndexed { i, dns -> makeDns("UserDef-$i", dns.trim() + ":53", 12) })
val localDns = JSONArray(arrayOf(
makeDns("Primary-1", "208.67.222.222:443", 9, false),
makeDns("Primary-2", "119.29.29.29:53", 9, false),
makeDns("Primary-3", "114.114.114.114:53", 9, false)))
when (profile.route) {
Acl.BYPASS_CHN, Acl.BYPASS_LAN_CHN, Acl.GFWLIST, Acl.CUSTOM_RULES -> {
put("PrimaryDNS", localDns)
put("AlternativeDNS", remoteDns)
put("IPNetworkFile", JSONObject(mapOf("Primary" to "china_ip_list.txt")))
put("AclFile", "domain_exceptions.acl")
}
Acl.CHINALIST -> {
put("PrimaryDNS", localDns)
put("AlternativeDNS", remoteDns)
}
else -> {
put("PrimaryDNS", remoteDns)
// no need to setup AlternativeDNS in Acl.ALL/BYPASS_LAN mode
put("OnlyPrimaryDNS", true)
}
} }
toString() Acl.CHINALIST -> { }
}) else -> forwardOnly = true
} }
}.also { servers[this] = it }.start(InetSocketAddress(DataStore.listenAddress, DataStore.portLocalDns))
}
if (!profile.udpdns) data.processes!!.start(buildAdditionalArguments(arrayListOf( override fun killProcesses(scope: CoroutineScope) {
File(app.applicationInfo.nativeLibraryDir, Executable.OVERTURE).absolutePath, servers.remove(this)?.shutdown(scope)
"-c", buildOvertureConfig("overture.conf")))) super.killProcesses(scope)
} }
} }
} }
...@@ -22,6 +22,8 @@ package com.github.shadowsocks.bg ...@@ -22,6 +22,8 @@ package com.github.shadowsocks.bg
import android.content.Context import android.content.Context
import android.util.Base64 import android.util.Base64
import android.util.Log
import com.crashlytics.android.Crashlytics
import com.github.shadowsocks.Core import com.github.shadowsocks.Core
import com.github.shadowsocks.acl.Acl import com.github.shadowsocks.acl.Acl
import com.github.shadowsocks.acl.AclSyncer import com.github.shadowsocks.acl.AclSyncer
...@@ -31,46 +33,44 @@ import com.github.shadowsocks.plugin.PluginConfiguration ...@@ -31,46 +33,44 @@ import com.github.shadowsocks.plugin.PluginConfiguration
import com.github.shadowsocks.plugin.PluginManager import com.github.shadowsocks.plugin.PluginManager
import com.github.shadowsocks.preference.DataStore import com.github.shadowsocks.preference.DataStore
import com.github.shadowsocks.utils.DirectBoot import com.github.shadowsocks.utils.DirectBoot
import com.github.shadowsocks.utils.disconnectFromMain
import com.github.shadowsocks.utils.parseNumericAddress import com.github.shadowsocks.utils.parseNumericAddress
import com.github.shadowsocks.utils.signaturesCompat import com.github.shadowsocks.utils.signaturesCompat
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.*
import kotlinx.coroutines.withContext
import kotlinx.coroutines.withTimeout
import java.io.File import java.io.File
import java.io.IOException import java.io.IOException
import java.net.HttpURLConnection import java.net.HttpURLConnection
import java.net.InetAddress
import java.net.UnknownHostException import java.net.UnknownHostException
import java.security.MessageDigest import java.security.MessageDigest
/** /**
* This class sets up environment for ss-local. * This class sets up environment for ss-local.
*/ */
class ProxyInstance(val profile: Profile, private val route: String = profile.route) : AutoCloseable { class ProxyInstance(val profile: Profile, private val route: String = profile.route) {
var configFile: File? = null private var configFile: File? = null
var trafficMonitor: TrafficMonitor? = null var trafficMonitor: TrafficMonitor? = null
private val plugin = PluginConfiguration(profile.plugin ?: "").selectedOptions private val plugin = PluginConfiguration(profile.plugin ?: "").selectedOptions
val pluginPath by lazy { PluginManager.init(plugin) } val pluginPath by lazy { PluginManager.init(plugin) }
suspend fun init(resolver: suspend (String) -> InetAddress) { suspend fun init(service: BaseService.Interface) {
if (profile.host == "198.199.101.152") { if (profile.host == "198.199.101.152") {
val mdg = MessageDigest.getInstance("SHA-1") val mdg = MessageDigest.getInstance("SHA-1")
mdg.update(Core.packageInfo.signaturesCompat.first().toByteArray()) mdg.update(Core.packageInfo.signaturesCompat.first().toByteArray())
val conn = RemoteConfig.proxyUrl.openConnection() as HttpURLConnection val conn = service.openConnection(RemoteConfig.proxyUrl) as HttpURLConnection
conn.requestMethod = "POST" conn.requestMethod = "POST"
conn.doOutput = true conn.doOutput = true
val proxies = try { val proxies = try {
withTimeout(30_000) { withTimeoutOrNull(30_000) {
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
conn.outputStream.bufferedWriter().use { conn.outputStream.bufferedWriter().use {
it.write("sig=" + Base64.encodeToString(mdg.digest(), Base64.DEFAULT)) it.write("sig=" + Base64.encodeToString(mdg.digest(), Base64.DEFAULT))
} }
conn.inputStream.bufferedReader().readText() conn.inputStream.bufferedReader().readText()
} }
} } ?: throw UnknownHostException()
} finally { } finally {
conn.disconnect() conn.disconnectFromMain()
}.split('|').toMutableList() }.split('|').toMutableList()
proxies.shuffle() proxies.shuffle()
val proxy = proxies.first().split(':') val proxy = proxies.first().split(':')
...@@ -83,9 +83,19 @@ class ProxyInstance(val profile: Profile, private val route: String = profile.ro ...@@ -83,9 +83,19 @@ class ProxyInstance(val profile: Profile, private val route: String = profile.ro
if (route == Acl.CUSTOM_RULES) Acl.save(Acl.CUSTOM_RULES, Acl.customRules.flatten(10)) if (route == Acl.CUSTOM_RULES) Acl.save(Acl.CUSTOM_RULES, Acl.customRules.flatten(10))
// it's hard to resolve DNS on a specific interface so we'll do it here // it's hard to resolve DNS on a specific interface so we'll do it here
if (profile.host.parseNumericAddress() == null) profile.host = withTimeout(10_000) { if (profile.host.parseNumericAddress() == null) profile.host = withTimeoutOrNull(10_000) {
withContext(Dispatchers.IO) { resolver(profile.host).hostAddress } var retries = 0
} ?: throw UnknownHostException() while (isActive) try {
val io = GlobalScope.async(Dispatchers.IO) { service.resolver(profile.host) }
return@withTimeoutOrNull io.await().firstOrNull()
} catch (e: UnknownHostException) {
// retries are only needed on Chrome OS where arc0 is brought up/down during VPN changes
if (!DataStore.hasArc0) throw e
Thread.yield()
Crashlytics.log(Log.WARN, "ProxyInstance-resolver", "Retry resolving attempt #${++retries}")
}
null // only here for type resolving
}?.hostAddress ?: throw UnknownHostException()
} }
/** /**
...@@ -115,7 +125,7 @@ class ProxyInstance(val profile: Profile, private val route: String = profile.ro ...@@ -115,7 +125,7 @@ class ProxyInstance(val profile: Profile, private val route: String = profile.ro
} }
// for UDP profile, it's only going to operate in UDP relay mode-only so this flag has no effect // for UDP profile, it's only going to operate in UDP relay mode-only so this flag has no effect
if (profile.udpdns) cmd += "-D" if (profile.route == Acl.ALL || profile.route == Acl.BYPASS_LAN) cmd += "-D"
if (DataStore.tcpFastOpen) cmd += "--fast-open" if (DataStore.tcpFastOpen) cmd += "--fast-open"
...@@ -126,9 +136,9 @@ class ProxyInstance(val profile: Profile, private val route: String = profile.ro ...@@ -126,9 +136,9 @@ class ProxyInstance(val profile: Profile, private val route: String = profile.ro
if (route !in arrayOf(Acl.ALL, Acl.CUSTOM_RULES)) AclSyncer.schedule(route) if (route !in arrayOf(Acl.ALL, Acl.CUSTOM_RULES)) AclSyncer.schedule(route)
} }
override fun close() { fun shutdown(scope: CoroutineScope) {
trafficMonitor?.apply { trafficMonitor?.apply {
close() thread.shutdown(scope)
// Make sure update total traffic when stopping the runner // Make sure update total traffic when stopping the runner
try { try {
// profile may have host, etc. modified and thus a re-fetch is necessary (possible race condition) // profile may have host, etc. modified and thus a re-fetch is necessary (possible race condition)
......
...@@ -29,12 +29,12 @@ import java.io.IOException ...@@ -29,12 +29,12 @@ import java.io.IOException
import java.nio.ByteBuffer import java.nio.ByteBuffer
import java.nio.ByteOrder import java.nio.ByteOrder
class TrafficMonitor(statFile: File) : AutoCloseable { class TrafficMonitor(statFile: File) {
private val thread = object : LocalSocketListener("TrafficMonitor-" + statFile.name, statFile) { val thread = object : LocalSocketListener("TrafficMonitor-" + statFile.name, statFile) {
private val buffer = ByteArray(16)
private val stat = ByteBuffer.wrap(buffer).order(ByteOrder.LITTLE_ENDIAN)
override fun acceptInternal(socket: LocalSocket) { override fun acceptInternal(socket: LocalSocket) {
val buffer = ByteArray(16)
if (socket.inputStream.read(buffer) != 16) throw IOException("Unexpected traffic stat length") if (socket.inputStream.read(buffer) != 16) throw IOException("Unexpected traffic stat length")
val stat = ByteBuffer.wrap(buffer).order(ByteOrder.LITTLE_ENDIAN)
val tx = stat.getLong(0) val tx = stat.getLong(0)
val rx = stat.getLong(8) val rx = stat.getLong(8)
if (current.txTotal != tx) { if (current.txTotal != tx) {
...@@ -79,6 +79,4 @@ class TrafficMonitor(statFile: File) : AutoCloseable { ...@@ -79,6 +79,4 @@ class TrafficMonitor(statFile: File) : AutoCloseable {
} }
return Pair(out, updated) return Pair(out, updated)
} }
override fun close() = thread.close()
} }
...@@ -36,20 +36,6 @@ class TransproxyService : Service(), LocalDnsService.Interface { ...@@ -36,20 +36,6 @@ class TransproxyService : Service(), LocalDnsService.Interface {
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int = override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int =
super<LocalDnsService.Interface>.onStartCommand(intent, flags, startId) super<LocalDnsService.Interface>.onStartCommand(intent, flags, startId)
private fun startDNSTunnel() {
val proxy = data.proxy!!
val cmd = arrayListOf(File(applicationInfo.nativeLibraryDir, Executable.SS_TUNNEL).absolutePath,
"-t", "10",
"-b", DataStore.listenAddress,
"-u",
"-l", DataStore.portLocalDns.toString(), // ss-tunnel listens on the same port as overture
"-L", proxy.profile.remoteDns.split(",").first().trim() + ":53",
// config is already built by BaseService.Interface
"-c", (data.udpFallback ?: proxy).configFile!!.absolutePath)
if (DataStore.tcpFastOpen) cmd += "--fast-open"
data.processes!!.start(cmd)
}
private fun startRedsocksDaemon() { private fun startRedsocksDaemon() {
File(Core.deviceStorage.noBackupFilesDir, "redsocks.conf").writeText("""base { File(Core.deviceStorage.noBackupFilesDir, "redsocks.conf").writeText("""base {
log_debug = off; log_debug = off;
...@@ -73,7 +59,6 @@ redsocks { ...@@ -73,7 +59,6 @@ redsocks {
override suspend fun startProcesses() { override suspend fun startProcesses() {
startRedsocksDaemon() startRedsocksDaemon()
super.startProcesses() super.startProcesses()
if (data.proxy!!.profile.udpdns) startDNSTunnel()
} }
override fun onDestroy() { override fun onDestroy() {
......
...@@ -39,22 +39,25 @@ import com.github.shadowsocks.net.DefaultNetworkListener ...@@ -39,22 +39,25 @@ import com.github.shadowsocks.net.DefaultNetworkListener
import com.github.shadowsocks.net.Subnet import com.github.shadowsocks.net.Subnet
import com.github.shadowsocks.preference.DataStore import com.github.shadowsocks.preference.DataStore
import com.github.shadowsocks.utils.Key import com.github.shadowsocks.utils.Key
import com.github.shadowsocks.utils.parseNumericAddress
import com.github.shadowsocks.utils.printLog import com.github.shadowsocks.utils.printLog
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import java.io.Closeable import java.io.Closeable
import java.io.File import java.io.File
import java.io.FileDescriptor import java.io.FileDescriptor
import java.io.IOException import java.io.IOException
import java.lang.reflect.Method import java.net.URL
import java.util.* import java.util.*
import android.net.VpnService as BaseVpnService import android.net.VpnService as BaseVpnService
class VpnService : BaseVpnService(), LocalDnsService.Interface { class VpnService : BaseVpnService(), LocalDnsService.Interface {
companion object { companion object {
private const val VPN_MTU = 1500 private const val VPN_MTU = 1500
private const val PRIVATE_VLAN = "172.19.0.%s" private const val PRIVATE_VLAN4_CLIENT = "172.19.0.1"
private const val PRIVATE_VLAN6 = "fdfe:dcba:9876::%s" private const val PRIVATE_VLAN4_ROUTER = "172.19.0.2"
private const val PRIVATE_VLAN6_CLIENT = "fdfe:dcba:9876::1"
private const val PRIVATE_VLAN6_ROUTER = "fdfe:dcba:9876::2"
/** /**
* https://android.googlesource.com/platform/prebuilts/runtime/+/94fec32/appcompat/hiddenapi-light-greylist.txt#9466 * https://android.googlesource.com/platform/prebuilts/runtime/+/94fec32/appcompat/hiddenapi-light-greylist.txt#9466
...@@ -112,12 +115,12 @@ class VpnService : BaseVpnService(), LocalDnsService.Interface { ...@@ -112,12 +115,12 @@ class VpnService : BaseVpnService(), LocalDnsService.Interface {
override fun onRevoke() = stopRunner() override fun onRevoke() = stopRunner()
override suspend fun killProcesses() { override fun killProcesses(scope: CoroutineScope) {
super.killProcesses(scope)
active = false active = false
DefaultNetworkListener.stop(this) scope.launch { DefaultNetworkListener.stop(this) }
worker?.shutdown() worker?.shutdown(scope)
worker = null worker = null
super.killProcesses()
conn?.close() conn?.close()
conn = null conn = null
} }
...@@ -133,13 +136,12 @@ class VpnService : BaseVpnService(), LocalDnsService.Interface { ...@@ -133,13 +136,12 @@ class VpnService : BaseVpnService(), LocalDnsService.Interface {
} }
override suspend fun preInit() = DefaultNetworkListener.start(this) { underlyingNetwork = it } override suspend fun preInit() = DefaultNetworkListener.start(this) { underlyingNetwork = it }
override suspend fun resolver(host: String) = DefaultNetworkListener.get().getByName(host) override suspend fun resolver(host: String) = DefaultNetworkListener.get().getAllByName(host)
override suspend fun openConnection(url: URL) = DefaultNetworkListener.get().openConnection(url)
override suspend fun startProcesses() { override suspend fun startProcesses() {
worker = ProtectWorker().apply { start() } worker = ProtectWorker().apply { start() }
super.startProcesses() super.startProcesses()
sendFd(startVpn()) sendFd(startVpn())
} }
...@@ -154,12 +156,11 @@ class VpnService : BaseVpnService(), LocalDnsService.Interface { ...@@ -154,12 +156,11 @@ class VpnService : BaseVpnService(), LocalDnsService.Interface {
.setConfigureIntent(Core.configureIntent(this)) .setConfigureIntent(Core.configureIntent(this))
.setSession(profile.formattedName) .setSession(profile.formattedName)
.setMtu(VPN_MTU) .setMtu(VPN_MTU)
.addAddress(PRIVATE_VLAN.format(Locale.ENGLISH, "1"), 24) .addAddress(PRIVATE_VLAN4_CLIENT, 30)
.addDnsServer(PRIVATE_VLAN4_ROUTER)
profile.remoteDns.split(",").forEach { builder.addDnsServer(it.trim()) }
if (profile.ipv6) { if (profile.ipv6) {
builder.addAddress(PRIVATE_VLAN6.format(Locale.ENGLISH, "1"), 126) builder.addAddress(PRIVATE_VLAN6_CLIENT, 126)
builder.addRoute("::", 0) builder.addRoute("::", 0)
} }
...@@ -185,8 +186,7 @@ class VpnService : BaseVpnService(), LocalDnsService.Interface { ...@@ -185,8 +186,7 @@ class VpnService : BaseVpnService(), LocalDnsService.Interface {
val subnet = Subnet.fromString(it)!! val subnet = Subnet.fromString(it)!!
builder.addRoute(subnet.address.hostAddress, subnet.prefixSize) builder.addRoute(subnet.address.hostAddress, subnet.prefixSize)
} }
profile.remoteDns.split(",").mapNotNull { it.trim().parseNumericAddress() } builder.addRoute(PRIVATE_VLAN4_ROUTER, 32)
.forEach { builder.addRoute(it, it.address.size shl 3) }
} }
} }
...@@ -198,22 +198,19 @@ class VpnService : BaseVpnService(), LocalDnsService.Interface { ...@@ -198,22 +198,19 @@ class VpnService : BaseVpnService(), LocalDnsService.Interface {
val fd = conn.fd val fd = conn.fd
val cmd = arrayListOf(File(applicationInfo.nativeLibraryDir, Executable.TUN2SOCKS).absolutePath, val cmd = arrayListOf(File(applicationInfo.nativeLibraryDir, Executable.TUN2SOCKS).absolutePath,
"--netif-ipaddr", PRIVATE_VLAN.format(Locale.ENGLISH, "2"), "--netif-ipaddr", PRIVATE_VLAN4_ROUTER,
"--netif-netmask", "255.255.255.0", "--netif-netmask", "255.255.255.0",
"--socks-server-addr", "${DataStore.listenAddress}:${DataStore.portProxy}", "--socks-server-addr", "${DataStore.listenAddress}:${DataStore.portProxy}",
"--tunfd", fd.toString(), "--tunfd", fd.toString(),
"--tunmtu", VPN_MTU.toString(), "--tunmtu", VPN_MTU.toString(),
"--sock-path", "sock_path", "--sock-path", "sock_path",
"--dnsgw", "127.0.0.1:${DataStore.portLocalDns}",
"--loglevel", "3") "--loglevel", "3")
if (profile.ipv6) { if (profile.ipv6) {
cmd += "--netif-ip6addr" cmd += "--netif-ip6addr"
cmd += PRIVATE_VLAN6.format(Locale.ENGLISH, "2") cmd += PRIVATE_VLAN6_ROUTER
} }
cmd += "--enable-udprelay" cmd += "--enable-udprelay"
if (!profile.udpdns) {
cmd += "--dnsgw"
cmd += "127.0.0.1:${DataStore.portLocalDns}"
}
data.processes!!.start(cmd, onRestartCallback = { data.processes!!.start(cmd, onRestartCallback = {
try { try {
sendFd(conn.fileDescriptor) sendFd(conn.fileDescriptor)
......
/*******************************************************************************
* *
* Copyright (C) 2019 by Max Lv <max.c.lv@gmail.com> *
* Copyright (C) 2019 by Mygod Studio <contact-shadowsocks-android@mygod.be> *
* *
* This program is free software: you can redistribute it and/or modify *
* it under the terms of the GNU General Public License as published by *
* the Free Software Foundation, either version 3 of the License, or *
* (at your option) any later version. *
* *
* This program is distributed in the hope that it will be useful, *
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
* GNU General Public License for more details. *
* *
* You should have received a copy of the GNU General Public License *
* along with this program. If not, see <http://www.gnu.org/licenses/>. *
* *
*******************************************************************************/
package com.github.shadowsocks.net
import com.github.shadowsocks.utils.printLog
import kotlinx.coroutines.*
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.channels.sendBlocking
import java.io.IOException
import java.nio.ByteBuffer
import java.nio.channels.*
class ChannelMonitor : Thread("ChannelMonitor") {
private data class Registration(val channel: SelectableChannel,
val ops: Int,
val listener: (SelectionKey) -> Unit) {
val result = CompletableDeferred<SelectionKey>()
}
private val selector = Selector.open()
private val registrationPipe = Pipe.open()
private val pendingRegistrations = Channel<Registration>(Channel.UNLIMITED)
private val closeChannel = Channel<Unit>(1)
@Volatile
private var running = true
private fun registerInternal(channel: SelectableChannel, ops: Int, block: (SelectionKey) -> Unit) =
channel.register(selector, ops, block)
init {
registrationPipe.source().apply {
configureBlocking(false)
registerInternal(this, SelectionKey.OP_READ) {
val junk = ByteBuffer.allocateDirect(1)
while (read(junk) > 0) {
pendingRegistrations.poll()!!.apply {
try {
result.complete(registerInternal(channel, ops, listener))
} catch (e: ClosedChannelException) {
result.completeExceptionally(e)
}
}
junk.clear()
}
}
}
start()
}
suspend fun register(channel: SelectableChannel, ops: Int, block: (SelectionKey) -> Unit): SelectionKey {
val registration = Registration(channel, ops, block)
pendingRegistrations.send(registration)
ByteBuffer.allocateDirect(1).also { junk ->
loop@ while (running) when (registrationPipe.sink().write(junk)) {
0 -> kotlinx.coroutines.yield()
1 -> break@loop
else -> throw IOException("Failed to register in the channel")
}
}
if (!running) throw ClosedChannelException()
return registration.result.await()
}
suspend fun wait(channel: SelectableChannel, ops: Int) = CompletableDeferred<SelectionKey>().run {
register(channel, ops) {
if (it.isValid) it.interestOps(0) // stop listening
complete(it)
}
await()
}
override fun run() {
while (running) {
val num = try {
selector.select()
} catch (e: IOException) {
printLog(e)
continue
}
if (num <= 0) continue
val iterator = selector.selectedKeys().iterator()
while (iterator.hasNext()) {
val key = iterator.next()
iterator.remove()
(key.attachment() as (SelectionKey) -> Unit)(key)
}
}
closeChannel.sendBlocking(Unit)
}
fun close(scope: CoroutineScope) {
running = false
selector.wakeup()
scope.launch {
closeChannel.receive()
selector.keys().forEach { it.channel().close() }
selector.close()
}
}
}
...@@ -34,9 +34,10 @@ abstract class ConcurrentLocalSocketListener(name: String, socketFile: File) : L ...@@ -34,9 +34,10 @@ abstract class ConcurrentLocalSocketListener(name: String, socketFile: File) : L
launch { super.accept(socket) } launch { super.accept(socket) }
} }
suspend fun shutdown() { override fun shutdown(scope: CoroutineScope) {
running = false
job.cancel() job.cancel()
close() super.shutdown(scope)
job.join() scope.launch { job.join() }
} }
} }
...@@ -26,19 +26,17 @@ import android.net.Network ...@@ -26,19 +26,17 @@ import android.net.Network
import android.net.NetworkCapabilities import android.net.NetworkCapabilities
import android.net.NetworkRequest import android.net.NetworkRequest
import android.os.Build import android.os.Build
import android.widget.Toast
import androidx.core.content.getSystemService import androidx.core.content.getSystemService
import com.crashlytics.android.Crashlytics import com.crashlytics.android.Crashlytics
import com.github.shadowsocks.Core.app import com.github.shadowsocks.Core.app
import kotlinx.coroutines.CompletableDeferred import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.channels.actor import kotlinx.coroutines.channels.actor
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import java.net.UnknownHostException import java.net.UnknownHostException
object DefaultNetworkListener : CoroutineScope { object DefaultNetworkListener {
override val coroutineContext get() = Dispatchers.Default
private sealed class NetworkMessage { private sealed class NetworkMessage {
class Start(val key: Any, val listener: (Network?) -> Unit) : NetworkMessage() class Start(val key: Any, val listener: (Network?) -> Unit) : NetworkMessage()
class Get : NetworkMessage() { class Get : NetworkMessage() {
...@@ -50,13 +48,13 @@ object DefaultNetworkListener : CoroutineScope { ...@@ -50,13 +48,13 @@ object DefaultNetworkListener : CoroutineScope {
class Update(val network: Network) : NetworkMessage() class Update(val network: Network) : NetworkMessage()
class Lost(val network: Network) : NetworkMessage() class Lost(val network: Network) : NetworkMessage()
} }
private val networkActor = actor<NetworkMessage> { private val networkActor = GlobalScope.actor<NetworkMessage>(Dispatchers.Unconfined) {
val listeners = mutableMapOf<Any, (Network?) -> Unit>() val listeners = mutableMapOf<Any, (Network?) -> Unit>()
var network: Network? = null var network: Network? = null
val pendingRequests = arrayListOf<NetworkMessage.Get>() val pendingRequests = arrayListOf<NetworkMessage.Get>()
for (message in channel) when (message) { for (message in channel) when (message) {
is NetworkMessage.Start -> { is NetworkMessage.Start -> {
if (listeners.isEmpty()) registerDefaultNetworkListener() if (listeners.isEmpty()) register()
listeners[message.key] = message.listener listeners[message.key] = message.listener
if (network != null) message.listener(network) if (network != null) message.listener(network)
} }
...@@ -64,9 +62,10 @@ object DefaultNetworkListener : CoroutineScope { ...@@ -64,9 +62,10 @@ object DefaultNetworkListener : CoroutineScope {
check(listeners.isNotEmpty()) { "Getting network without any listeners is not supported" } check(listeners.isNotEmpty()) { "Getting network without any listeners is not supported" }
if (network == null) pendingRequests += message else message.response.complete(network) if (network == null) pendingRequests += message else message.response.complete(network)
} }
is NetworkMessage.Stop -> { is NetworkMessage.Stop -> if (!listeners.isEmpty() && // was not empty
if (!listeners.isEmpty() && // was not empty listeners.remove(message.key) != null && listeners.isEmpty()) {
listeners.remove(message.key) != null && listeners.isEmpty()) unregisterDefaultNetworkListener() network = null
unregister()
} }
is NetworkMessage.Put -> { is NetworkMessage.Put -> {
...@@ -104,10 +103,10 @@ object DefaultNetworkListener : CoroutineScope { ...@@ -104,10 +103,10 @@ object DefaultNetworkListener : CoroutineScope {
private var fallback = false private var fallback = false
private val connectivity = app.getSystemService<ConnectivityManager>()!! private val connectivity = app.getSystemService<ConnectivityManager>()!!
private val defaultNetworkRequest = NetworkRequest.Builder() private val request = NetworkRequest.Builder().apply {
.addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
.addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_RESTRICTED) addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_RESTRICTED)
.build() }.build()
/** /**
* Unfortunately registerDefaultNetworkCallback is going to return VPN interface since Android P DP1: * Unfortunately registerDefaultNetworkCallback is going to return VPN interface since Android P DP1:
* https://android.googlesource.com/platform/frameworks/base/+/dda156ab0c5d66ad82bdcf76cda07cbc0a9c8a2e * https://android.googlesource.com/platform/frameworks/base/+/dda156ab0c5d66ad82bdcf76cda07cbc0a9c8a2e
...@@ -118,18 +117,18 @@ object DefaultNetworkListener : CoroutineScope { ...@@ -118,18 +117,18 @@ object DefaultNetworkListener : CoroutineScope {
* *
* Source: https://android.googlesource.com/platform/frameworks/base/+/2df4c7d/services/core/java/com/android/server/ConnectivityService.java#887 * Source: https://android.googlesource.com/platform/frameworks/base/+/2df4c7d/services/core/java/com/android/server/ConnectivityService.java#887
*/ */
private fun registerDefaultNetworkListener() { private fun register() {
if (Build.VERSION.SDK_INT in 24..27) @TargetApi(24) { if (Build.VERSION.SDK_INT in 24..27) @TargetApi(24) {
connectivity.registerDefaultNetworkCallback(Callback) connectivity.registerDefaultNetworkCallback(Callback)
} else try { } else try {
fallback = false fallback = false
// we want REQUEST here instead of LISTEN // we want REQUEST here instead of LISTEN
connectivity.requestNetwork(defaultNetworkRequest, Callback) connectivity.requestNetwork(request, Callback)
} catch (e: SecurityException) { } catch (e: SecurityException) {
// known bug: https://stackoverflow.com/a/33509180/2245107 // known bug: https://stackoverflow.com/a/33509180/2245107
if (Build.VERSION.SDK_INT != 23) Crashlytics.logException(e) if (Build.VERSION.SDK_INT != 23) Crashlytics.logException(e)
fallback = true fallback = true
} }
} }
private fun unregisterDefaultNetworkListener() = connectivity.unregisterNetworkCallback(Callback) private fun unregister() = connectivity.unregisterNetworkCallback(Callback)
} }
...@@ -20,6 +20,7 @@ ...@@ -20,6 +20,7 @@
package com.github.shadowsocks.net package com.github.shadowsocks.net
import android.os.Build
import android.os.SystemClock import android.os.SystemClock
import androidx.lifecycle.MutableLiveData import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
...@@ -29,13 +30,13 @@ import com.github.shadowsocks.acl.Acl ...@@ -29,13 +30,13 @@ import com.github.shadowsocks.acl.Acl
import com.github.shadowsocks.core.R import com.github.shadowsocks.core.R
import com.github.shadowsocks.preference.DataStore import com.github.shadowsocks.preference.DataStore
import com.github.shadowsocks.utils.Key import com.github.shadowsocks.utils.Key
import com.github.shadowsocks.utils.responseLength import com.github.shadowsocks.utils.disconnectFromMain
import kotlinx.coroutines.* import kotlinx.coroutines.*
import java.io.IOException import java.io.IOException
import java.net.HttpURLConnection import java.net.HttpURLConnection
import java.net.InetSocketAddress
import java.net.Proxy import java.net.Proxy
import java.net.URL import java.net.URL
import java.net.URLConnection
/** /**
* Based on: https://android.googlesource.com/platform/frameworks/base/+/b19a838/services/core/java/com/android/server/connectivity/NetworkMonitor.java#1071 * Based on: https://android.googlesource.com/platform/frameworks/base/+/b19a838/services/core/java/com/android/server/connectivity/NetworkMonitor.java#1071
...@@ -84,9 +85,9 @@ class HttpsTest : ViewModel() { ...@@ -84,9 +85,9 @@ class HttpsTest : ViewModel() {
Acl.CHINALIST -> "www.qualcomm.cn" Acl.CHINALIST -> "www.qualcomm.cn"
else -> "www.google.com" else -> "www.google.com"
}, "/generate_204") }, "/generate_204")
val conn = (if (DataStore.serviceMode == Key.modeVpn) url.openConnection() else val conn = (if (DataStore.serviceMode != Key.modeVpn) {
url.openConnection(Proxy(Proxy.Type.SOCKS, InetSocketAddress("127.0.0.1", DataStore.portProxy)))) url.openConnection(Proxy(Proxy.Type.SOCKS, DataStore.proxyAddress))
as HttpURLConnection } else url.openConnection()) as HttpURLConnection
conn.setRequestProperty("Connection", "close") conn.setRequestProperty("Connection", "close")
conn.instanceFollowRedirects = false conn.instanceFollowRedirects = false
conn.useCaches = false conn.useCaches = false
...@@ -109,7 +110,7 @@ class HttpsTest : ViewModel() { ...@@ -109,7 +110,7 @@ class HttpsTest : ViewModel() {
private fun cancelTest() = running?.let { (conn, job) -> private fun cancelTest() = running?.let { (conn, job) ->
job.cancel() // ensure job is cancelled before interrupting job.cancel() // ensure job is cancelled before interrupting
conn.disconnect() conn.disconnectFromMain()
running = null running = null
} }
...@@ -117,4 +118,7 @@ class HttpsTest : ViewModel() { ...@@ -117,4 +118,7 @@ class HttpsTest : ViewModel() {
cancelTest() cancelTest()
status.value = Status.Idle status.value = Status.Idle
} }
private val URLConnection.responseLength: Long
get() = if (Build.VERSION.SDK_INT >= 24) contentLengthLong else contentLength.toLong()
} }
/*******************************************************************************
* *
* Copyright (C) 2019 by Max Lv <max.c.lv@gmail.com> *
* Copyright (C) 2019 by Mygod Studio <contact-shadowsocks-android@mygod.be> *
* *
* This program is free software: you can redistribute it and/or modify *
* it under the terms of the GNU General Public License as published by *
* the Free Software Foundation, either version 3 of the License, or *
* (at your option) any later version. *
* *
* This program is distributed in the hope that it will be useful, *
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
* GNU General Public License for more details. *
* *
* You should have received a copy of the GNU General Public License *
* along with this program. If not, see <http://www.gnu.org/licenses/>. *
* *
*******************************************************************************/
package com.github.shadowsocks.net
import android.util.Log
import com.crashlytics.android.Crashlytics
import com.github.shadowsocks.utils.printLog
import kotlinx.coroutines.*
import org.xbill.DNS.*
import java.io.IOException
import java.net.*
import java.nio.ByteBuffer
import java.nio.channels.DatagramChannel
import java.nio.channels.SelectionKey
import java.nio.channels.SocketChannel
/**
* A simple DNS conditional forwarder.
*
* No cache is provided as localResolver may change from time to time. We expect DNS clients to do cache themselves.
*
* Based on:
* https://github.com/bitcoinj/httpseed/blob/809dd7ad9280f4bc98a356c1ffb3d627bf6c7ec5/src/main/kotlin/dns.kt
* https://github.com/shadowsocks/overture/tree/874f22613c334a3b78e40155a55479b7b69fee04
*/
class LocalDnsServer(private val localResolver: suspend (String) -> Array<InetAddress>,
private val remoteDns: Socks5Endpoint, private val proxy: SocketAddress) : CoroutineScope {
/**
* Forward all requests to remote and ignore localResolver.
*/
var forwardOnly = false
/**
* Forward UDP queries to TCP.
*/
var tcp = true
var remoteDomainMatcher: Regex? = null
var localIpMatcher: List<Subnet> = emptyList()
companion object {
private const val TAG = "LocalDnsServer"
private const val TIMEOUT = 10_000L
/**
* TTL returned from localResolver is set to 120. Android API does not provide TTL,
* so we suppose Android apps should not care about TTL either.
*/
private const val TTL = 120L
private const val UDP_PACKET_SIZE = 512
private fun prepareDnsResponse(request: Message) = Message(request.header.id).apply {
header.setFlag(Flags.QR.toInt()) // this is a response
if (request.header.getFlag(Flags.RD.toInt())) header.setFlag(Flags.RD.toInt())
addRecord(request.question, Section.QUESTION)
}
}
private val monitor = ChannelMonitor()
private val job = SupervisorJob()
override val coroutineContext = job + CoroutineExceptionHandler { _, t -> printLog(t) }
suspend fun start(listen: SocketAddress) = DatagramChannel.open().apply {
configureBlocking(false)
socket().bind(listen)
monitor.register(this, SelectionKey.OP_READ) { handlePacket(this) }
}
private fun handlePacket(channel: DatagramChannel) {
val buffer = ByteBuffer.allocateDirect(UDP_PACKET_SIZE)
val source = channel.receive(buffer)!!
buffer.flip()
launch {
val reply = resolve(buffer)
while (channel.send(reply, source) <= 0) monitor.wait(channel, SelectionKey.OP_WRITE)
}
}
private suspend fun resolve(packet: ByteBuffer): ByteBuffer {
val request = try {
Message(packet)
} catch (e: IOException) { // we cannot parse the message, do not attempt to handle it at all
printLog(e)
return forward(packet)
}
return coroutineScope {
val remote = async { withTimeout(TIMEOUT) { forward(packet) } }
try {
if (forwardOnly || request.header.opcode != Opcode.QUERY) return@coroutineScope remote.await()
val question = request.question
if (question?.type != Type.A) return@coroutineScope remote.await()
val host = question.name.toString(true)
if (remoteDomainMatcher?.containsMatchIn(host) == true) return@coroutineScope remote.await()
val localResults = try {
withTimeout(TIMEOUT) { GlobalScope.async(Dispatchers.IO) { localResolver(host) }.await() }
} catch (_: TimeoutCancellationException) {
Crashlytics.log(Log.WARN, TAG, "Local resolving timed out, falling back to remote resolving")
return@coroutineScope remote.await()
} catch (_: UnknownHostException) {
return@coroutineScope remote.await()
}
if (localResults.isEmpty()) return@coroutineScope remote.await()
if (localIpMatcher.isEmpty() || localIpMatcher.any { subnet -> localResults.any(subnet::matches) }) {
remote.cancel()
ByteBuffer.wrap(prepareDnsResponse(request).apply {
header.setFlag(Flags.RA.toInt()) // recursion available
for (address in localResults) addRecord(when (address) {
is Inet4Address -> ARecord(request.question.name, DClass.IN, TTL, address)
is Inet6Address -> AAAARecord(request.question.name, DClass.IN, TTL, address)
else -> throw IllegalStateException("Unsupported address $address")
}, Section.ANSWER)
}.toWire())
} else remote.await()
} catch (e: Exception) {
remote.cancel()
when (e) {
is TimeoutCancellationException -> Crashlytics.log(Log.WARN, TAG, "Remote resolving timed out")
is CancellationException -> { } // ignore
else -> printLog(e)
}
ByteBuffer.wrap(prepareDnsResponse(request).apply {
header.rcode = Rcode.SERVFAIL
}.toWire())
}
}
}
private suspend fun forward(packet: ByteBuffer): ByteBuffer {
packet.position(0) // the packet might have been parsed, reset to beginning
return if (tcp) SocketChannel.open().use { channel ->
channel.configureBlocking(false)
channel.connect(proxy)
val wrapped = remoteDns.tcpWrap(packet)
while (!channel.finishConnect()) monitor.wait(channel, SelectionKey.OP_CONNECT)
while (channel.write(wrapped) >= 0 && wrapped.hasRemaining()) monitor.wait(channel, SelectionKey.OP_WRITE)
val result = remoteDns.tcpReceiveBuffer(UDP_PACKET_SIZE)
remoteDns.tcpUnwrap(result, channel::read) { monitor.wait(channel, SelectionKey.OP_READ) }
result
} else DatagramChannel.open().use { channel ->
channel.configureBlocking(false)
monitor.wait(channel, SelectionKey.OP_WRITE)
check(channel.send(remoteDns.udpWrap(packet), proxy) > 0)
monitor.wait(channel, SelectionKey.OP_READ)
val result = remoteDns.udpReceiveBuffer(UDP_PACKET_SIZE)
check(channel.receive(result) == proxy)
result.flip()
remoteDns.udpUnwrap(result)
result
}
}
fun shutdown(scope: CoroutineScope) {
job.cancel()
monitor.close(scope)
scope.launch { job.join() }
}
}
...@@ -27,42 +27,53 @@ import android.system.ErrnoException ...@@ -27,42 +27,53 @@ import android.system.ErrnoException
import android.system.Os import android.system.Os
import android.system.OsConstants import android.system.OsConstants
import com.github.shadowsocks.utils.printLog import com.github.shadowsocks.utils.printLog
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.channels.sendBlocking
import kotlinx.coroutines.launch
import java.io.File import java.io.File
import java.io.IOException import java.io.IOException
abstract class LocalSocketListener(name: String, socketFile: File) : Thread(name), AutoCloseable { abstract class LocalSocketListener(name: String, socketFile: File) : Thread(name) {
private val localSocket = LocalSocket().apply { private val localSocket = LocalSocket().apply {
socketFile.delete() // It's a must-have to close and reuse previous local socket. socketFile.delete() // It's a must-have to close and reuse previous local socket.
bind(LocalSocketAddress(socketFile.absolutePath, LocalSocketAddress.Namespace.FILESYSTEM)) bind(LocalSocketAddress(socketFile.absolutePath, LocalSocketAddress.Namespace.FILESYSTEM))
} }
private val serverSocket = LocalServerSocket(localSocket.fileDescriptor) private val serverSocket = LocalServerSocket(localSocket.fileDescriptor)
private val closeChannel = Channel<Unit>(1)
@Volatile @Volatile
private var running = true protected var running = true
/** /**
* Inherited class do not need to close input/output streams as they will be closed automatically. * Inherited class do not need to close input/output streams as they will be closed automatically.
*/ */
protected open fun accept(socket: LocalSocket) = socket.use { acceptInternal(socket) } protected open fun accept(socket: LocalSocket) = socket.use { acceptInternal(socket) }
protected abstract fun acceptInternal(socket: LocalSocket) protected abstract fun acceptInternal(socket: LocalSocket)
final override fun run() = localSocket.use { final override fun run() {
while (running) { localSocket.use {
try { while (running) {
accept(serverSocket.accept()) try {
} catch (e: IOException) { accept(serverSocket.accept())
if (running) printLog(e) } catch (e: IOException) {
continue if (running) printLog(e)
continue
}
} }
} }
closeChannel.sendBlocking(Unit)
} }
override fun close() { open fun shutdown(scope: CoroutineScope) {
running = false running = false
// see also: https://issuetracker.google.com/issues/36945762#comment15 localSocket.fileDescriptor.apply {
try { // see also: https://issuetracker.google.com/issues/36945762#comment15
Os.shutdown(localSocket.fileDescriptor, OsConstants.SHUT_RDWR) if (valid()) try {
} catch (e: ErrnoException) { Os.shutdown(this, OsConstants.SHUT_RDWR)
if (e.errno != OsConstants.EBADF) throw e // suppress fd already closed } catch (e: ErrnoException) {
// suppress fd inactive or already closed
if (e.errno != OsConstants.EBADF && e.errno != OsConstants.ENOTCONN) throw IOException(e)
}
} }
join() scope.launch { closeChannel.receive() }
} }
} }
/*******************************************************************************
* *
* Copyright (C) 2019 by Max Lv <max.c.lv@gmail.com> *
* Copyright (C) 2019 by Mygod Studio <contact-shadowsocks-android@mygod.be> *
* *
* This program is free software: you can redistribute it and/or modify *
* it under the terms of the GNU General Public License as published by *
* the Free Software Foundation, either version 3 of the License, or *
* (at your option) any later version. *
* *
* This program is distributed in the hope that it will be useful, *
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
* GNU General Public License for more details. *
* *
* You should have received a copy of the GNU General Public License *
* along with this program. If not, see <http://www.gnu.org/licenses/>. *
* *
*******************************************************************************/
package com.github.shadowsocks.net
import com.github.shadowsocks.utils.parseNumericAddress
import net.sourceforge.jsocks.Socks4Message
import net.sourceforge.jsocks.Socks5Message
import java.io.IOException
import java.net.Inet4Address
import java.net.Inet6Address
import java.nio.ByteBuffer
import kotlin.math.max
class Socks5Endpoint(host: String, port: Int) {
private val dest = host.parseNumericAddress().let { numeric ->
val bytes = numeric?.address ?: host.toByteArray().apply { check(size < 256) { "Hostname too long" } }
val type = when (numeric) {
null -> Socks5Message.SOCKS_ATYP_DOMAINNAME
is Inet4Address -> Socks5Message.SOCKS_ATYP_IPV4
is Inet6Address -> Socks5Message.SOCKS_ATYP_IPV6
else -> throw IllegalStateException("Unsupported address type")
}
ByteBuffer.allocate(bytes.size + (if (numeric == null) 1 else 0) + 3).apply {
put(type.toByte())
if (numeric == null) put(bytes.size.toByte())
put(bytes)
putShort(port.toShort())
}
}.array()
private val headerReserved = max(3 + 3 + 16, 3 + dest.size)
fun tcpWrap(message: ByteBuffer): ByteBuffer {
check(message.remaining() < 65536) { "TCP message too large" }
return ByteBuffer.allocateDirect(8 + dest.size + message.remaining()).apply {
put(Socks5Message.SOCKS_VERSION.toByte())
put(1) // nmethods
put(0) // no authentication required
// header
put(Socks5Message.SOCKS_VERSION.toByte())
put(Socks4Message.REQUEST_CONNECT.toByte())
put(0) // reserved
put(dest)
// data
putShort(message.remaining().toShort())
put(message)
flip()
}
}
fun tcpReceiveBuffer(size: Int) = ByteBuffer.allocateDirect(headerReserved + 4 + size)
suspend fun tcpUnwrap(buffer: ByteBuffer, reader: (ByteBuffer) -> Int, wait: suspend () -> Unit) {
suspend fun readBytes(till: Int) {
if (buffer.position() >= till) return
while (reader(buffer) >= 0 && buffer.position() < till) wait()
if (buffer.position() < till) throw IOException("EOF")
}
suspend fun read(index: Int): Byte {
readBytes(index + 1)
return buffer[index]
}
check(read(0) == Socks5Message.SOCKS_VERSION.toByte()) { "Unsupported SOCKS version" }
if (read(1) != 0.toByte()) throw IOException("Unsupported authentication ${buffer[1]}")
check(read(2) == Socks5Message.SOCKS_VERSION.toByte()) { "Unsupported SOCKS version" }
if (read(3) != 0.toByte()) throw IOException("SOCKS5 server returned error ${buffer[3]}")
val dataOffset = when (read(5)) {
Socks5Message.SOCKS_ATYP_IPV4.toByte() -> 4
Socks5Message.SOCKS_ATYP_DOMAINNAME.toByte() -> 1 + read(6)
Socks5Message.SOCKS_ATYP_IPV6.toByte() -> 16
else -> throw IllegalStateException("Unsupported address type ${buffer[5]}")
} + 8
readBytes(dataOffset + 2)
buffer.limit(buffer.position()) // store old position to update mark
buffer.position(dataOffset)
val dataLength = buffer.short.toUShort().toInt()
val end = buffer.position() + dataLength
check(end <= buffer.capacity()) { "Buffer too small to contain the message" }
buffer.mark()
buffer.position(buffer.limit()) // restore old position
buffer.limit(end)
readBytes(buffer.limit())
buffer.reset()
}
fun udpWrap(packet: ByteBuffer) = ByteBuffer.allocateDirect(3 + dest.size + packet.remaining()).apply {
// header
putShort(0) // reserved
put(0) // fragment number
put(dest)
// data
put(packet)
flip()
}
fun udpReceiveBuffer(size: Int) = ByteBuffer.allocateDirect(headerReserved + size)
fun udpUnwrap(packet: ByteBuffer) {
packet.position(3)
packet.position(6 + when (packet.get()) {
Socks5Message.SOCKS_ATYP_IPV4.toByte() -> 4
Socks5Message.SOCKS_ATYP_DOMAINNAME.toByte() -> 1 + packet.get()
Socks5Message.SOCKS_ATYP_IPV6.toByte() -> 16
else -> throw IllegalStateException("Unsupported address type")
})
packet.mark()
}
}
...@@ -45,6 +45,21 @@ class Subnet(val address: InetAddress, val prefixSize: Int) : Comparable<Subnet> ...@@ -45,6 +45,21 @@ class Subnet(val address: InetAddress, val prefixSize: Int) : Comparable<Subnet>
if (prefixSize < 0 || prefixSize > addressLength) throw IllegalArgumentException("prefixSize: $prefixSize") if (prefixSize < 0 || prefixSize > addressLength) throw IllegalArgumentException("prefixSize: $prefixSize")
} }
fun matches(other: InetAddress): Boolean {
if (address.javaClass != other.javaClass) return false
// TODO optimize?
val a = address.address
val b = other.address
var i = 0
while (i * 8 < prefixSize && i * 8 + 8 <= prefixSize) {
if (a[i] != b[i]) return false
++i
}
if (i * 8 == prefixSize) return true
val mask = 256 - (1 shl (i * 8 + 8 - prefixSize))
return (a[i].toInt() and mask) == (b[i].toInt() and mask)
}
override fun toString(): String = override fun toString(): String =
if (prefixSize == addressLength) address.hostAddress else address.hostAddress + '/' + prefixSize if (prefixSize == addressLength) address.hostAddress else address.hostAddress + '/' + prefixSize
......
...@@ -25,10 +25,11 @@ import androidx.preference.PreferenceDataStore ...@@ -25,10 +25,11 @@ import androidx.preference.PreferenceDataStore
import com.github.shadowsocks.Core import com.github.shadowsocks.Core
import com.github.shadowsocks.database.PrivateDatabase import com.github.shadowsocks.database.PrivateDatabase
import com.github.shadowsocks.database.PublicDatabase import com.github.shadowsocks.database.PublicDatabase
import com.github.shadowsocks.net.TcpFastOpen
import com.github.shadowsocks.utils.DirectBoot import com.github.shadowsocks.utils.DirectBoot
import com.github.shadowsocks.utils.Key import com.github.shadowsocks.utils.Key
import com.github.shadowsocks.net.TcpFastOpen
import com.github.shadowsocks.utils.parsePort import com.github.shadowsocks.utils.parsePort
import java.net.InetSocketAddress
import java.net.NetworkInterface import java.net.NetworkInterface
import java.net.SocketException import java.net.SocketException
...@@ -69,7 +70,7 @@ object DataStore : OnPreferenceDataStoreChangeListener { ...@@ -69,7 +70,7 @@ object DataStore : OnPreferenceDataStoreChangeListener {
* An alternative way to detect this interface could be checking MAC address = 00:ff:aa:00:00:55, but there is no * An alternative way to detect this interface could be checking MAC address = 00:ff:aa:00:00:55, but there is no
* reliable way of getting MAC address for now. * reliable way of getting MAC address for now.
*/ */
private val hasArc0 by lazy { val hasArc0 by lazy {
var retry = 0 var retry = 0
while (retry < 5) { while (retry < 5) {
try { try {
...@@ -88,6 +89,7 @@ object DataStore : OnPreferenceDataStoreChangeListener { ...@@ -88,6 +89,7 @@ object DataStore : OnPreferenceDataStoreChangeListener {
var portProxy: Int var portProxy: Int
get() = getLocalPort(Key.portProxy, 1080) get() = getLocalPort(Key.portProxy, 1080)
set(value) = publicStore.putString(Key.portProxy, value.toString()) set(value) = publicStore.putString(Key.portProxy, value.toString())
val proxyAddress get() = InetSocketAddress("127.0.0.1", portProxy)
var portLocalDns: Int var portLocalDns: Int
get() = getLocalPort(Key.portLocalDns, 5450) get() = getLocalPort(Key.portLocalDns, 5450)
set(value) = publicStore.putString(Key.portLocalDns, value.toString()) set(value) = publicStore.putString(Key.portLocalDns, value.toString())
......
...@@ -36,8 +36,11 @@ import android.util.TypedValue ...@@ -36,8 +36,11 @@ import android.util.TypedValue
import androidx.annotation.AttrRes import androidx.annotation.AttrRes
import androidx.preference.Preference import androidx.preference.Preference
import com.crashlytics.android.Crashlytics import com.crashlytics.android.Crashlytics
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import java.net.HttpURLConnection
import java.net.InetAddress import java.net.InetAddress
import java.net.URLConnection
private val parseNumericAddress by lazy { private val parseNumericAddress by lazy {
InetAddress::class.java.getDeclaredMethod("parseNumericAddress", String::class.java).apply { InetAddress::class.java.getDeclaredMethod("parseNumericAddress", String::class.java).apply {
...@@ -52,6 +55,10 @@ private val parseNumericAddress by lazy { ...@@ -52,6 +55,10 @@ private val parseNumericAddress by lazy {
fun String?.parseNumericAddress(): InetAddress? = Os.inet_pton(OsConstants.AF_INET, this) fun String?.parseNumericAddress(): InetAddress? = Os.inet_pton(OsConstants.AF_INET, this)
?: Os.inet_pton(OsConstants.AF_INET6, this)?.let { parseNumericAddress.invoke(null, this) as InetAddress } ?: Os.inet_pton(OsConstants.AF_INET6, this)?.let { parseNumericAddress.invoke(null, this) as InetAddress }
fun HttpURLConnection.disconnectFromMain() {
if (Build.VERSION.SDK_INT >= 26) disconnect() else GlobalScope.launch(Dispatchers.IO) { disconnect() }
}
fun parsePort(str: String?, default: Int, min: Int = 1025): Int { fun parsePort(str: String?, default: Int, min: Int = 1025): Int {
val value = str?.toIntOrNull() ?: default val value = str?.toIntOrNull() ?: default
return if (value < min || value > 65535) default else value return if (value < min || value > 65535) default else value
...@@ -61,9 +68,6 @@ fun broadcastReceiver(callback: (Context, Intent) -> Unit): BroadcastReceiver = ...@@ -61,9 +68,6 @@ fun broadcastReceiver(callback: (Context, Intent) -> Unit): BroadcastReceiver =
override fun onReceive(context: Context, intent: Intent) = callback(context, intent) override fun onReceive(context: Context, intent: Intent) = callback(context, intent)
} }
val URLConnection.responseLength: Long
get() = if (Build.VERSION.SDK_INT >= 24) contentLengthLong else contentLength.toLong()
fun ContentResolver.openBitmap(uri: Uri) = fun ContentResolver.openBitmap(uri: Uri) =
if (Build.VERSION.SDK_INT >= 28) ImageDecoder.decodeBitmap(ImageDecoder.createSource(this, uri)) if (Build.VERSION.SDK_INT >= 28) ImageDecoder.decodeBitmap(ImageDecoder.createSource(this, uri))
else BitmapFactory.decodeStream(openInputStream(uri)) else BitmapFactory.decodeStream(openInputStream(uri))
......
...@@ -278,39 +278,6 @@ LOCAL_LDLIBS := -llog ...@@ -278,39 +278,6 @@ LOCAL_LDLIBS := -llog
include $(BUILD_SHARED_EXECUTABLE) include $(BUILD_SHARED_EXECUTABLE)
########################################################
## shadowsocks-libev tunnel
########################################################
include $(CLEAR_VARS)
SHADOWSOCKS_SOURCES := tunnel.c \
cache.c udprelay.c utils.c netutils.c json.c jconf.c \
crypto.c aead.c stream.c base64.c \
plugin.c ppbloom.c \
android.c
LOCAL_MODULE := ss-tunnel
LOCAL_SRC_FILES := $(addprefix shadowsocks-libev/src/, $(SHADOWSOCKS_SOURCES))
LOCAL_CFLAGS := -Wall -fno-strict-aliasing -DMODULE_TUNNEL \
-DUSE_CRYPTO_MBEDTLS -DHAVE_CONFIG_H -DSSTUNNEL_JNI \
-DCONNECT_IN_PROGRESS=EINPROGRESS \
-I$(LOCAL_PATH)/libancillary \
-I$(LOCAL_PATH)/include \
-I$(LOCAL_PATH)/libsodium/src/libsodium/include \
-I$(LOCAL_PATH)/libsodium/src/libsodium/include/sodium \
-I$(LOCAL_PATH)/mbedtls/include \
-I$(LOCAL_PATH)/libev \
-I$(LOCAL_PATH)/shadowsocks-libev/libcork/include \
-I$(LOCAL_PATH)/shadowsocks-libev/libbloom \
-I$(LOCAL_PATH)/include/shadowsocks-libev
LOCAL_STATIC_LIBRARIES := libev libmbedtls libsodium libcork libbloom libancillary
LOCAL_LDLIBS := -llog
include $(BUILD_SHARED_EXECUTABLE)
######################################################## ########################################################
## tun2socks ## tun2socks
######################################################## ########################################################
...@@ -358,7 +325,6 @@ TUN2SOCKS_SOURCES := \ ...@@ -358,7 +325,6 @@ TUN2SOCKS_SOURCES := \
flow/PacketProtoDecoder.c \ flow/PacketProtoDecoder.c \
socksclient/BSocksClient.c \ socksclient/BSocksClient.c \
tuntap/BTap.c \ tuntap/BTap.c \
lwip/src/core/timers.c \
lwip/src/core/udp.c \ lwip/src/core/udp.c \
lwip/src/core/memp.c \ lwip/src/core/memp.c \
lwip/src/core/init.c \ lwip/src/core/init.c \
...@@ -367,14 +333,16 @@ TUN2SOCKS_SOURCES := \ ...@@ -367,14 +333,16 @@ TUN2SOCKS_SOURCES := \
lwip/src/core/tcp_out.c \ lwip/src/core/tcp_out.c \
lwip/src/core/netif.c \ lwip/src/core/netif.c \
lwip/src/core/def.c \ lwip/src/core/def.c \
lwip/src/core/ip.c \
lwip/src/core/mem.c \ lwip/src/core/mem.c \
lwip/src/core/tcp_in.c \ lwip/src/core/tcp_in.c \
lwip/src/core/stats.c \ lwip/src/core/stats.c \
lwip/src/core/inet_chksum.c \ lwip/src/core/inet_chksum.c \
lwip/src/core/timeouts.c \
lwip/src/core/ipv4/icmp.c \ lwip/src/core/ipv4/icmp.c \
lwip/src/core/ipv4/igmp.c \ lwip/src/core/ipv4/igmp.c \
lwip/src/core/ipv4/ip4_addr.c \ lwip/src/core/ipv4/ip4_addr.c \
lwip/src/core/ipv4/ip_frag.c \ lwip/src/core/ipv4/ip4_frag.c \
lwip/src/core/ipv4/ip4.c \ lwip/src/core/ipv4/ip4.c \
lwip/src/core/ipv4/autoip.c \ lwip/src/core/ipv4/autoip.c \
lwip/src/core/ipv6/ethip6.c \ lwip/src/core/ipv6/ethip6.c \
......
Subproject commit 4c0050878618423da53e7e335fbb55ec2af6be06 Subproject commit ba4c45d6b4d30fd6b2557e92a770ccae12ec9257
Subproject commit 6d1e18544504c41600d9d682aa980eaa24448558
...@@ -57,8 +57,8 @@ ...@@ -57,8 +57,8 @@
<string name="tcp_fastopen_summary">Toggling might require ROOT permission</string> <string name="tcp_fastopen_summary">Toggling might require ROOT permission</string>
<string name="tcp_fastopen_summary_unsupported">Unsupported kernel version: %s &lt; 3.7.1</string> <string name="tcp_fastopen_summary_unsupported">Unsupported kernel version: %s &lt; 3.7.1</string>
<string name="tcp_fastopen_failure">Toggle failed</string> <string name="tcp_fastopen_failure">Toggle failed</string>
<string name="udp_dns">DNS Forwarding</string> <string name="udp_dns">Send DNS over UDP</string>
<string name="udp_dns_summary">Forward all DNS requests to remote</string> <string name="udp_dns_summary">Requires UDP forwarding on server side</string>
<string name="udp_fallback">UDP Fallback</string> <string name="udp_fallback">UDP Fallback</string>
<!-- notification category --> <!-- notification category -->
......
...@@ -11,7 +11,7 @@ ...@@ -11,7 +11,7 @@
# The setting is particularly useful for tweaking memory settings. # The setting is particularly useful for tweaking memory settings.
android.enableJetifier=true android.enableJetifier=true
android.enableR8=true android.enableR8=true
android.enableR8.fullMode=true # android.enableR8.fullMode=true
android.useAndroidX=true android.useAndroidX=true
# When configured, Gradle will run in incubating parallel mode. # When configured, Gradle will run in incubating parallel mode.
......
...@@ -2,4 +2,4 @@ distributionBase=GRADLE_USER_HOME ...@@ -2,4 +2,4 @@ distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-5.1.1-all.zip distributionUrl=https\://services.gradle.org/distributions/gradle-5.2-all.zip
...@@ -24,7 +24,6 @@ android { ...@@ -24,7 +24,6 @@ android {
targetSdkVersion rootProject.sdkVersion targetSdkVersion rootProject.sdkVersion
versionCode rootProject.versionCode versionCode rootProject.versionCode
versionName rootProject.versionName versionName rootProject.versionName
testApplicationId "com.github.shadowsocks.test"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
resConfigs "fa", "fr", "ja", "ko", "ru", "tr", "zh-rCN", "zh-rTW" resConfigs "fa", "fr", "ja", "ko", "ru", "tr", "zh-rCN", "zh-rTW"
} }
...@@ -48,14 +47,13 @@ android { ...@@ -48,14 +47,13 @@ android {
} }
sourceSets.main.jniLibs.srcDirs += sourceSets.main.jniLibs.srcDirs +=
new File(project(':core').buildDir, "intermediates/bundles/${getCurrentFlavor()}/jni") new File(project(':core').buildDir, "intermediates/bundles/${getCurrentFlavor()}/jni")
sourceSets.main.jniLibs.srcDirs += new File(project(':core').projectDir, "build/go")
} }
dependencies { dependencies {
implementation project(':core') implementation project(':core')
implementation "androidx.browser:browser:1.0.0" implementation "androidx.browser:browser:1.0.0"
implementation 'com.google.android.gms:play-services-vision:17.0.2' implementation 'com.google.android.gms:play-services-vision:17.0.2'
implementation 'com.google.firebase:firebase-ads:17.1.2' implementation 'com.google.firebase:firebase-ads:17.1.3'
implementation "com.takisoft.preferencex:preferencex-simplemenu:$preferencexVersion" implementation "com.takisoft.preferencex:preferencex-simplemenu:$preferencexVersion"
implementation 'com.twofortyfouram:android-plugin-api-for-locale:1.0.4' implementation 'com.twofortyfouram:android-plugin-api-for-locale:1.0.4'
implementation 'net.glxn.qrgen:android:2.0' implementation 'net.glxn.qrgen:android:2.0'
......
...@@ -49,7 +49,6 @@ import com.github.shadowsocks.aidl.IShadowsocksService ...@@ -49,7 +49,6 @@ import com.github.shadowsocks.aidl.IShadowsocksService
import com.github.shadowsocks.aidl.ShadowsocksConnection import com.github.shadowsocks.aidl.ShadowsocksConnection
import com.github.shadowsocks.aidl.TrafficStats import com.github.shadowsocks.aidl.TrafficStats
import com.github.shadowsocks.bg.BaseService import com.github.shadowsocks.bg.BaseService
import com.github.shadowsocks.bg.Executable
import com.github.shadowsocks.database.Profile import com.github.shadowsocks.database.Profile
import com.github.shadowsocks.database.ProfileManager import com.github.shadowsocks.database.ProfileManager
import com.github.shadowsocks.preference.DataStore import com.github.shadowsocks.preference.DataStore
...@@ -135,7 +134,6 @@ class MainActivity : AppCompatActivity(), ShadowsocksConnection.Callback, OnPref ...@@ -135,7 +134,6 @@ class MainActivity : AppCompatActivity(), ShadowsocksConnection.Callback, OnPref
override fun onServiceDisconnected() = changeState(BaseService.IDLE) override fun onServiceDisconnected() = changeState(BaseService.IDLE)
override fun onBinderDied() { override fun onBinderDied() {
connection.disconnect(this) connection.disconnect(this)
Executable.killAll()
connection.connect(this, this) connection.connect(this, this)
} }
......
...@@ -26,7 +26,6 @@ ...@@ -26,7 +26,6 @@
<li>pcre: <a href="https://android.googlesource.com/platform/external/pcre/+/master/dist2/LICENCE">BSD</a>&nbsp;</li> <li>pcre: <a href="https://android.googlesource.com/platform/external/pcre/+/master/dist2/LICENCE">BSD</a>&nbsp;</li>
<li>libancillary: <a href="https://github.com/shadowsocks/libancillary/blob/shadowsocks-android/COPYING">BSD</a>&nbsp;</li> <li>libancillary: <a href="https://github.com/shadowsocks/libancillary/blob/shadowsocks-android/COPYING">BSD</a>&nbsp;</li>
<li>shadowsocks-libev: <a href="https://github.com/shadowsocks/shadowsocks-libev/blob/master/LICENSE">GPLv3</a>&nbsp;</li> <li>shadowsocks-libev: <a href="https://github.com/shadowsocks/shadowsocks-libev/blob/master/LICENSE">GPLv3</a>&nbsp;</li>
<li>overture: <a href="https://github.com/shawn1m/overture/blob/master/LICENSE">MIT</a>&nbsp;</li>
<li>libev: <a href="https://github.com/shadowsocks/libev/blob/master/LICENSE">GPLv2</a>&nbsp;</li> <li>libev: <a href="https://github.com/shadowsocks/libev/blob/master/LICENSE">GPLv2</a>&nbsp;</li>
<li>libsodium: <a href="https://github.com/jedisct1/libsodium/blob/master/LICENSE">ISC</a>&nbsp;</li> <li>libsodium: <a href="https://github.com/jedisct1/libsodium/blob/master/LICENSE">ISC</a>&nbsp;</li>
</ul> </ul>
......
...@@ -25,7 +25,6 @@ android { ...@@ -25,7 +25,6 @@ android {
targetSdkVersion rootProject.sdkVersion targetSdkVersion rootProject.sdkVersion
versionCode rootProject.versionCode versionCode rootProject.versionCode
versionName rootProject.versionName versionName rootProject.versionName
testApplicationId "com.github.shadowsocks.tv.test"
resConfigs "fa", "fr", "ja", "ko", "ru", "tr", "zh-rCN", "zh-rTW" resConfigs "fa", "fr", "ja", "ko", "ru", "tr", "zh-rCN", "zh-rTW"
} }
buildTypes { buildTypes {
...@@ -48,7 +47,6 @@ android { ...@@ -48,7 +47,6 @@ android {
} }
sourceSets.main.jniLibs.srcDirs += sourceSets.main.jniLibs.srcDirs +=
new File(project(':core').buildDir, "intermediates/bundles/${getCurrentFlavor()}/jni") new File(project(':core').buildDir, "intermediates/bundles/${getCurrentFlavor()}/jni")
sourceSets.main.jniLibs.srcDirs += new File(project(':core').projectDir, "build/go")
} }
dependencies { dependencies {
......
...@@ -47,7 +47,6 @@ import com.github.shadowsocks.aidl.IShadowsocksService ...@@ -47,7 +47,6 @@ import com.github.shadowsocks.aidl.IShadowsocksService
import com.github.shadowsocks.aidl.ShadowsocksConnection import com.github.shadowsocks.aidl.ShadowsocksConnection
import com.github.shadowsocks.aidl.TrafficStats import com.github.shadowsocks.aidl.TrafficStats
import com.github.shadowsocks.bg.BaseService import com.github.shadowsocks.bg.BaseService
import com.github.shadowsocks.bg.Executable
import com.github.shadowsocks.database.Profile import com.github.shadowsocks.database.Profile
import com.github.shadowsocks.database.ProfileManager import com.github.shadowsocks.database.ProfileManager
import com.github.shadowsocks.net.HttpsTest import com.github.shadowsocks.net.HttpsTest
...@@ -151,7 +150,6 @@ class MainPreferenceFragment : LeanbackPreferenceFragmentCompat(), ShadowsocksCo ...@@ -151,7 +150,6 @@ class MainPreferenceFragment : LeanbackPreferenceFragmentCompat(), ShadowsocksCo
override fun onServiceDisconnected() = changeState(BaseService.IDLE) override fun onServiceDisconnected() = changeState(BaseService.IDLE)
override fun onBinderDied() { override fun onBinderDied() {
connection.disconnect(requireContext()) connection.disconnect(requireContext())
Executable.killAll()
connection.connect(requireContext(), this) connection.connect(requireContext(), this)
} }
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment