440 lines
13 KiB
Scala
440 lines
13 KiB
Scala
/* Shadowsocks - A shadowsocks client for Android
|
|
* Copyright (C) 2012 <max.c.lv@gmail.com>
|
|
*
|
|
* 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/>.
|
|
*
|
|
*
|
|
* ___====-_ _-====___
|
|
* _--^^^#####// \\#####^^^--_
|
|
* _-^##########// ( ) \\##########^-_
|
|
* -############// |\^^/| \\############-
|
|
* _/############// (@::@) \\############\_
|
|
* /#############(( \\// ))#############\
|
|
* -###############\\ (oo) //###############-
|
|
* -#################\\ / VV \ //#################-
|
|
* -###################\\/ \//###################-
|
|
* _#/|##########/\######( /\ )######/\##########|\#_
|
|
* |/ |#/\#/\#/\/ \#/\##\ | | /##/\#/ \/\#/\#/\#| \|
|
|
* ` |/ V V ` V \#\| | | |/#/ V ' V V \| '
|
|
* ` ` ` ` / | | | | \ ' ' ' '
|
|
* ( | | | | )
|
|
* __\ | | | | /__
|
|
* (vvv(VVV)(VVV)vvv)
|
|
*
|
|
* HERE BE DRAGONS
|
|
*
|
|
*/
|
|
package com.github.shadowsocks
|
|
|
|
import android.app._
|
|
import android.content._
|
|
import android.content.pm.PackageInfo
|
|
import android.content.pm.PackageManager
|
|
import android.os._
|
|
import android.preference.PreferenceManager
|
|
import android.support.v4.app.NotificationCompat
|
|
import android.util.Log
|
|
import com.google.analytics.tracking.android.EasyTracker
|
|
import java.io._
|
|
import java.lang.ref.WeakReference
|
|
import android.net.VpnService
|
|
import org.apache.http.conn.util.InetAddressUtils
|
|
import android.os.Message
|
|
import scala.Some
|
|
|
|
object ShadowVpnService {
|
|
def isServiceStarted: Boolean = {
|
|
if (sRunningInstance == null) {
|
|
false
|
|
} else if (sRunningInstance.get == null) {
|
|
sRunningInstance = null
|
|
false
|
|
} else {
|
|
true
|
|
}
|
|
}
|
|
|
|
var sRunningInstance: WeakReference[ShadowVpnService] = null
|
|
}
|
|
|
|
class ShadowVpnService extends VpnService {
|
|
|
|
val TAG = "ShadowVpnService"
|
|
val BASE = "/data/data/com.github.shadowsocks/"
|
|
val SHADOWSOCKS_CONF = "{\"server\": [%s], \"server_port\": %d, \"local_port\": %d, \"password\": %s, \"timeout\": %d}"
|
|
val MSG_CONNECT_START: Int = 0
|
|
val MSG_CONNECT_FINISH: Int = 1
|
|
val MSG_CONNECT_SUCCESS: Int = 2
|
|
val MSG_CONNECT_FAIL: Int = 3
|
|
val MSG_HOST_CHANGE: Int = 4
|
|
val MSG_STOP_SELF: Int = 5
|
|
val VPN_MTU = 1500
|
|
|
|
var conn: ParcelFileDescriptor = null
|
|
var udpgw: String = null
|
|
var notificationManager: NotificationManager = null
|
|
var receiver: BroadcastReceiver = null
|
|
var appHost: String = null
|
|
var remotePort: Int = 0
|
|
var localPort: Int = 0
|
|
var sitekey: String = null
|
|
var settings: SharedPreferences = null
|
|
var isGlobalProxy: Boolean = false
|
|
var isGFWList: Boolean = false
|
|
var isBypassApps: Boolean = false
|
|
var isDNSProxy: Boolean = false
|
|
var isHTTPProxy: Boolean = false
|
|
var encMethod: String = null
|
|
var apps: Array[ProxiedApp] = null
|
|
|
|
val handler: Handler = new Handler {
|
|
override def handleMessage(msg: Message) {
|
|
val ed: SharedPreferences.Editor = settings.edit
|
|
msg.what match {
|
|
case MSG_CONNECT_START =>
|
|
ed.putBoolean("isConnecting", true)
|
|
case MSG_CONNECT_FINISH =>
|
|
ed.putBoolean("isConnecting", false)
|
|
case MSG_CONNECT_SUCCESS =>
|
|
ed.putBoolean("isRunning", true)
|
|
case MSG_CONNECT_FAIL =>
|
|
ed.putBoolean("isRunning", false)
|
|
case MSG_HOST_CHANGE =>
|
|
ed.putString("appHost", appHost)
|
|
case MSG_STOP_SELF =>
|
|
destroy()
|
|
stopSelf()
|
|
}
|
|
ed.commit
|
|
super.handleMessage(msg)
|
|
}
|
|
}
|
|
|
|
def getPid(name: String): Int = {
|
|
try {
|
|
val reader: BufferedReader = new BufferedReader(new FileReader(BASE + name + ".pid"))
|
|
val line = reader.readLine
|
|
return Integer.valueOf(line)
|
|
} catch {
|
|
case e: FileNotFoundException => {
|
|
Log.e(TAG, "Cannot open pid file: " + name)
|
|
}
|
|
case e: IOException => {
|
|
Log.e(TAG, "Cannot read pid file: " + name)
|
|
}
|
|
case e: NumberFormatException => {
|
|
Log.e(TAG, "Invalid pid", e)
|
|
}
|
|
}
|
|
-1
|
|
}
|
|
|
|
def startShadowsocksDaemon() {
|
|
new Thread {
|
|
override def run() {
|
|
val cmd: String = (BASE +
|
|
"shadowsocks -s \"%s\" -p \"%d\" -l \"%d\" -k \"%s\" -m \"%s\" -f " +
|
|
BASE +
|
|
"shadowsocks.pid").format(appHost, remotePort, localPort, sitekey, encMethod)
|
|
System.exec(cmd)
|
|
}
|
|
}.start()
|
|
}
|
|
|
|
def getVersionName: String = {
|
|
var version: String = null
|
|
try {
|
|
val pi: PackageInfo = getPackageManager.getPackageInfo(getPackageName, 0)
|
|
version = pi.versionName
|
|
} catch {
|
|
case e: PackageManager.NameNotFoundException => {
|
|
version = "Package name not found"
|
|
}
|
|
}
|
|
version
|
|
}
|
|
|
|
def handleCommand(intent: Intent) {
|
|
|
|
if (VpnService.prepare(this) != null) {
|
|
val i = new Intent(this, classOf[ShadowVpnActivity])
|
|
i.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
|
startActivity(i)
|
|
stopSelf()
|
|
return
|
|
}
|
|
|
|
appHost = settings.getString("proxy", "127.0.0.1")
|
|
sitekey = settings.getString("sitekey", "default")
|
|
encMethod = settings.getString("encMethod", "table")
|
|
try {
|
|
remotePort = Integer.valueOf(settings.getString("remotePort", "1984"))
|
|
} catch {
|
|
case ex: NumberFormatException => {
|
|
remotePort = 1984
|
|
}
|
|
}
|
|
try {
|
|
localPort = Integer.valueOf(settings.getString("port", "1984"))
|
|
} catch {
|
|
case ex: NumberFormatException => {
|
|
localPort = 1984
|
|
}
|
|
}
|
|
isGlobalProxy = settings.getBoolean("isGlobalProxy", false)
|
|
isGFWList = settings.getBoolean("isGFWList", false)
|
|
isBypassApps = settings.getBoolean("isBypassApps", false)
|
|
isDNSProxy = settings.getBoolean("isDNSProxy", false)
|
|
isHTTPProxy = settings.getBoolean("isHTTPProxy", false)
|
|
if (isHTTPProxy) {
|
|
localPort -= 1
|
|
}
|
|
new Thread(new Runnable {
|
|
def run() {
|
|
handler.sendEmptyMessage(MSG_CONNECT_START)
|
|
|
|
// Resolve server address
|
|
var resolved: Boolean = false
|
|
if (!InetAddressUtils.isIPv4Address(appHost) && !InetAddressUtils.isIPv6Address(appHost)) {
|
|
Utils.resolve(appHost, enableIPv6 = true) match {
|
|
case Some(addr) =>
|
|
appHost = addr
|
|
resolved = true
|
|
case None => resolved = false
|
|
}
|
|
} else {
|
|
resolved = true
|
|
}
|
|
|
|
// Resolve UDP gateway
|
|
if (resolved) {
|
|
Utils.resolve("u.maxcdn.info", enableIPv6 = false) match {
|
|
case Some(host) => udpgw = host
|
|
case None => resolved = false
|
|
}
|
|
}
|
|
|
|
if (resolved && handleConnection) {
|
|
handler.sendEmptyMessageDelayed(MSG_CONNECT_SUCCESS, 300)
|
|
} else {
|
|
notifyAlert(getString(R.string.forward_fail), getString(R.string.service_failed))
|
|
handler.sendEmptyMessageDelayed(MSG_CONNECT_FAIL, 300)
|
|
handler.sendEmptyMessageDelayed(MSG_STOP_SELF, 500)
|
|
}
|
|
handler.sendEmptyMessageDelayed(MSG_CONNECT_FINISH, 300)
|
|
}
|
|
}).start()
|
|
markServiceStarted()
|
|
}
|
|
|
|
def waitForProcess(name: String): Boolean = {
|
|
val pid: Int = getPid(name)
|
|
if (pid == -1) return false
|
|
Exec.hangupProcessGroup(-pid)
|
|
val t: Thread = new Thread {
|
|
override def run() {
|
|
Exec.waitFor(-pid)
|
|
Log.d(TAG, "Successfully exit pid: " + pid)
|
|
}
|
|
}
|
|
t.start()
|
|
try {
|
|
t.join(300)
|
|
} catch {
|
|
case ignored: InterruptedException => {
|
|
}
|
|
}
|
|
!t.isAlive
|
|
}
|
|
|
|
def startVpn() {
|
|
|
|
val address = appHost.split('.')
|
|
val prefix1 = address(0)
|
|
val prefix2 = address.slice(0, 2).mkString(".")
|
|
val prefix3 = address.slice(0, 3).mkString(".")
|
|
|
|
val builder = new Builder()
|
|
builder
|
|
.setSession(getString(R.string.app_name))
|
|
.setMtu(VPN_MTU)
|
|
.addAddress("172.16.0.1", 24)
|
|
.addDnsServer("8.8.8.8")
|
|
.addDnsServer("8.8.4.4")
|
|
|
|
if (InetAddressUtils.isIPv6Address(appHost)) {
|
|
builder.addRoute("0.0.0.0", 0)
|
|
} else if (isGFWList) {
|
|
val gfwList = getResources.getStringArray(R.array.gfw_list)
|
|
gfwList.foreach(addr =>
|
|
if (addr != prefix2) {
|
|
builder.addRoute(addr + ".0.0", 16)
|
|
} else {
|
|
for (i <- 0 to 255) {
|
|
val prefix = Array(addr, i.toString).mkString(".")
|
|
if (prefix != prefix3) builder.addRoute(prefix + ".0", 24)
|
|
}
|
|
})
|
|
builder.addRoute("8.8.0.0", 16)
|
|
} else {
|
|
for (i <- 1 to 254) {
|
|
if (i != 127 && i != 172 && i != 192 && i != 10 && i.toString != prefix1) {
|
|
builder.addRoute(i + ".0.0.0", 8)
|
|
} else if (i.toString == prefix1) {
|
|
for (j <- 0 to 255) {
|
|
val prefix = Array(i.toString, j.toString).mkString(".")
|
|
if (prefix != prefix2) builder.addRoute(prefix + ".0.0", 16)
|
|
}
|
|
}
|
|
builder.addRoute("8.8.0.0", 16)
|
|
}
|
|
}
|
|
|
|
conn = builder.establish()
|
|
if (conn == null) {
|
|
stopSelf()
|
|
return
|
|
}
|
|
|
|
val fd = conn.getFd
|
|
|
|
val cmd = (BASE +
|
|
"tun2socks --netif-ipaddr 172.16.0.2 --udpgw-remote-server-addr %s:7300 " +
|
|
"--netif-netmask 255.255.255.0 --socks-server-addr 127.0.0.1:%d --tunfd %d --tunmtu %d --pid " +
|
|
BASE +
|
|
"tun2socks.pid").format(udpgw, localPort, fd, VPN_MTU)
|
|
|
|
Log.d(TAG, cmd)
|
|
System.exec(cmd)
|
|
}
|
|
|
|
/** Called when the activity is first created. */
|
|
def handleConnection: Boolean = {
|
|
startShadowsocksDaemon()
|
|
startVpn()
|
|
true
|
|
}
|
|
|
|
def initSoundVibrateLights(notification: Notification) {
|
|
notification.sound = null
|
|
notification.defaults |= Notification.DEFAULT_LIGHTS
|
|
}
|
|
|
|
def markServiceStarted() {
|
|
ShadowVpnService.sRunningInstance = new WeakReference[ShadowVpnService](this)
|
|
}
|
|
|
|
def markServiceStopped() {
|
|
ShadowVpnService.sRunningInstance = null
|
|
}
|
|
|
|
def notifyAlert(title: String, info: String) {
|
|
val openIntent: Intent = new Intent(this, classOf[Shadowsocks])
|
|
openIntent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)
|
|
val contentIntent: PendingIntent = PendingIntent.getActivity(this, 0, openIntent, 0)
|
|
val builder: NotificationCompat.Builder = new NotificationCompat.Builder(this)
|
|
builder
|
|
.setSmallIcon(R.drawable.ic_stat_shadowsocks)
|
|
.setWhen(0)
|
|
.setTicker(title)
|
|
.setContentTitle(getString(R.string.app_name))
|
|
.setContentText(info)
|
|
.setContentIntent(contentIntent)
|
|
.setAutoCancel(true)
|
|
notificationManager.notify(1, builder.build)
|
|
}
|
|
|
|
override def onBind(intent: Intent): IBinder = {
|
|
val action = intent.getAction
|
|
if (VpnService.SERVICE_INTERFACE == action) {
|
|
return super.onBind(intent)
|
|
}
|
|
null
|
|
}
|
|
|
|
override def onCreate() {
|
|
super.onCreate()
|
|
EasyTracker.getTracker.sendEvent("service", "start", getVersionName, 0L)
|
|
settings = PreferenceManager.getDefaultSharedPreferences(this)
|
|
notificationManager = getSystemService(Context.NOTIFICATION_SERVICE)
|
|
.asInstanceOf[NotificationManager]
|
|
|
|
// register close receiver
|
|
val filter = new IntentFilter()
|
|
filter.addAction(Intent.ACTION_SHUTDOWN)
|
|
filter.addAction(Utils.CLOSE_ACTION)
|
|
receiver = new BroadcastReceiver {
|
|
def onReceive(p1: Context, p2: Intent) {
|
|
destroy()
|
|
stopSelf()
|
|
}
|
|
}
|
|
registerReceiver(receiver, filter)
|
|
}
|
|
|
|
def destroy() {
|
|
EasyTracker.getTracker.sendEvent("service", "stop", getVersionName, 0L)
|
|
if (receiver != null) {
|
|
unregisterReceiver(receiver)
|
|
receiver = null
|
|
}
|
|
new Thread {
|
|
override def run() {
|
|
onDisconnect()
|
|
}
|
|
}.start()
|
|
val ed: SharedPreferences.Editor = settings.edit
|
|
ed.putBoolean("isRunning", false)
|
|
ed.putBoolean("isConnecting", false)
|
|
ed.commit
|
|
if (conn != null) {
|
|
conn.close()
|
|
conn = null
|
|
}
|
|
notificationManager.cancel(1)
|
|
markServiceStopped()
|
|
}
|
|
|
|
/** Called when the activity is closed. */
|
|
override def onDestroy() {
|
|
destroy()
|
|
super.onDestroy()
|
|
}
|
|
|
|
def onDisconnect() {
|
|
val sb = new StringBuilder
|
|
if (!waitForProcess("shadowsocks")) {
|
|
sb ++= "kill -9 `cat /data/data/com.github.shadowsocks/shadowsocks.pid`" ++= "\n"
|
|
sb ++= "killall -9 shadowsocks" ++= "\n"
|
|
}
|
|
if (!waitForProcess("tun2socks")) {
|
|
sb ++= "kill -9 `cat /data/data/com.github.shadowsocks/tun2socks.pid`" ++= "\n"
|
|
sb ++= "killall -9 tun2socks" ++= "\n"
|
|
}
|
|
Utils.runCommand(sb.toString())
|
|
}
|
|
|
|
override def onStart(intent: Intent, startId: Int) {
|
|
handleCommand(intent)
|
|
}
|
|
|
|
override def onStartCommand(intent: Intent, flags: Int, startId: Int): Int = {
|
|
handleCommand(intent)
|
|
Service.START_STICKY
|
|
}
|
|
|
|
override def onRevoke() {
|
|
stopSelf()
|
|
}
|
|
} |