Initial draft for porting this project to Gradle + Kotlin
This is one of my stupidest idea ever.
This commit is contained in:
@@ -0,0 +1,403 @@
|
||||
/*******************************************************************************
|
||||
* *
|
||||
* Copyright (C) 2017 by Max Lv <max.c.lv@gmail.com> *
|
||||
* Copyright (C) 2017 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.bg
|
||||
|
||||
import android.app.Service
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.IntentFilter
|
||||
import android.os.*
|
||||
import android.text.TextUtils
|
||||
import android.util.Base64
|
||||
import android.util.Log
|
||||
import android.widget.Toast
|
||||
import com.github.shadowsocks.App.Companion.app
|
||||
import com.github.shadowsocks.R
|
||||
import com.github.shadowsocks.acl.Acl
|
||||
import com.github.shadowsocks.acl.AclSyncJob
|
||||
import com.github.shadowsocks.aidl.IShadowsocksService
|
||||
import com.github.shadowsocks.aidl.IShadowsocksServiceCallback
|
||||
import com.github.shadowsocks.database.Profile
|
||||
import com.github.shadowsocks.database.ProfileManager
|
||||
import com.github.shadowsocks.plugin.PluginConfiguration
|
||||
import com.github.shadowsocks.plugin.PluginManager
|
||||
import com.github.shadowsocks.plugin.PluginOptions
|
||||
import com.github.shadowsocks.preference.DataStore
|
||||
import com.github.shadowsocks.utils.*
|
||||
import okhttp3.FormBody
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Request
|
||||
import org.json.JSONObject
|
||||
import java.io.File
|
||||
import java.net.UnknownHostException
|
||||
import java.security.MessageDigest
|
||||
import java.util.*
|
||||
import java.util.concurrent.TimeUnit
|
||||
import kotlin.reflect.KClass
|
||||
|
||||
/**
|
||||
* This object uses WeakMap to simulate the effects of multi-inheritance.
|
||||
*/
|
||||
object BaseService {
|
||||
/**
|
||||
* IDLE state is only used by UI and will never be returned by BaseService.
|
||||
*/
|
||||
const val IDLE = 0
|
||||
const val CONNECTING = 1
|
||||
const val CONNECTED = 2
|
||||
const val STOPPING = 3
|
||||
const val STOPPED = 4
|
||||
|
||||
class Data internal constructor(private val service: Interface) {
|
||||
@Volatile var profile: Profile? = null
|
||||
@Volatile var state = STOPPED
|
||||
@Volatile var plugin = PluginOptions()
|
||||
@Volatile var pluginPath: String? = null
|
||||
var sslocalProcess: GuardedProcess? = null
|
||||
|
||||
var timer: Timer? = null
|
||||
var trafficMonitorThread: TrafficMonitorThread? = null
|
||||
|
||||
val callbacks = RemoteCallbackList<IShadowsocksServiceCallback>()
|
||||
val bandwidthListeners = HashSet<IBinder>() // the binder is the real identifier
|
||||
|
||||
var notification: ServiceNotification? = null
|
||||
val closeReceiver = broadcastReceiver { _, intent ->
|
||||
when (intent.action) {
|
||||
Action.RELOAD -> service.forceLoad()
|
||||
else -> {
|
||||
Toast.makeText(service as Context, R.string.stopping, Toast.LENGTH_SHORT).show()
|
||||
service.stopRunner(true)
|
||||
}
|
||||
}
|
||||
}
|
||||
var closeReceiverRegistered = false
|
||||
|
||||
private fun state() = state
|
||||
|
||||
val binder = object : IShadowsocksService.Stub() {
|
||||
override fun getState(): Int = state()
|
||||
override fun getProfileName(): String = profile?.formattedName ?: "Idle"
|
||||
|
||||
override fun registerCallback(cb: IShadowsocksServiceCallback) {
|
||||
callbacks.register(cb)
|
||||
}
|
||||
override fun startListeningForBandwidth(cb: IShadowsocksServiceCallback) {
|
||||
if (bandwidthListeners.add(cb.asBinder())) {
|
||||
if (timer == null) {
|
||||
val t = Timer(true)
|
||||
t.schedule(object : TimerTask() {
|
||||
override fun run() {
|
||||
if (state == CONNECTED && TrafficMonitor.updateRate()) app.handler.post {
|
||||
if (bandwidthListeners.isNotEmpty()) {
|
||||
val txRate = TrafficMonitor.txRate
|
||||
val rxRate = TrafficMonitor.rxRate
|
||||
val txTotal = TrafficMonitor.txTotal
|
||||
val rxTotal = TrafficMonitor.rxTotal
|
||||
val n = callbacks.beginBroadcast()
|
||||
for (i in 0 until n) try {
|
||||
val item = callbacks.getBroadcastItem(i)
|
||||
if (bandwidthListeners.contains(item.asBinder()))
|
||||
item.trafficUpdated(profile!!.id, txRate, rxRate, txTotal, rxTotal)
|
||||
} catch (_: Exception) { } // ignore
|
||||
callbacks.finishBroadcast()
|
||||
}
|
||||
}
|
||||
}
|
||||
}, 1000, 1000)
|
||||
timer = t
|
||||
}
|
||||
TrafficMonitor.updateRate()
|
||||
if (state == CONNECTED) cb.trafficUpdated(profile!!.id,
|
||||
TrafficMonitor.txRate, TrafficMonitor.rxRate,
|
||||
TrafficMonitor.txTotal, TrafficMonitor.rxTotal)
|
||||
}
|
||||
}
|
||||
|
||||
override fun stopListeningForBandwidth(cb: IShadowsocksServiceCallback) {
|
||||
if (bandwidthListeners.remove(cb.asBinder()) && bandwidthListeners.isEmpty()) {
|
||||
timer!!.cancel()
|
||||
timer = null
|
||||
}
|
||||
}
|
||||
|
||||
override fun unregisterCallback(cb: IShadowsocksServiceCallback) {
|
||||
stopListeningForBandwidth(cb) // saves an RPC, and safer
|
||||
callbacks.unregister(cb)
|
||||
}
|
||||
}
|
||||
|
||||
internal fun updateTrafficTotal(tx: Long, rx: Long) {
|
||||
val profile = profile ?: return
|
||||
val p = ProfileManager.getProfile(profile.id) ?: return // profile may have host, etc. modified
|
||||
p.tx += tx
|
||||
p.rx += rx
|
||||
ProfileManager.updateProfile(p)
|
||||
app.handler.post {
|
||||
if (bandwidthListeners.isNotEmpty()) {
|
||||
val n = callbacks.beginBroadcast()
|
||||
for (i in 0 until n) {
|
||||
try {
|
||||
val item = callbacks.getBroadcastItem(i)
|
||||
if (bandwidthListeners.contains(item.asBinder())) item.trafficPersisted(p.id)
|
||||
} catch (_: Exception) { } // ignore
|
||||
}
|
||||
callbacks.finishBroadcast()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal fun buildShadowsocksConfig() {
|
||||
val profile = profile!!
|
||||
val config = JSONObject()
|
||||
.put("server", profile.host)
|
||||
.put("server_port", profile.remotePort)
|
||||
.put("password", profile.password)
|
||||
.put("method", profile.method)
|
||||
val pluginPath = pluginPath
|
||||
if (pluginPath != null) {
|
||||
val pluginCmd = arrayListOf(pluginPath)
|
||||
if (TcpFastOpen.sendEnabled) pluginCmd.add("--fast-open")
|
||||
config
|
||||
.put("plugin", Commandline.toString(service.buildAdditionalArguments(pluginCmd)))
|
||||
.put("plugin_opts", plugin.toString())
|
||||
}
|
||||
File(app.filesDir, "shadowsocks.json").bufferedWriter().use { it.write(config.toString()) }
|
||||
}
|
||||
}
|
||||
interface Interface {
|
||||
val tag: String
|
||||
|
||||
fun onBind(intent: Intent): IBinder? = if (intent.action == Action.SERVICE) data.binder else null
|
||||
|
||||
fun checkProfile(profile: Profile): Boolean =
|
||||
if (TextUtils.isEmpty(profile.host) || TextUtils.isEmpty(profile.password)) {
|
||||
stopRunner(true, (this as Context).getString(R.string.proxy_empty))
|
||||
false
|
||||
} else true
|
||||
fun forceLoad() {
|
||||
val p = app.currentProfile
|
||||
?: return stopRunner(true, (this as Context).getString(R.string.profile_empty))
|
||||
if (!checkProfile(p)) return
|
||||
val s = data.state
|
||||
when (s) {
|
||||
STOPPED -> startRunner()
|
||||
CONNECTED -> {
|
||||
stopRunner(false)
|
||||
startRunner()
|
||||
}
|
||||
else -> Log.w(tag, "Illegal state when invoking use: $s")
|
||||
}
|
||||
}
|
||||
|
||||
fun buildAdditionalArguments(cmd: ArrayList<String>): ArrayList<String> = cmd
|
||||
|
||||
fun startNativeProcesses() {
|
||||
val data = data
|
||||
data.buildShadowsocksConfig()
|
||||
val cmd = buildAdditionalArguments(arrayListOf(
|
||||
File((this as Context).applicationInfo.nativeLibraryDir, Executable.SS_LOCAL).absolutePath,
|
||||
"-u",
|
||||
"-b", "127.0.0.1",
|
||||
"-l", DataStore.portProxy.toString(),
|
||||
"-t", "600",
|
||||
"-c", "shadowsocks.json"))
|
||||
|
||||
val route = data.profile!!.route
|
||||
if (route != Acl.ALL) {
|
||||
cmd += "--acl"
|
||||
cmd += Acl.getFile(if (route == Acl.CUSTOM_RULES) Acl.CUSTOM_RULES_FLATTENED else route).absolutePath
|
||||
}
|
||||
|
||||
if (TcpFastOpen.sendEnabled) cmd += "--fast-open"
|
||||
|
||||
data.sslocalProcess = GuardedProcess(cmd).start()
|
||||
}
|
||||
|
||||
fun createNotification(): ServiceNotification
|
||||
|
||||
fun startRunner() {
|
||||
this as Context
|
||||
if (Build.VERSION.SDK_INT >= 26) startForegroundService(Intent(this, javaClass))
|
||||
else startService(Intent(this, javaClass))
|
||||
}
|
||||
|
||||
fun killProcesses() {
|
||||
val data = data
|
||||
data.sslocalProcess?.destroy()
|
||||
data.sslocalProcess = null
|
||||
}
|
||||
|
||||
fun stopRunner(stopService: Boolean, msg: String? = null) {
|
||||
// channge the state
|
||||
changeState(STOPPING)
|
||||
|
||||
app.track(tag, "stop")
|
||||
|
||||
killProcesses()
|
||||
|
||||
// clean up recevier
|
||||
val data = data
|
||||
this as Service
|
||||
if (data.closeReceiverRegistered) {
|
||||
unregisterReceiver(data.closeReceiver)
|
||||
data.closeReceiverRegistered = false
|
||||
}
|
||||
|
||||
data.notification?.destroy()
|
||||
data.notification = null
|
||||
|
||||
// Make sure update total traffic when stopping the runner
|
||||
data.updateTrafficTotal(TrafficMonitor.txTotal, TrafficMonitor.rxTotal)
|
||||
|
||||
TrafficMonitor.reset()
|
||||
data.trafficMonitorThread?.stopThread()
|
||||
data.trafficMonitorThread = null
|
||||
|
||||
// change the state
|
||||
changeState(STOPPED, msg)
|
||||
|
||||
// stop the service if nothing has bound to it
|
||||
if (stopService) stopSelf()
|
||||
|
||||
data.profile = null
|
||||
}
|
||||
|
||||
val data: Data get() = instances[this]!!
|
||||
|
||||
fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
||||
val data = data
|
||||
if (data.state != STOPPED) return Service.START_NOT_STICKY
|
||||
val profile = app.currentProfile
|
||||
if (profile == null) {
|
||||
stopRunner(true)
|
||||
return Service.START_NOT_STICKY
|
||||
}
|
||||
data.profile = profile
|
||||
profile.name = profile.formattedName
|
||||
|
||||
TrafficMonitor.reset()
|
||||
val thread = TrafficMonitorThread()
|
||||
thread.start()
|
||||
data.trafficMonitorThread = thread
|
||||
|
||||
if (!data.closeReceiverRegistered) {
|
||||
// register close receiver
|
||||
val filter = IntentFilter()
|
||||
filter.addAction(Action.RELOAD)
|
||||
filter.addAction(Intent.ACTION_SHUTDOWN)
|
||||
filter.addAction(Action.CLOSE)
|
||||
(this as Context).registerReceiver(data.closeReceiver, filter)
|
||||
data.closeReceiverRegistered = true
|
||||
}
|
||||
|
||||
data.notification = createNotification()
|
||||
app.track(tag, "start")
|
||||
|
||||
changeState(CONNECTING)
|
||||
|
||||
thread {
|
||||
this as Context
|
||||
try {
|
||||
if (profile.host == "198.199.101.152") {
|
||||
val client = OkHttpClient.Builder()
|
||||
.dns {
|
||||
listOf((Dns.resolve(it, false) ?: throw UnknownHostException())
|
||||
.parseNumericAddress())
|
||||
}
|
||||
.connectTimeout(10, TimeUnit.SECONDS)
|
||||
.writeTimeout(10, TimeUnit.SECONDS)
|
||||
.readTimeout(30, TimeUnit.SECONDS)
|
||||
.build()
|
||||
val mdg = MessageDigest.getInstance("SHA-1")
|
||||
mdg.update(app.info.signatures[0].toByteArray())
|
||||
val requestBody = FormBody.Builder()
|
||||
.add("sig", String(Base64.encode(mdg.digest(), 0)))
|
||||
.build()
|
||||
val request = Request.Builder()
|
||||
.url(app.remoteConfig.getString("proxy_url"))
|
||||
.post(requestBody)
|
||||
.build()
|
||||
|
||||
val proxies = client.newCall(request).execute()
|
||||
.body()!!.string().split('|').toMutableList()
|
||||
proxies.shuffle()
|
||||
val proxy = proxies.first().split(':')
|
||||
profile.host = proxy[0].trim()
|
||||
profile.remotePort = proxy[1].trim().toInt()
|
||||
profile.password = proxy[2].trim()
|
||||
profile.method = proxy[3].trim()
|
||||
}
|
||||
|
||||
if (profile.route == Acl.CUSTOM_RULES)
|
||||
Acl.save(Acl.CUSTOM_RULES_FLATTENED, Acl.customRules.flatten(10))
|
||||
|
||||
data.plugin = PluginConfiguration(profile.plugin ?: "").selectedOptions
|
||||
data.pluginPath = PluginManager.init(data.plugin)
|
||||
|
||||
// Clean up
|
||||
killProcesses()
|
||||
|
||||
if (!profile.host.isNumericAddress())
|
||||
profile.host = Dns.resolve(profile.host, true) ?: throw UnknownHostException()
|
||||
|
||||
startNativeProcesses()
|
||||
|
||||
if (profile.route !in arrayOf(Acl.ALL, Acl.CUSTOM_RULES)) AclSyncJob.schedule(profile.route)
|
||||
|
||||
changeState(CONNECTED)
|
||||
} catch (_: UnknownHostException) {
|
||||
stopRunner(true, getString(R.string.invalid_server))
|
||||
} catch (_: VpnService.NullConnectionException) {
|
||||
stopRunner(true, getString(R.string.reboot_required))
|
||||
} catch (exc: Throwable) {
|
||||
stopRunner(true, "${getString(R.string.service_failed)}: ${exc.message}")
|
||||
app.track(exc)
|
||||
}
|
||||
}
|
||||
return Service.START_NOT_STICKY
|
||||
}
|
||||
|
||||
fun changeState(s: Int, msg: String? = null) {
|
||||
val data = instances[this]!!
|
||||
if (data.state == s && msg == null) return
|
||||
if (data.callbacks.registeredCallbackCount > 0) app.handler.post {
|
||||
val n = data.callbacks.beginBroadcast()
|
||||
for (i in 0 until n) try {
|
||||
data.callbacks.getBroadcastItem(i).stateChanged(s, data.binder.profileName, msg)
|
||||
} catch (_: Exception) { } // ignore
|
||||
data.callbacks.finishBroadcast()
|
||||
}
|
||||
data.state = s
|
||||
}
|
||||
}
|
||||
|
||||
private val instances = WeakHashMap<Interface, Data>()
|
||||
internal fun register(instance: Interface) = instances.put(instance, Data(instance))
|
||||
|
||||
val usingVpnMode: Boolean get() = DataStore.serviceMode == Key.modeVpn
|
||||
val serviceClass: KClass<out Any> get() = when (DataStore.serviceMode) {
|
||||
Key.modeProxy -> ProxyService::class
|
||||
Key.modeVpn -> VpnService::class
|
||||
Key.modeTransproxy -> TransproxyService::class
|
||||
else -> throw UnknownError()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
/*******************************************************************************
|
||||
* *
|
||||
* Copyright (C) 2017 by Max Lv <max.c.lv@gmail.com> *
|
||||
* Copyright (C) 2017 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.bg
|
||||
|
||||
import android.content.Context
|
||||
import android.net.ConnectivityManager
|
||||
import android.net.LinkProperties
|
||||
import android.os.Build
|
||||
import android.util.Log
|
||||
import com.github.shadowsocks.App.Companion.app
|
||||
import com.github.shadowsocks.BuildConfig
|
||||
import okhttp3.Dns as Okdns
|
||||
import org.xbill.DNS.*
|
||||
import java.net.Inet6Address
|
||||
import java.net.InetAddress
|
||||
import java.net.NetworkInterface
|
||||
import java.net.UnknownHostException
|
||||
import java.util.*
|
||||
|
||||
object Dns {
|
||||
private const val TAG = "Dns"
|
||||
|
||||
@Throws(Exception::class)
|
||||
fun getDnsResolver(context: Context): String {
|
||||
val dnsResolvers = getDnsResolvers(context)
|
||||
if (dnsResolvers.isEmpty()) throw Exception("Couldn't find an active DNS resolver")
|
||||
val dnsResolver = dnsResolvers.iterator().next().toString()
|
||||
if (dnsResolver.startsWith("/")) return dnsResolver.substring(1)
|
||||
return dnsResolver
|
||||
}
|
||||
|
||||
@Throws(Exception::class)
|
||||
private fun getDnsResolvers(context: Context): Collection<InetAddress> {
|
||||
val addresses = ArrayList<InetAddress>()
|
||||
val connectivityManager = context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
|
||||
val classLinkProperties = Class.forName("android.net.LinkProperties")
|
||||
val getActiveLinkPropertiesMethod = ConnectivityManager::class.java.getMethod("getActiveLinkProperties")
|
||||
val linkProperties = getActiveLinkPropertiesMethod.invoke(connectivityManager)
|
||||
if (linkProperties != null) {
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
|
||||
val dnses = classLinkProperties.getMethod("getDnses").invoke(linkProperties) as Collection<*>
|
||||
dnses.mapTo(addresses) { it as InetAddress }
|
||||
} else addresses.addAll((linkProperties as LinkProperties).dnsServers)
|
||||
}
|
||||
return addresses
|
||||
}
|
||||
|
||||
private val hasIPv6Support get() = try {
|
||||
val result = NetworkInterface.getNetworkInterfaces().asSequence().flatMap { it.inetAddresses.asSequence() }
|
||||
.count { it is Inet6Address && !it.isLoopbackAddress && !it.isLinkLocalAddress } > 0
|
||||
if (result && BuildConfig.DEBUG) Log.d(TAG, "IPv6 address detected")
|
||||
result
|
||||
} catch (ex: Exception) {
|
||||
Log.e(TAG, "Failed to get interfaces' addresses.")
|
||||
app.track(ex)
|
||||
false
|
||||
}
|
||||
|
||||
private fun resolve(host: String, addrType: Int): String? {
|
||||
try {
|
||||
val lookup = Lookup(host, addrType)
|
||||
val resolver = SimpleResolver("208.67.220.220")
|
||||
resolver.setTCP(true)
|
||||
resolver.setPort(443)
|
||||
resolver.setTimeout(5)
|
||||
lookup.setResolver(resolver)
|
||||
val records = (lookup.run() ?: return null).toMutableList()
|
||||
records.shuffle()
|
||||
for (r in records) {
|
||||
when (addrType) {
|
||||
Type.A -> return (r as ARecord).address.hostAddress
|
||||
Type.AAAA -> return (r as AAAARecord).address.hostAddress
|
||||
}
|
||||
}
|
||||
} catch (_: Exception) { }
|
||||
return null
|
||||
}
|
||||
private fun resolve(host: String): String? = try {
|
||||
InetAddress.getByName(host).hostAddress
|
||||
} catch (_: UnknownHostException) {
|
||||
null
|
||||
}
|
||||
fun resolve(host: String, enableIPv6: Boolean): String? =
|
||||
(if (enableIPv6 && hasIPv6Support) resolve(host, Type.AAAA) else null)
|
||||
?: resolve(host, Type.A)
|
||||
?: resolve(host)
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
/*******************************************************************************
|
||||
* *
|
||||
* Copyright (C) 2017 by Max Lv <max.c.lv@gmail.com> *
|
||||
* Copyright (C) 2017 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.bg
|
||||
|
||||
import android.text.TextUtils
|
||||
import android.util.Log
|
||||
import com.github.shadowsocks.App.Companion.app
|
||||
import com.github.shadowsocks.JniHelper
|
||||
import java.io.File
|
||||
|
||||
object Executable {
|
||||
const val REDSOCKS = "libredsocks.so"
|
||||
const val PDNSD = "libpdnsd.so"
|
||||
const val SS_LOCAL = "libss-local.so"
|
||||
const val SS_TUNNEL = "libss-tunnel.so"
|
||||
const val TUN2SOCKS = "libtun2socks.so"
|
||||
const val OVERTURE = "liboverture.so"
|
||||
|
||||
val EXECUTABLES = setOf(SS_LOCAL, SS_TUNNEL, PDNSD, REDSOCKS, TUN2SOCKS, OVERTURE)
|
||||
|
||||
fun killAll() {
|
||||
for (process in File("/proc").listFiles { _, name -> TextUtils.isDigitsOnly(name) }) {
|
||||
val exe = File(File(process, "cmdline").bufferedReader().use { it.readText() }
|
||||
.split(Character.MIN_VALUE, limit = 2).first())
|
||||
if (exe.parent == app.applicationInfo.nativeLibraryDir && EXECUTABLES.contains(exe.name)) {
|
||||
val errno = JniHelper.sigkill(process.name.toInt())
|
||||
if (errno != 0) Log.w("kill", "SIGKILL ${exe.absolutePath} (${process.name}) failed with $errno")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,113 @@
|
||||
/*******************************************************************************
|
||||
* *
|
||||
* Copyright (C) 2017 by Max Lv <max.c.lv@gmail.com> *
|
||||
* Copyright (C) 2017 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.bg
|
||||
|
||||
import android.os.Build
|
||||
import android.os.SystemClock
|
||||
import android.util.Log
|
||||
import com.github.shadowsocks.App.Companion.app
|
||||
import com.github.shadowsocks.BuildConfig
|
||||
import com.github.shadowsocks.JniHelper
|
||||
import com.github.shadowsocks.utils.Commandline
|
||||
import java.io.IOException
|
||||
import java.io.InputStream
|
||||
import java.util.concurrent.Semaphore
|
||||
|
||||
class GuardedProcess(private val cmd: List<String>) {
|
||||
companion object {
|
||||
const val TAG = "GuardedProcess"
|
||||
}
|
||||
|
||||
private lateinit var guardThread: Thread
|
||||
@Volatile
|
||||
private var isDestroyed = false
|
||||
@Volatile
|
||||
private lateinit var process: Process
|
||||
|
||||
private fun streamLogger(input: InputStream, logger: (String, String) -> Int) = Thread {
|
||||
try {
|
||||
input.bufferedReader().useLines { it.forEach { logger(TAG, it) } }
|
||||
} catch (_: IOException) { } // ignore
|
||||
}
|
||||
|
||||
fun start(onRestartCallback: (() -> Unit)? = null): GuardedProcess {
|
||||
val semaphore = Semaphore(1)
|
||||
semaphore.acquire()
|
||||
var ioException: IOException? = null
|
||||
guardThread = Thread({
|
||||
try {
|
||||
var callback: (() -> Unit)? = null
|
||||
while (!isDestroyed) {
|
||||
if (BuildConfig.DEBUG) Log.d(TAG, "start process: " + Commandline.toString(cmd))
|
||||
val startTime = SystemClock.elapsedRealtime()
|
||||
|
||||
process = ProcessBuilder(cmd)
|
||||
.redirectErrorStream(true)
|
||||
.directory(app.filesDir)
|
||||
.start()
|
||||
|
||||
streamLogger(process.inputStream, Log::i).start()
|
||||
streamLogger(process.errorStream, Log::e).start()
|
||||
|
||||
if (callback == null) callback = onRestartCallback else callback()
|
||||
|
||||
semaphore.release()
|
||||
process.waitFor()
|
||||
|
||||
synchronized(this) {
|
||||
if (SystemClock.elapsedRealtime() - startTime < 1000) {
|
||||
Log.w(TAG, "process exit too fast, stop guard: " + Commandline.toString(cmd))
|
||||
isDestroyed = true
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (_: InterruptedException) {
|
||||
if (BuildConfig.DEBUG) Log.d(TAG, "thread interrupt, destroy process: " + Commandline.toString(cmd))
|
||||
destroyProcess()
|
||||
} catch (e: IOException) {
|
||||
ioException = e
|
||||
} finally {
|
||||
semaphore.release()
|
||||
}
|
||||
}, "GuardThread-" + cmd.first())
|
||||
guardThread.start()
|
||||
semaphore.acquire()
|
||||
if (ioException != null) throw ioException!!
|
||||
return this
|
||||
}
|
||||
|
||||
fun destroy() {
|
||||
isDestroyed = true
|
||||
guardThread.interrupt()
|
||||
destroyProcess()
|
||||
try {
|
||||
guardThread.join()
|
||||
} catch (_: InterruptedException) { }
|
||||
}
|
||||
|
||||
private fun destroyProcess() {
|
||||
if (Build.VERSION.SDK_INT < 24) @Suppress("DEPRECATION") {
|
||||
JniHelper.sigtermCompat(process)
|
||||
JniHelper.waitForCompat(process, 500)
|
||||
}
|
||||
process.destroy()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,117 @@
|
||||
/*******************************************************************************
|
||||
* *
|
||||
* Copyright (C) 2017 by Max Lv <max.c.lv@gmail.com> *
|
||||
* Copyright (C) 2017 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.bg
|
||||
|
||||
import android.content.Context
|
||||
import com.github.shadowsocks.App.Companion.app
|
||||
import com.github.shadowsocks.acl.Acl
|
||||
import com.github.shadowsocks.preference.DataStore
|
||||
import com.github.shadowsocks.utils.parseNumericAddress
|
||||
import org.json.JSONArray
|
||||
import org.json.JSONObject
|
||||
import java.io.File
|
||||
import java.net.Inet6Address
|
||||
import java.util.*
|
||||
|
||||
/**
|
||||
* This object also uses WeakMap to simulate the effects of multi-inheritance, but more lightweight.
|
||||
*/
|
||||
object LocalDnsService {
|
||||
interface Interface : BaseService.Interface {
|
||||
var overtureProcess: GuardedProcess?
|
||||
get() = overtureProcesses[this]
|
||||
set(value) {
|
||||
if (value == null) overtureProcesses.remove(this) else overtureProcesses[this] = value
|
||||
}
|
||||
|
||||
override fun startNativeProcesses() {
|
||||
super.startNativeProcesses()
|
||||
val profile = data.profile!!
|
||||
|
||||
fun buildOvertureConfig(file: String): String {
|
||||
val config = JSONObject()
|
||||
.put("BindAddress", "127.0.0.1:" + DataStore.portLocalDns)
|
||||
.put("RedirectIPv6Record", true)
|
||||
.put("DomainBase64Decode", true)
|
||||
.put("HostsFile", "hosts")
|
||||
.put("MinimumTTL", 3600)
|
||||
.put("CacheSize", 4096)
|
||||
fun makeDns(name: String, address: String, edns: Boolean = true) = {
|
||||
val dns = JSONObject()
|
||||
.put("Name", name)
|
||||
.put("Address", (when (address.parseNumericAddress()) {
|
||||
is Inet6Address -> "[$address]"
|
||||
else -> address
|
||||
}) + ":53")
|
||||
.put("Timeout", 6)
|
||||
.put("EDNSClientSubnet", JSONObject().put("Policy", "disable"))
|
||||
if (edns) dns
|
||||
.put("Protocol", "tcp")
|
||||
.put("Socks5Address", "127.0.0.1:" + DataStore.portProxy)
|
||||
else dns.put("Protocol", "udp")
|
||||
dns
|
||||
}
|
||||
val remoteDns = JSONArray(profile.remoteDns.split(",")
|
||||
.mapIndexed { i, dns -> makeDns("UserDef-" + i, dns.trim())() })
|
||||
val localDns = JSONArray(arrayOf(
|
||||
makeDns("Primary-1", "119.29.29.29", false),
|
||||
makeDns("Primary-2", "114.114.114.114", false)
|
||||
))
|
||||
|
||||
this as Context
|
||||
try {
|
||||
val localLinkDns = Dns.getDnsResolver(this)
|
||||
localDns.put(makeDns("Primary-3", localLinkDns, false))
|
||||
} catch (_: Exception) { } // ignore
|
||||
|
||||
when (profile.route) {
|
||||
Acl.BYPASS_CHN, Acl.BYPASS_LAN_CHN, Acl.GFWLIST, Acl.CUSTOM_RULES -> config
|
||||
.put("PrimaryDNS", localDns)
|
||||
.put("AlternativeDNS", remoteDns)
|
||||
.put("IPNetworkFile", "china_ip_list.txt")
|
||||
.put("DomainFile", "gfwlist.txt")
|
||||
Acl.CHINALIST -> config
|
||||
.put("PrimaryDNS", localDns)
|
||||
.put("AlternativeDNS", remoteDns)
|
||||
else -> config
|
||||
.put("PrimaryDNS", remoteDns)
|
||||
// no need to setup AlternativeDNS in Acl.ALL/BYPASS_LAN mode
|
||||
.put("OnlyPrimaryDNS", true)
|
||||
}
|
||||
File(filesDir, file).bufferedWriter().use { it.write(config.toString()) }
|
||||
return file
|
||||
}
|
||||
|
||||
if (!profile.udpdns) overtureProcess = GuardedProcess(buildAdditionalArguments(arrayListOf(
|
||||
File(app.applicationInfo.nativeLibraryDir, Executable.OVERTURE).absolutePath,
|
||||
"-c", buildOvertureConfig("overture.conf")
|
||||
))).start()
|
||||
}
|
||||
|
||||
override fun killProcesses() {
|
||||
super.killProcesses()
|
||||
overtureProcess?.destroy()
|
||||
overtureProcess = null
|
||||
}
|
||||
}
|
||||
|
||||
private val overtureProcesses = WeakHashMap<Interface, GuardedProcess>()
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
/*******************************************************************************
|
||||
* *
|
||||
* Copyright (C) 2017 by Max Lv <max.c.lv@gmail.com> *
|
||||
* Copyright (C) 2017 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.bg
|
||||
|
||||
import android.net.LocalServerSocket
|
||||
import android.net.LocalSocket
|
||||
import android.net.LocalSocketAddress
|
||||
import android.util.Log
|
||||
import com.github.shadowsocks.App.Companion.app
|
||||
import java.io.File
|
||||
import java.io.IOException
|
||||
import java.util.concurrent.atomic.AtomicReference
|
||||
|
||||
abstract class LocalSocketListener(private val tag: String) : Thread() {
|
||||
init {
|
||||
setUncaughtExceptionHandler(app::track)
|
||||
}
|
||||
|
||||
protected abstract val socketFile: File
|
||||
private val serverSocket = AtomicReference<LocalServerSocket?>()
|
||||
|
||||
/**
|
||||
* Inherited class do not need to close input/output streams as they will be closed automatically.
|
||||
*/
|
||||
protected abstract fun accept(socket: LocalSocket)
|
||||
override fun run() {
|
||||
socketFile.delete()
|
||||
try {
|
||||
val localSocket = LocalSocket()
|
||||
localSocket.bind(LocalSocketAddress(socketFile.absolutePath, LocalSocketAddress.Namespace.FILESYSTEM))
|
||||
serverSocket.set(LocalServerSocket(localSocket.fileDescriptor))
|
||||
} catch (e: IOException) {
|
||||
Log.e(tag, "unable to bind", e)
|
||||
return
|
||||
}
|
||||
while (true) {
|
||||
val serverSocket = serverSocket.get() ?: return
|
||||
try {
|
||||
try {
|
||||
serverSocket.accept()
|
||||
} catch (e: IOException) {
|
||||
Log.e(tag, "Error when accept socket", e)
|
||||
app.track(e)
|
||||
null
|
||||
}?.use(this::accept)
|
||||
} catch (e: Exception) {
|
||||
Log.e(tag, "Error when recv traffic stat", e)
|
||||
app.track(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun stopThread() {
|
||||
val old = serverSocket.getAndSet(null) ?: return
|
||||
try {
|
||||
old.close()
|
||||
} catch (_: Exception) { } // ignore
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
/*******************************************************************************
|
||||
* *
|
||||
* Copyright (C) 2017 by Max Lv <max.c.lv@gmail.com> *
|
||||
* Copyright (C) 2017 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.bg
|
||||
|
||||
import android.app.Service
|
||||
import android.content.Intent
|
||||
import android.os.IBinder
|
||||
|
||||
/**
|
||||
* Shadowsocks service at its minimum.
|
||||
*/
|
||||
class ProxyService : Service(), BaseService.Interface {
|
||||
init {
|
||||
BaseService.register(this)
|
||||
}
|
||||
|
||||
override val tag: String get() = "ShadowsocksProxyService"
|
||||
override fun createNotification(): ServiceNotification =
|
||||
ServiceNotification(this, data.profile!!.name, "service-proxy", true)
|
||||
|
||||
override fun onBind(intent: Intent): IBinder? = super.onBind(intent)
|
||||
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int =
|
||||
super<BaseService.Interface>.onStartCommand(intent, flags, startId)
|
||||
}
|
||||
@@ -0,0 +1,130 @@
|
||||
/*******************************************************************************
|
||||
* *
|
||||
* Copyright (C) 2017 by Max Lv <max.c.lv@gmail.com> *
|
||||
* Copyright (C) 2017 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.bg
|
||||
|
||||
import android.app.KeyguardManager
|
||||
import android.app.NotificationManager
|
||||
import android.app.PendingIntent
|
||||
import android.app.Service
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.IntentFilter
|
||||
import android.os.Build
|
||||
import android.os.PowerManager
|
||||
import android.support.v4.app.NotificationCompat
|
||||
import android.support.v4.content.ContextCompat
|
||||
import com.github.shadowsocks.MainActivity
|
||||
import com.github.shadowsocks.R
|
||||
import com.github.shadowsocks.aidl.IShadowsocksServiceCallback
|
||||
import com.github.shadowsocks.utils.Action
|
||||
import com.github.shadowsocks.utils.broadcastReceiver
|
||||
import java.util.*
|
||||
|
||||
class ServiceNotification(private val service: BaseService.Interface, profileName: String,
|
||||
channel: String, private val visible: Boolean = false) {
|
||||
private val keyGuard = (service as Context).getSystemService(Context.KEYGUARD_SERVICE) as KeyguardManager
|
||||
private val nm by lazy {
|
||||
(service as Context).getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
||||
}
|
||||
private val callback by lazy {
|
||||
object : IShadowsocksServiceCallback.Stub() {
|
||||
override fun stateChanged(state: Int, profileName: String?, msg: String?) { } // ignore
|
||||
override fun trafficUpdated(profileId: Int, txRate: Long, rxRate: Long, txTotal: Long, rxTotal: Long) {
|
||||
service as Context
|
||||
val txr = service.getString(R.string.speed, TrafficMonitor.formatTraffic(txRate))
|
||||
val rxr = service.getString(R.string.speed, TrafficMonitor.formatTraffic(rxRate))
|
||||
builder.setContentText("$txr↑\t$rxr↓")
|
||||
style.bigText(service.getString(R.string.stat_summary).format(Locale.ENGLISH, txr, rxr,
|
||||
TrafficMonitor.formatTraffic(txTotal), TrafficMonitor.formatTraffic(rxTotal)))
|
||||
show()
|
||||
}
|
||||
override fun trafficPersisted(profileId: Int) { }
|
||||
}
|
||||
}
|
||||
private val lockReceiver = broadcastReceiver { _, intent -> update(intent.action) }
|
||||
private var callbackRegistered = false
|
||||
|
||||
private val builder = NotificationCompat.Builder(service as Context, channel)
|
||||
.setWhen(0)
|
||||
.setColor(ContextCompat.getColor(service, R.color.material_primary_500))
|
||||
.setTicker(service.getString(R.string.forward_success))
|
||||
.setContentTitle(profileName)
|
||||
.setContentIntent(PendingIntent.getActivity(service, 0, Intent(service, MainActivity::class.java)
|
||||
.setFlags(Intent.FLAG_ACTIVITY_REORDER_TO_FRONT), 0))
|
||||
.setSmallIcon(R.drawable.ic_stat_shadowsocks)
|
||||
private val style = NotificationCompat.BigTextStyle(builder)
|
||||
private var isVisible = true
|
||||
|
||||
init {
|
||||
service as Context
|
||||
if (Build.VERSION.SDK_INT < 24) builder.addAction(R.drawable.ic_navigation_close,
|
||||
service.getString(R.string.stop), PendingIntent.getBroadcast(service, 0, Intent(Action.CLOSE), 0))
|
||||
val power = service.getSystemService(Context.POWER_SERVICE) as PowerManager
|
||||
update(if (if (Build.VERSION.SDK_INT >= 20) power.isInteractive else @Suppress("DEPRECATION") power.isScreenOn)
|
||||
Intent.ACTION_SCREEN_ON else Intent.ACTION_SCREEN_OFF, true)
|
||||
val screenFilter = IntentFilter()
|
||||
screenFilter.addAction(Intent.ACTION_SCREEN_ON)
|
||||
screenFilter.addAction(Intent.ACTION_SCREEN_OFF)
|
||||
if (visible && Build.VERSION.SDK_INT in 21 until 26) screenFilter.addAction(Intent.ACTION_USER_PRESENT)
|
||||
service.registerReceiver(lockReceiver, screenFilter)
|
||||
}
|
||||
|
||||
private fun update(action: String, forceShow: Boolean = false) {
|
||||
if (forceShow || service.data.state == BaseService.CONNECTED) when (action) {
|
||||
Intent.ACTION_SCREEN_OFF -> {
|
||||
setVisible(visible && Build.VERSION.SDK_INT < 21, forceShow)
|
||||
unregisterCallback() // unregister callback to save battery
|
||||
}
|
||||
Intent.ACTION_SCREEN_ON -> {
|
||||
setVisible(visible && (Build.VERSION.SDK_INT < 21 || !keyGuard.inKeyguardRestrictedInputMode()),
|
||||
forceShow)
|
||||
service.data.binder.registerCallback(callback)
|
||||
service.data.binder.startListeningForBandwidth(callback)
|
||||
callbackRegistered = true
|
||||
}
|
||||
Intent.ACTION_USER_PRESENT -> setVisible(true, forceShow)
|
||||
}
|
||||
}
|
||||
|
||||
private fun unregisterCallback() {
|
||||
if (callbackRegistered) {
|
||||
service.data.binder.unregisterCallback(callback)
|
||||
callbackRegistered = false
|
||||
}
|
||||
}
|
||||
|
||||
private fun setVisible(visible: Boolean, forceShow: Boolean = false) {
|
||||
if (isVisible != visible) {
|
||||
isVisible = visible
|
||||
builder.priority = if (visible) NotificationCompat.PRIORITY_LOW else NotificationCompat.PRIORITY_MIN
|
||||
show()
|
||||
} else if (forceShow) show()
|
||||
}
|
||||
|
||||
private fun show() = (service as Service).startForeground(1, builder.build())
|
||||
|
||||
fun destroy() {
|
||||
(service as Service).unregisterReceiver(lockReceiver)
|
||||
unregisterCallback()
|
||||
service.stopForeground(true)
|
||||
nm.cancel(1)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
/*******************************************************************************
|
||||
* *
|
||||
* Copyright (C) 2017 by Max Lv <max.c.lv@gmail.com> *
|
||||
* Copyright (C) 2017 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.bg
|
||||
|
||||
import android.graphics.drawable.Icon
|
||||
import android.os.Build
|
||||
import android.service.quicksettings.Tile
|
||||
import android.support.annotation.RequiresApi
|
||||
import com.github.shadowsocks.App.Companion.app
|
||||
import com.github.shadowsocks.R
|
||||
import com.github.shadowsocks.ShadowsocksConnection
|
||||
import com.github.shadowsocks.aidl.IShadowsocksService
|
||||
import com.github.shadowsocks.aidl.IShadowsocksServiceCallback
|
||||
import android.service.quicksettings.TileService as BaseTileService
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.N)
|
||||
class TileService : BaseTileService(), ShadowsocksConnection.Interface {
|
||||
private val iconIdle by lazy { Icon.createWithResource(this, R.drawable.ic_start_idle).setTint(0x79ffffff) }
|
||||
private val iconBusy by lazy { Icon.createWithResource(this, R.drawable.ic_start_busy) }
|
||||
private val iconConnected by lazy { Icon.createWithResource(this, R.drawable.ic_start_connected) }
|
||||
|
||||
override val serviceCallback: IShadowsocksServiceCallback.Stub by lazy {
|
||||
@RequiresApi(Build.VERSION_CODES.N)
|
||||
object : IShadowsocksServiceCallback.Stub() {
|
||||
override fun stateChanged(state: Int, profileName: String?, msg: String?) {
|
||||
val tile = qsTile ?: return
|
||||
when (state) {
|
||||
BaseService.STOPPED -> {
|
||||
tile.icon = iconIdle
|
||||
tile.label = getString(R.string.app_name)
|
||||
tile.state = Tile.STATE_INACTIVE
|
||||
}
|
||||
BaseService.CONNECTED -> {
|
||||
tile.icon = iconConnected
|
||||
tile.label = profileName ?: getString(R.string.app_name)
|
||||
tile.state = Tile.STATE_ACTIVE
|
||||
}
|
||||
else -> {
|
||||
tile.icon = iconBusy
|
||||
tile.label = getString(R.string.app_name)
|
||||
tile.state = Tile.STATE_UNAVAILABLE
|
||||
}
|
||||
}
|
||||
tile.updateTile()
|
||||
}
|
||||
override fun trafficUpdated(profileId: Int, txRate: Long, rxRate: Long, txTotal: Long, rxTotal: Long) { }
|
||||
override fun trafficPersisted(profileId: Int) { }
|
||||
}
|
||||
}
|
||||
|
||||
override fun onServiceConnected(service: IShadowsocksService) =
|
||||
serviceCallback.stateChanged(service.state, service.profileName, null)
|
||||
|
||||
override fun onStartListening() {
|
||||
super.onStartListening()
|
||||
connection.connect()
|
||||
}
|
||||
override fun onStopListening() {
|
||||
super.onStopListening()
|
||||
connection.disconnect()
|
||||
}
|
||||
|
||||
override fun onClick() {
|
||||
if (isLocked) unlockAndRun(this::toggle) else toggle()
|
||||
}
|
||||
|
||||
private fun toggle() {
|
||||
val service = connection.service ?: return
|
||||
when (service.state) {
|
||||
BaseService.STOPPED -> app.startService()
|
||||
BaseService.CONNECTED -> app.stopService()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
/*******************************************************************************
|
||||
* *
|
||||
* Copyright (C) 2017 by Max Lv <max.c.lv@gmail.com> *
|
||||
* Copyright (C) 2017 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.bg
|
||||
|
||||
import android.os.SystemClock
|
||||
import com.github.shadowsocks.App.Companion.app
|
||||
import com.github.shadowsocks.R
|
||||
import java.text.DecimalFormat
|
||||
|
||||
object TrafficMonitor {
|
||||
// Bytes per second
|
||||
var txRate = 0L
|
||||
var rxRate = 0L
|
||||
|
||||
// Bytes for the current session
|
||||
var txTotal = 0L
|
||||
var rxTotal = 0L
|
||||
|
||||
// Bytes for the last query
|
||||
var txLast = 0L
|
||||
var rxLast = 0L
|
||||
var timestampLast = 0L
|
||||
@Volatile
|
||||
var dirty = true
|
||||
|
||||
private val units = arrayOf("KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB", "BB", "NB", "DB", "CB")
|
||||
private val numberFormat = DecimalFormat("@@@")
|
||||
fun formatTraffic(size: Long): String {
|
||||
var n: Double = size.toDouble()
|
||||
var i = -1
|
||||
while (n >= 999.5) {
|
||||
n /= 1024
|
||||
++i
|
||||
}
|
||||
return if (i < 0) "$size ${app.resources.getQuantityString(R.plurals.bytes, size.toInt())}"
|
||||
else "${numberFormat.format(n)} ${units[i]}"
|
||||
}
|
||||
|
||||
fun updateRate(): Boolean {
|
||||
val now = SystemClock.elapsedRealtime()
|
||||
val delta = now - timestampLast
|
||||
timestampLast = now
|
||||
var updated = false
|
||||
if (delta != 0L)
|
||||
if (dirty) {
|
||||
txRate = (txTotal - txLast) * 1000 / delta
|
||||
rxRate = (rxTotal - rxLast) * 1000 / delta
|
||||
txLast = txTotal
|
||||
rxLast = rxTotal
|
||||
dirty = false
|
||||
updated = true
|
||||
} else {
|
||||
if (txRate != 0L) {
|
||||
txRate = 0
|
||||
updated = true
|
||||
}
|
||||
if (rxRate != 0L) {
|
||||
rxRate = 0
|
||||
updated = true
|
||||
}
|
||||
}
|
||||
return updated
|
||||
}
|
||||
|
||||
fun update(tx: Long, rx: Long) {
|
||||
if (txTotal != tx) {
|
||||
txTotal = tx
|
||||
dirty = true
|
||||
}
|
||||
if (rxTotal != rx) {
|
||||
rxTotal = rx
|
||||
dirty = true
|
||||
}
|
||||
}
|
||||
|
||||
fun reset() {
|
||||
txRate = 0
|
||||
rxRate = 0
|
||||
txTotal = 0
|
||||
rxTotal = 0
|
||||
txLast = 0
|
||||
rxLast = 0
|
||||
dirty = true
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
/*******************************************************************************
|
||||
* *
|
||||
* Copyright (C) 2017 by Max Lv <max.c.lv@gmail.com> *
|
||||
* Copyright (C) 2017 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.bg
|
||||
|
||||
import android.net.LocalSocket
|
||||
import com.github.shadowsocks.App.Companion.app
|
||||
import java.io.File
|
||||
import java.io.IOException
|
||||
import java.nio.ByteBuffer
|
||||
import java.nio.ByteOrder
|
||||
|
||||
class TrafficMonitorThread : LocalSocketListener("TrafficMonitorThread") {
|
||||
override val socketFile = File(app.filesDir, "/stat_path")
|
||||
|
||||
override fun accept(socket: LocalSocket) {
|
||||
val buffer = ByteArray(16)
|
||||
if (socket.inputStream.read(buffer) != 16) throw IOException("Unexpected traffic stat length")
|
||||
val stat = ByteBuffer.wrap(buffer).order(ByteOrder.LITTLE_ENDIAN)
|
||||
TrafficMonitor.update(stat.getLong(0), stat.getLong(8))
|
||||
socket.outputStream.write(0)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
/*******************************************************************************
|
||||
* *
|
||||
* Copyright (C) 2017 by Max Lv <max.c.lv@gmail.com> *
|
||||
* Copyright (C) 2017 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.bg
|
||||
|
||||
import android.app.Service
|
||||
import android.content.Intent
|
||||
import android.os.IBinder
|
||||
import com.github.shadowsocks.preference.DataStore
|
||||
import java.io.File
|
||||
|
||||
class TransproxyService : Service(), LocalDnsService.Interface {
|
||||
init {
|
||||
BaseService.register(this)
|
||||
}
|
||||
|
||||
override val tag: String get() = "ShadowsocksTransproxyService"
|
||||
override fun createNotification(): ServiceNotification =
|
||||
ServiceNotification(this, data.profile!!.name, "service-transproxy", true)
|
||||
|
||||
override fun onBind(intent: Intent): IBinder? = super.onBind(intent)
|
||||
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int =
|
||||
super<LocalDnsService.Interface>.onStartCommand(intent, flags, startId)
|
||||
|
||||
private var sstunnelProcess: GuardedProcess? = null
|
||||
private var redsocksProcess: GuardedProcess? = null
|
||||
|
||||
private fun startDNSTunnel() {
|
||||
val cmd = arrayListOf(File(applicationInfo.nativeLibraryDir, Executable.SS_TUNNEL).absolutePath,
|
||||
"-t", "10",
|
||||
"-b", "127.0.0.1",
|
||||
"-u",
|
||||
"-l", DataStore.portLocalDns.toString(), // ss-tunnel listens on the same port as overture
|
||||
"-L", data.profile!!.remoteDns.split(",").first().trim() + ":53",
|
||||
"-c", "shadowsocks.json") // config is already built by BaseService.Interface
|
||||
sstunnelProcess = GuardedProcess(cmd).start()
|
||||
}
|
||||
|
||||
private fun startRedsocksDaemon() {
|
||||
val config = """base {
|
||||
log_debug = off;
|
||||
log_info = off;
|
||||
log = stderr;
|
||||
daemon = off;
|
||||
redirector = iptables;
|
||||
}
|
||||
redsocks {
|
||||
local_ip = 127.0.0.1;
|
||||
local_port = ${DataStore.portTransproxy};
|
||||
ip = 127.0.0.1;
|
||||
port = ${DataStore.portProxy};
|
||||
type = socks5;
|
||||
}
|
||||
"""
|
||||
File(filesDir, "redsocks.conf").bufferedWriter().use { it.write(config) }
|
||||
redsocksProcess = GuardedProcess(arrayListOf(
|
||||
File(applicationInfo.nativeLibraryDir, Executable.REDSOCKS).absolutePath,
|
||||
"-c", "redsocks.conf")
|
||||
).start()
|
||||
}
|
||||
|
||||
override fun startNativeProcesses() {
|
||||
startRedsocksDaemon()
|
||||
super.startNativeProcesses()
|
||||
if (data.profile!!.udpdns) startDNSTunnel()
|
||||
}
|
||||
|
||||
override fun killProcesses() {
|
||||
super.killProcesses()
|
||||
sstunnelProcess?.destroy()
|
||||
sstunnelProcess = null
|
||||
redsocksProcess?.destroy()
|
||||
redsocksProcess = null
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,194 @@
|
||||
/*******************************************************************************
|
||||
* *
|
||||
* Copyright (C) 2017 by Max Lv <max.c.lv@gmail.com> *
|
||||
* Copyright (C) 2017 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.bg
|
||||
|
||||
import android.app.Service
|
||||
import android.content.Intent
|
||||
import android.content.pm.PackageManager
|
||||
import android.net.LocalSocket
|
||||
import android.os.Build
|
||||
import android.os.IBinder
|
||||
import android.os.ParcelFileDescriptor
|
||||
import android.util.Log
|
||||
import com.github.shadowsocks.App.Companion.app
|
||||
import com.github.shadowsocks.JniHelper
|
||||
import com.github.shadowsocks.R
|
||||
import com.github.shadowsocks.VpnRequestActivity
|
||||
import com.github.shadowsocks.acl.Acl
|
||||
import com.github.shadowsocks.preference.DataStore
|
||||
import com.github.shadowsocks.utils.Subnet
|
||||
import com.github.shadowsocks.utils.parseNumericAddress
|
||||
import java.io.File
|
||||
import java.io.FileDescriptor
|
||||
import java.lang.reflect.Method
|
||||
import java.util.*
|
||||
import android.net.VpnService as BaseVpnService
|
||||
|
||||
class VpnService : BaseVpnService(), LocalDnsService.Interface {
|
||||
companion object {
|
||||
private const val VPN_MTU = 1500
|
||||
private const val PRIVATE_VLAN = "26.26.26.%s"
|
||||
private const val PRIVATE_VLAN6 = "fdfe:dcba:9876::%s"
|
||||
|
||||
private val getInt: Method = FileDescriptor::class.java.getDeclaredMethod("getInt$")
|
||||
}
|
||||
|
||||
private inner class ProtectWorker : LocalSocketListener("ShadowsocksVpnThread") {
|
||||
override val socketFile: File = File(filesDir, "protect_path")
|
||||
|
||||
override fun accept(socket: LocalSocket) {
|
||||
socket.inputStream.read()
|
||||
val fds = socket.ancillaryFileDescriptors
|
||||
if (fds.isEmpty()) return
|
||||
val fd = getInt.invoke(fds.first()) as Int
|
||||
val ret = protect(fd)
|
||||
JniHelper.close(fd) // Trick to close file decriptor
|
||||
socket.outputStream.write(if (ret) 0 else 1)
|
||||
}
|
||||
}
|
||||
class NullConnectionException : NullPointerException()
|
||||
|
||||
init {
|
||||
BaseService.register(this)
|
||||
}
|
||||
|
||||
override val tag: String get() = "ShadowsocksVpnService"
|
||||
override fun createNotification(): ServiceNotification =
|
||||
ServiceNotification(this, data.profile!!.name, "service-vpn")
|
||||
|
||||
private var conn: ParcelFileDescriptor? = null
|
||||
private var worker: ProtectWorker? = null
|
||||
private var tun2socksProcess: GuardedProcess? = null
|
||||
|
||||
override fun onBind(intent: Intent): IBinder? = when (intent.action) {
|
||||
SERVICE_INTERFACE -> super<BaseVpnService>.onBind(intent)
|
||||
else -> super<LocalDnsService.Interface>.onBind(intent)
|
||||
}
|
||||
|
||||
override fun onRevoke() = stopRunner(true)
|
||||
|
||||
override fun killProcesses() {
|
||||
worker?.stopThread()
|
||||
worker = null
|
||||
super.killProcesses()
|
||||
tun2socksProcess?.destroy()
|
||||
tun2socksProcess = null
|
||||
conn?.close()
|
||||
conn = null
|
||||
}
|
||||
|
||||
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
||||
if (BaseService.usingVpnMode)
|
||||
if (BaseVpnService.prepare(this) != null)
|
||||
startActivity(Intent(this, VpnRequestActivity::class.java)
|
||||
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK))
|
||||
else return super<LocalDnsService.Interface>.onStartCommand(intent, flags, startId)
|
||||
stopRunner(true)
|
||||
return Service.START_NOT_STICKY
|
||||
}
|
||||
|
||||
override fun startNativeProcesses() {
|
||||
val worker = ProtectWorker()
|
||||
worker.start()
|
||||
this.worker = worker
|
||||
|
||||
super.startNativeProcesses()
|
||||
|
||||
val fd = startVpn()
|
||||
if (!sendFd(fd)) throw Exception("sendFd failed")
|
||||
}
|
||||
|
||||
override fun buildAdditionalArguments(cmd: ArrayList<String>): ArrayList<String> {
|
||||
cmd += "-V"
|
||||
return cmd
|
||||
}
|
||||
|
||||
private fun startVpn(): Int {
|
||||
val profile = data.profile!!
|
||||
val builder = Builder()
|
||||
.setSession(profile.formattedName)
|
||||
.setMtu(VPN_MTU)
|
||||
.addAddress(PRIVATE_VLAN.format(Locale.ENGLISH, "1"), 24)
|
||||
|
||||
profile.remoteDns.split(",").forEach { builder.addDnsServer(it.trim()) }
|
||||
|
||||
if (profile.ipv6) {
|
||||
builder.addAddress(PRIVATE_VLAN6.format(Locale.ENGLISH, "1"), 126)
|
||||
builder.addRoute("::", 0)
|
||||
}
|
||||
|
||||
if (Build.VERSION.SDK_INT >= 21 && profile.proxyApps) {
|
||||
for (pkg in profile.individual.split('\n')) try {
|
||||
if (profile.bypass) builder.addDisallowedApplication(pkg) else builder.addAllowedApplication(pkg)
|
||||
} catch (ex: PackageManager.NameNotFoundException) {
|
||||
Log.e(tag, "Invalid package name", ex)
|
||||
}
|
||||
}
|
||||
|
||||
when (profile.route) {
|
||||
Acl.ALL, Acl.BYPASS_CHN, Acl.CUSTOM_RULES -> builder.addRoute("0.0.0.0", 0)
|
||||
else -> {
|
||||
resources.getStringArray(R.array.bypass_private_route).forEach {
|
||||
val subnet = Subnet.fromString(it)
|
||||
builder.addRoute(subnet.address.hostAddress, subnet.prefixSize)
|
||||
}
|
||||
profile.remoteDns.split(",").map { it.trim().parseNumericAddress() }
|
||||
.forEach { builder.addRoute(it, it.address.size shl 3) }
|
||||
}
|
||||
}
|
||||
|
||||
val conn = builder.establish() ?: throw NullConnectionException()
|
||||
this.conn = conn
|
||||
val fd = conn.fd
|
||||
|
||||
val cmd = arrayListOf(File(applicationInfo.nativeLibraryDir, Executable.TUN2SOCKS).absolutePath,
|
||||
"--netif-ipaddr", PRIVATE_VLAN.format(Locale.ENGLISH, "2"),
|
||||
"--netif-netmask", "255.255.255.0",
|
||||
"--socks-server-addr", "127.0.0.1:${DataStore.portProxy}",
|
||||
"--tunfd", fd.toString(),
|
||||
"--tunmtu", VPN_MTU.toString(),
|
||||
"--sock-path", "sock_path",
|
||||
"--loglevel", "3")
|
||||
if (profile.ipv6) {
|
||||
cmd += "--netif-ip6addr"
|
||||
cmd += PRIVATE_VLAN6.format(Locale.ENGLISH, "2")
|
||||
}
|
||||
cmd += "--enable-udprelay"
|
||||
if (!profile.udpdns) {
|
||||
cmd += "--dnsgw"
|
||||
cmd += "127.0.0.1:${DataStore.portLocalDns}"
|
||||
}
|
||||
tun2socksProcess = GuardedProcess(cmd).start { sendFd(fd) }
|
||||
return fd
|
||||
}
|
||||
|
||||
private fun sendFd(fd: Int): Boolean {
|
||||
if (fd != -1) {
|
||||
var tries = 1
|
||||
while (tries < 5) {
|
||||
Thread.sleep(1000L * tries)
|
||||
if (JniHelper.sendFd(fd, File(filesDir, "sock_path").absolutePath) != -1) return true
|
||||
tries += 1
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user