Initial draft for porting this project to Gradle + Kotlin

This is one of my stupidest idea ever.
This commit is contained in:
Mygod
2017-12-03 10:20:57 -08:00
parent 2f682ef3ec
commit 3e8b254118
206 changed files with 7885 additions and 7475 deletions
@@ -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
}
}