Files
shadowsocks-android/src/main/scala/com/github/shadowsocks/ShadowsocksService.scala
T
2013-10-29 11:58:23 +08:00

603 lines
19 KiB
Scala

/*
* Shadowsocks - A shadowsocks client for Android
* Copyright (C) 2013 <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.Notification
import android.app.NotificationManager
import android.app.PendingIntent
import android.app.Service
import android.content._
import android.content.pm.PackageInfo
import android.content.pm.PackageManager
import android.os._
import android.support.v4.app.NotificationCompat
import android.util.Log
import com.google.analytics.tracking.android.{Fields, MapBuilder, EasyTracker}
import java.io._
import java.lang.reflect.InvocationTargetException
import java.lang.reflect.Method
import org.apache.http.conn.util.InetAddressUtils
import scala.collection._
import java.util.{TimerTask, Timer}
import android.net.TrafficStats
import android.graphics._
import scala.concurrent.ops._
import scala.Some
case class TrafficStat(tx: Long, rx: Long, timestamp: Long)
object ShadowsocksService {
def isServiceStarted(context: Context): Boolean = {
Utils.isServiceStarted("com.github.shadowsocks.ShadowsocksService", context)
}
}
class ShadowsocksService extends Service {
val TAG = "ShadowsocksService"
val BASE = "/data/data/com.github.shadowsocks/"
val CMD_IPTABLES_RETURN = " -t nat -A OUTPUT -p tcp -d 0.0.0.0 -j RETURN\n"
val CMD_IPTABLES_REDIRECT_ADD_SOCKS = " -t nat -A OUTPUT -p tcp " + "-j REDIRECT --to 8123\n"
val CMD_IPTABLES_DNAT_ADD_SOCKS = " -t nat -A OUTPUT -p tcp " +
"-j DNAT --to-destination 127.0.0.1:8123\n"
val DNS_PORT = 8153
val MSG_CONNECT_FINISH = 1
val MSG_CONNECT_SUCCESS = 2
val MSG_CONNECT_FAIL = 3
val MSG_STOP_SELF = 4
val MSG_VPN_ERROR = 5
private val mStartForegroundSignature = Array[Class[_]](classOf[Int], classOf[Notification])
private val mStopForegroundSignature = Array[Class[_]](classOf[Boolean])
private val mSetForegroundSignature = Array[Class[_]](classOf[Boolean])
var receiver: BroadcastReceiver = null
var notificationManager: NotificationManager = null
var config: Config = null
var hasRedirectSupport = false
var apps: Array[ProxiedApp] = null
val uid = Process.myUid()
private var mSetForeground: Method = null
private var mStartForeground: Method = null
private var mStopForeground: Method = null
private var mSetForegroundArgs = new Array[AnyRef](1)
private var mStartForegroundArgs = new Array[AnyRef](2)
private var mStopForegroundArgs = new Array[AnyRef](1)
private var state = State.INIT
private var last: TrafficStat = null
private var lastTxRate = 0
private var lastRxRate = 0
private var timer: Timer = null
private val TIMER_INTERVAL = 2
private def changeState(s: Int) {
if (state != s) {
state = s
val intent = new Intent(Action.UPDATE_STATE)
intent.putExtra(Extra.STATE, state)
sendBroadcast(intent)
}
}
val handler: Handler = new Handler {
override def handleMessage(msg: Message) {
msg.what match {
case MSG_CONNECT_SUCCESS =>
changeState(State.CONNECTED)
case MSG_CONNECT_FAIL =>
changeState(State.STOPPED)
case MSG_STOP_SELF =>
stopSelf()
case _ =>
}
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() {
val cmd: String = (BASE +
"shadowsocks -b 127.0.0.1 -s \"%s\" -p \"%d\" -l \"%d\" -k \"%s\" -m \"%s\" -f " +
BASE + "shadowsocks.pid")
.format(config.proxy, config.remotePort, config.localPort, config.sitekey, config.encMethod)
if (BuildConfig.DEBUG) Log.d(TAG, cmd)
Utils.runCommand(cmd)
}
def startDnsDaemon() {
val cmd: String = BASE + "pdnsd -c " + BASE + "pdnsd.conf"
val conf: String = Config.PDNSD.format("127.0.0.1")
Config.printToFile(new File(BASE + "pdnsd.conf"))(p => {
p.println(conf)
})
Utils.runCommand(cmd)
}
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 (intent == null) {
stopSelf()
return
}
changeState(State.CONNECTING)
config = Extra.get(intent)
if (config.isTrafficStat) {
// initialize timer
val task = new TimerTask {
def run() {
val pm = getSystemService(Context.POWER_SERVICE).asInstanceOf[PowerManager]
val now = new TrafficStat(TrafficStats.getUidTxBytes(uid),
TrafficStats.getUidRxBytes(uid), java.lang.System.currentTimeMillis())
val txRate = ((now.tx - last.tx) / 1024 / TIMER_INTERVAL).toInt
val rxRate = ((now.rx - last.rx) / 1024 / TIMER_INTERVAL).toInt
last = now
if (lastTxRate == txRate && lastRxRate == rxRate) {
return
} else {
lastTxRate = txRate
lastRxRate = rxRate
}
if ((pm.isScreenOn && state == State.CONNECTED) || (txRate == 0 && rxRate == 0)) {
notifyForegroundAlert(getString(R.string.forward_success),
getString(R.string.service_status).format(math.max(txRate, rxRate)), math.max(txRate, rxRate))
}
}
}
last = new TrafficStat(TrafficStats.getUidTxBytes(uid),
TrafficStats.getUidRxBytes(uid), java.lang.System.currentTimeMillis())
timer = new Timer(true)
timer.schedule(task, TIMER_INTERVAL * 1000, TIMER_INTERVAL * 1000)
}
spawn {
if (config.proxy == "198.199.101.152") {
val container = getApplication.asInstanceOf[ShadowsocksApplication].tagContainer
try {
config = Config.getPublicConfig(container, config)
} catch {
case ex: Exception => {
notifyAlert(getString(R.string.forward_fail), getString(R.string.service_failed))
stopSelf()
handler.sendEmptyMessageDelayed(MSG_CONNECT_FAIL, 500)
return
}
}
}
killProcesses()
var resolved: Boolean = false
if (!InetAddressUtils.isIPv4Address(config.proxy) &&
!InetAddressUtils.isIPv6Address(config.proxy)) {
Utils.resolve(config.proxy, enableIPv6 = true) match {
case Some(a) =>
config.proxy = a
resolved = true
case None => resolved = false
}
} else {
resolved = true
}
hasRedirectSupport = Utils.getHasRedirectSupport
if (resolved && handleConnection) {
notifyForegroundAlert(getString(R.string.forward_success),
getString(R.string.service_running).format(config.profileName))
handler.sendEmptyMessageDelayed(MSG_CONNECT_SUCCESS, 500)
} else {
notifyAlert(getString(R.string.forward_fail), getString(R.string.service_failed))
stopSelf()
handler.sendEmptyMessageDelayed(MSG_CONNECT_FAIL, 500)
}
handler.sendEmptyMessageDelayed(MSG_CONNECT_FINISH, 500)
}
}
def startRedsocksDaemon() {
val conf = Config.REDSOCKS.format(config.localPort)
val cmd = "%sredsocks -p %sredsocks.pid -c %sredsocks.conf".format(BASE, BASE, BASE)
Config.printToFile(new File(BASE + "redsocks.conf"))(p => {
p.println(conf)
})
Utils.runRootCommand(cmd)
}
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)
}
}
t.start()
try {
t.join(300)
} catch {
case ignored: InterruptedException => {
}
}
!t.isAlive
}
/** Called when the activity is first created. */
def handleConnection: Boolean = {
startShadowsocksDaemon()
startDnsDaemon()
startRedsocksDaemon()
setupIptables
true
}
def initSoundVibrateLights(notification: Notification) {
notification.sound = null
notification.defaults |= Notification.DEFAULT_LIGHTS
}
def invokeMethod(method: Method, args: Array[AnyRef]) {
try {
method.invoke(this, mStartForegroundArgs: _*)
} catch {
case e: InvocationTargetException => {
Log.w(TAG, "Unable to invoke method", e)
}
case e: IllegalAccessException => {
Log.w(TAG, "Unable to invoke method", e)
}
}
}
def notifyForegroundAlert(title: String, info: String) {
notifyForegroundAlert(title, info, -1)
}
def notifyForegroundAlert(title: String, info: String, rate: Int) {
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 closeIntent: Intent = new Intent(Action.CLOSE)
val actionIntent: PendingIntent = PendingIntent.getBroadcast(this, 0, closeIntent, 0)
val builder: NotificationCompat.Builder = new NotificationCompat.Builder(this)
val icon = getResources.getDrawable(R.drawable.ic_stat_shadowsocks)
if (rate >= 0) {
val bitmap =Utils.getBitmap(rate.toString, icon.getIntrinsicWidth * 4,
icon.getIntrinsicHeight * 4, Color.TRANSPARENT)
builder.setLargeIcon(bitmap)
if (rate < 1000) {
builder.setSmallIcon(R.drawable.ic_stat_speed, rate)
} else if (rate <= 10000) {
val mb = rate / 100 - 10 + 1000
builder.setSmallIcon(R.drawable.ic_stat_speed, mb)
} else {
builder.setSmallIcon(R.drawable.ic_stat_speed, 1091)
}
} else {
builder.setSmallIcon(R.drawable.ic_stat_shadowsocks)
}
builder
.setWhen(0)
.setTicker(title)
.setContentTitle(getString(R.string.app_name))
.setContentText(info)
.setContentIntent(contentIntent)
.addAction(android.R.drawable.ic_menu_close_clear_cancel, getString(R.string.stop),
actionIntent)
startForegroundCompat(1, builder.build)
}
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)
}
def onBind(intent: Intent): IBinder = {
null
}
override def onCreate() {
super.onCreate()
Config.refresh(this)
EasyTracker
.getInstance(this)
.send(MapBuilder
.createEvent(TAG, "start", getVersionName, 0L)
.set(Fields.SESSION_CONTROL, "start")
.build())
notificationManager = this
.getSystemService(Context.NOTIFICATION_SERVICE)
.asInstanceOf[NotificationManager]
try {
mStartForeground = getClass.getMethod("startForeground", mStartForegroundSignature: _*)
mStopForeground = getClass.getMethod("stopForeground", mStopForegroundSignature: _*)
} catch {
case e: NoSuchMethodException => {
mStartForeground = {
mStopForeground = null
mStopForeground
}
}
}
try {
mSetForeground = getClass.getMethod("setForeground", mSetForegroundSignature: _*)
} catch {
case e: NoSuchMethodException => {
throw new IllegalStateException(
"OS doesn't have Service.startForeground OR Service.setForeground!")
}
}
// register close receiver
val filter = new IntentFilter()
filter.addAction(Intent.ACTION_SHUTDOWN)
filter.addAction(Action.CLOSE)
receiver = new BroadcastReceiver() {
def onReceive(p1: Context, p2: Intent) {
stopSelf()
}
}
registerReceiver(receiver, filter)
}
/** Called when the activity is closed. */
override def onDestroy() {
// reset timer
if (timer != null) {
timer.cancel()
timer = null
}
// clean up context
changeState(State.STOPPED)
EasyTracker
.getInstance(this)
.send(MapBuilder
.createEvent(TAG, "stop", getVersionName, 0L)
.set(Fields.SESSION_CONTROL, "stop")
.build())
stopForegroundCompat(1)
if (receiver != null) {
unregisterReceiver(receiver)
receiver = null
}
// reset NAT
killProcesses()
super.onDestroy()
}
def killProcesses() {
Utils.runRootCommand(Utils.getIptables + " -t nat -F OUTPUT")
val sb = new StringBuilder
sb ++= "kill -9 `cat /data/data/com.github.shadowsocks/redsocks.pid`" ++= "\n"
sb ++= "killall -9 redsocks" ++= "\n"
Utils.runRootCommand(sb.toString())
sb.clear()
if (!waitForProcess("pdnsd")) {
sb ++= "kill -9 `cat /data/data/com.github.shadowsocks/pdnsd.pid`" ++= "\n"
sb ++= "killall -9 pdnsd" ++= "\n"
}
if (!waitForProcess("shadowsocks")) {
sb ++= "kill -9 `cat /data/data/com.github.shadowsocks/shadowsocks.pid`" ++= "\n"
sb ++= "killall -9 shadowsocks" ++= "\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
}
def setupIptables: Boolean = {
val init_sb = new StringBuilder
val http_sb = new StringBuilder
init_sb.append(Utils.getIptables).append(" -t nat -F OUTPUT\n")
val cmd_bypass = Utils.getIptables + CMD_IPTABLES_RETURN
if (!InetAddressUtils.isIPv6Address(config.proxy.toUpperCase)) {
init_sb.append(cmd_bypass.replace("-d 0.0.0.0", "-d " + config.proxy))
}
init_sb.append(cmd_bypass.replace("0.0.0.0", "127.0.0.1"))
if (hasRedirectSupport) {
init_sb
.append(Utils.getIptables)
.append(" -t nat -A OUTPUT -p udp --dport 53 -j REDIRECT --to ")
.append(DNS_PORT)
.append("\n")
} else {
init_sb
.append(Utils.getIptables)
.append(" -t nat -A OUTPUT -p udp --dport 53 -j DNAT --to-destination 127.0.0.1:")
.append(DNS_PORT)
.append("\n")
}
if (config.isGFWList) {
val chn_list: Array[String] = getResources.getStringArray(R.array.chn_list)
for (item <- chn_list) {
init_sb.append(cmd_bypass.replace("0.0.0.0", item))
}
}
if (config.isGlobalProxy || config.isBypassApps) {
http_sb.append(if (hasRedirectSupport) {
Utils.getIptables + CMD_IPTABLES_REDIRECT_ADD_SOCKS
} else {
Utils.getIptables + CMD_IPTABLES_DNAT_ADD_SOCKS
})
}
if (!config.isGlobalProxy) {
if (apps == null || apps.length <= 0) {
apps = AppManager.getProxiedApps(this, config.proxiedAppString)
}
val uidSet: mutable.HashSet[Int] = new mutable.HashSet[Int]
for (app <- apps) {
if (app.proxied) {
uidSet.add(app.uid)
}
}
for (uid <- uidSet) {
if (!config.isBypassApps) {
http_sb.append((if (hasRedirectSupport) {
Utils.getIptables + CMD_IPTABLES_REDIRECT_ADD_SOCKS
} else {
Utils.getIptables + CMD_IPTABLES_DNAT_ADD_SOCKS
}).replace("-t nat", "-t nat -m owner --uid-owner " + uid))
} else {
init_sb.append(cmd_bypass.replace("-d 0.0.0.0", "-m owner --uid-owner " + uid))
}
}
}
val init_rules: String = init_sb.toString()
Utils.runRootCommand(init_rules, 30 * 1000)
val redt_rules: String = http_sb.toString()
Utils.runRootCommand(redt_rules)
true
}
/**
* This is a wrapper around the new startForeground method, using the older
* APIs if it is not available.
*/
def startForegroundCompat(id: Int, notification: Notification) {
if (mStartForeground != null) {
mStartForegroundArgs(0) = int2Integer(id)
mStartForegroundArgs(1) = notification
invokeMethod(mStartForeground, mStartForegroundArgs)
return
}
mSetForegroundArgs(0) = boolean2Boolean(x = true)
invokeMethod(mSetForeground, mSetForegroundArgs)
notificationManager.notify(id, notification)
}
/**
* This is a wrapper around the new stopForeground method, using the older
* APIs if it is not available.
*/
def stopForegroundCompat(id: Int) {
if (mStopForeground != null) {
mStopForegroundArgs(0) = boolean2Boolean(x = true)
try {
mStopForeground.invoke(this, mStopForegroundArgs: _*)
} catch {
case e: InvocationTargetException => {
Log.w(TAG, "Unable to invoke stopForeground", e)
}
case e: IllegalAccessException => {
Log.w(TAG, "Unable to invoke stopForeground", e)
}
}
return
}
notificationManager.cancel(id)
mSetForegroundArgs(0) = boolean2Boolean(x = false)
invokeMethod(mSetForeground, mSetForegroundArgs)
}
}