diff --git a/.gitignore b/.gitignore index 659d10c9..9ea44594 100644 --- a/.gitignore +++ b/.gitignore @@ -14,6 +14,7 @@ gen_* *.class *.o local.sbt +.deps #Intellij IDEA *.iml @@ -26,8 +27,6 @@ out/ #NDK src/main/obj src/main/libs -src/main/assets/armeabi -src/main/assets/armeabi-v7a -src/main/assets/x86 -src/main/assets/api-16/ +src/main/assets/armeabi-v7a/* +src/main/assets/x86/* jni/Application.mk diff --git a/.gitmodules b/.gitmodules index 0d7fb324..0e932ce4 100644 --- a/.gitmodules +++ b/.gitmodules @@ -24,3 +24,9 @@ path = src/main/jni/pdnsd url = https://github.com/shadowsocks/pdnsd.git branch = shadowsocks-android +[submodule "kcptun/kcptun"] + path = kcptun/kcptun + url = https://github.com/shadowsocks/kcptun +[submodule "kcptun/go"] + path = kcptun/go + url = https://github.com/shadowsocks/go diff --git a/.travis.yml b/.travis.yml index 94398a1d..1f05bad5 100644 --- a/.travis.yml +++ b/.travis.yml @@ -31,6 +31,7 @@ install: - ./travis-ci/setup.sh - export NDK_CCACHE=ccache - export ARCH=`uname -m` + - export GOROOT_BOOTSTRAP=$HOME/.android/go - export ANDROID_NDK_HOME=$HOME/.android/android-ndk-r12b - export ANDROID_HOME=$HOME/.android/android-sdk-linux - export PATH=${ANDROID_NDK_HOME}:${ANDROID_HOME}/tools:${ANDROID_HOME}/platform-tools:${PATH} diff --git a/README.md b/README.md index 26237309..63c22fc8 100644 --- a/README.md +++ b/README.md @@ -12,6 +12,7 @@ A [shadowsocks](http://shadowsocks.org) client for Android, written in Scala. * JDK 1.8 * SBT 0.13.0+ +* GO 1.4+ * Android SDK r25+ * Android NDK r12b+ @@ -19,6 +20,7 @@ A [shadowsocks](http://shadowsocks.org) client for Android, written in Scala. * Set environment variable `ANDROID_HOME` to `/path/to/android-sdk` * Set environment variable `ANDROID_NDK_HOME` to `/path/to/android-ndk` +* Set environment variable `GOROOT_BOOTSTRAP` to `/path/to/go` * Create your key following the instructions at https://developer.android.com/studio/publish/app-signing.html * Put your key in ~/.keystore * Create `local.properties` from `local.properties.example` with your own key information @@ -26,33 +28,11 @@ A [shadowsocks](http://shadowsocks.org) client for Android, written in Scala. ```bash git submodule update --init - + # Build the App sbt native-build clean android:package-release ``` -#### BUILD on Mac OS X (with HomeBrew) - -* Install Android SDK and NDK by run `brew install android-ndk android-sdk` -* Add `export ANDROID_HOME=/usr/local/Cellar/android-sdk/$version` to your .bashrc , then reopen the shell to load it. -* Add `export ANDROID_NDK_HOME=/usr/local/Cellar/android-ndk/$version` to your .bashrc , then reopen the shell to load it. -* echo "y" | android update sdk --filter tools,platform-tools,build-tools-23.0.2,android-23,extra-google-m2repository --no-ui -a -* echo "y" | android update sdk --filter extra-android-m2repository --no-ui --no-https -a -* Create your key following the instructions at http://developer.android.com/guide/publishing/app-signing.html#cert -* Put your key in ~/.keystore -* Create `local.properties` from `local.properties.example` with your own key information . -* Invoke the building like this - -```bash - git submodule update --init - - # Build native binaries - ./build.sh - - # Build the apk - sbt clean android:package-release -``` - ## OPEN SOURCE LICENSES * shadowsocks-libev: [GPLv3](https://github.com/shadowsocks/shadowsocks-libev/blob/master/LICENSE) @@ -62,6 +42,7 @@ A [shadowsocks](http://shadowsocks.org) client for Android, written in Scala. * pdnsd: [GPLv3](https://github.com/shadowsocks/shadowsocks-android/blob/master/src/main/jni/pdnsd/COPYING) * libev: [GPLv2](https://github.com/shadowsocks/shadowsocks-android/blob/master/src/main/jni/libev/LICENSE) * libevent: [BSD](https://github.com/shadowsocks/libevent/blob/master/LICENSE) +* kcptun: [MIT](https://github.com/xtaci/kcptun) ### LICENSE diff --git a/build.sbt b/build.sbt index 19f12b0f..40b00afb 100644 --- a/build.sbt +++ b/build.sbt @@ -55,6 +55,8 @@ proguardCache in Android := Seq() proguardOptions in Android ++= Seq( "-keep class com.github.shadowsocks.System { *; }", + "-dontwarn com.google.android.gms.**", + "-keep class com.google.android.gms.**", "-dontnote com.google.android.gms.internal.**", "-dontnote com.j256.ormlite.**", "-dontnote me.dm7.barcodescannerview.**", diff --git a/build.sh b/build.sh index 513c0323..47b5b5fb 100755 --- a/build.sh +++ b/build.sh @@ -4,21 +4,25 @@ function try () { "$@" || exit -1 } -try pushd src/main - -# Build -#try $ANDROID_NDK_HOME/ndk-build clean +# Build native binaries +pushd src/main try $ANDROID_NDK_HOME/ndk-build -j8 -# copy executables rm -rf assets/armeabi-v7a rm -rf assets/x86 mkdir -p assets/armeabi-v7a mkdir -p assets/x86 + +#copy executables for app in pdnsd redsocks ss-local ss-tunnel tun2socks do + echo $app try mv libs/armeabi-v7a/$app assets/armeabi-v7a/ try mv libs/x86/$app assets/x86/ done +popd -try popd +# Build kcptun +pushd kcptun +try ./make.bash +popd diff --git a/kcptun/go b/kcptun/go new file mode 160000 index 00000000..f3e7e7b6 --- /dev/null +++ b/kcptun/go @@ -0,0 +1 @@ +Subproject commit f3e7e7b69f0fd0c22858d09b4dd3764ed703d140 diff --git a/kcptun/kcptun b/kcptun/kcptun new file mode 160000 index 00000000..eced3949 --- /dev/null +++ b/kcptun/kcptun @@ -0,0 +1 @@ +Subproject commit eced3949b2f33e1801aec6124a01922482358f3e diff --git a/kcptun/make.bash b/kcptun/make.bash new file mode 100755 index 00000000..65e23d72 --- /dev/null +++ b/kcptun/make.bash @@ -0,0 +1,65 @@ +#!/bin/bash + +function try () { +"$@" || exit -1 +} + +[ -z "$ANDROID_NDK_HOME" ] && ANDROID_NDK_HOME=~/android-ndk-r12b + +DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" +DEPS=$DIR/.deps +ANDROID_ARM_TOOLCHAIN=$DEPS/android-toolchain-16-arm +ANDROID_X86_TOOLCHAIN=$DEPS/android-toolchain-16-x86 + +ANDROID_ARM_CC=$ANDROID_ARM_TOOLCHAIN/bin/arm-linux-androideabi-gcc +ANDROID_ARM_STRIP=$ANDROID_ARM_TOOLCHAIN/bin/arm-linux-androideabi-strip + +ANDROID_X86_CC=$ANDROID_X86_TOOLCHAIN/bin/i686-linux-android-gcc +ANDROID_X86_STRIP=$ANDROID_X86_TOOLCHAIN/bin/i686-linux-android-strip + + +if [ ! -d "$DEPS" ]; then + mkdir -p $DEPS +fi + +if [ ! -d "$ANDROID_ARM_TOOLCHAIN" ]; then + echo "Make standalone toolchain for ARM arch" + $ANDROID_NDK_HOME/build/tools/make_standalone_toolchain.py --arch arm \ + --api 16 --install-dir $ANDROID_ARM_TOOLCHAIN +fi + +if [ ! -d "$ANDROID_X86_TOOLCHAIN" ]; then + echo "Make standalone toolchain for X86 arch" + $ANDROID_NDK_HOME/build/tools/make_standalone_toolchain.py --arch x86 \ + --api 16 --install-dir $ANDROID_X86_TOOLCHAIN +fi + +if [ ! -d "$DIR/go/bin" ]; then + echo "Build the custom go" + + pushd $DIR/go/src + try ./make.bash + popd +fi + +export GOROOT=$DIR/go +export GOPATH=$DEPS +export PATH=$GOROOT/bin:$PATH + +pushd kcptun/client + +echo "Get dependences for kcptun" +go get + +echo "Cross compile kcptun for arm" +try env CGO_ENABLED=1 CC=$ANDROID_ARM_CC GOOS=android GOARCH=arm GOARM=7 go build +try $ANDROID_ARM_STRIP client +try mv client $DIR/../src/main/assets/armeabi-v7a/kcptun + +echo "Cross compile kcptun for x86" +try env CGO_ENABLED=1 CC=$ANDROID_X86_CC GOOS=android GOARCH=386 go build +try $ANDROID_X86_STRIP client +try mv client $DIR/../src/main/assets/x86/kcptun +popd + +echo "Successfully build kcptun" diff --git a/project/plugins.sbt b/project/plugins.sbt index 649f9d2c..3e14dab1 100644 --- a/project/plugins.sbt +++ b/project/plugins.sbt @@ -1,4 +1,4 @@ -addSbtPlugin("org.scala-android" % "sbt-android" % "1.6.8") +addSbtPlugin("org.scala-android" % "sbt-android" % "1.6.10") addSbtPlugin("com.timushev.sbt" % "sbt-updates" % "0.1.10") diff --git a/src/main/AndroidManifest.xml b/src/main/AndroidManifest.xml index caf790ef..ebb17b4f 100644 --- a/src/main/AndroidManifest.xml +++ b/src/main/AndroidManifest.xml @@ -1,7 +1,7 @@ + android:versionCode="144" + android:versionName="3.0.2"> diff --git a/src/main/assets/pages/about.html b/src/main/assets/pages/about.html index e26db438..f4f70fbc 100644 --- a/src/main/assets/pages/about.html +++ b/src/main/assets/pages/about.html @@ -296,6 +296,7 @@ along with this program. If not, see http
  • pdnsd: GPLv3
  • libev: GPLv2
  • libevent: BSD
  • +
  • kcptun: MIT
  • diff --git a/src/main/jni/shadowsocks-libev b/src/main/jni/shadowsocks-libev index d4aabcb4..915047e9 160000 --- a/src/main/jni/shadowsocks-libev +++ b/src/main/jni/shadowsocks-libev @@ -1 +1 @@ -Subproject commit d4aabcb40b4bcbca352269a1474a19de691950d0 +Subproject commit 915047e957d5bd581be8e144ed92a1bb05441af5 diff --git a/src/main/res/values-ru/strings.xml b/src/main/res/values-ru/strings.xml index 98a5222a..9a51b96d 100644 --- a/src/main/res/values-ru/strings.xml +++ b/src/main/res/values-ru/strings.xml @@ -134,4 +134,11 @@ Отправлено: Получено: + + + Включить Shadowsocks над KCPTUN + KCP порт + %d (KCPTUN омер порта удалённого сервера) + KCP параметры + Командная строка KCPTUN diff --git a/src/main/res/values-zh-rCN/strings.xml b/src/main/res/values-zh-rCN/strings.xml index 6309eb9a..40252f06 100644 --- a/src/main/res/values-zh-rCN/strings.xml +++ b/src/main/res/values-zh-rCN/strings.xml @@ -134,4 +134,11 @@ 发送: 接收: + + 启用 KCP 协议 + KCP 端口 + %d(远端 KCPTUN 服务器的端口号) + KCP 参数 + KCPTUN 的命令行参数 + diff --git a/src/main/res/values-zh/strings.xml b/src/main/res/values-zh/strings.xml index 94af6526..76bd9b08 100644 --- a/src/main/res/values-zh/strings.xml +++ b/src/main/res/values-zh/strings.xml @@ -136,4 +136,11 @@ 傳送: 接收: + + 啟用 KCP 協定 + KCP 連接埠 + %d(遠端 KCPTUN 伺服器的連接埠號碼) + KCP 參數 + KCPTUN 的命令行參數 + diff --git a/src/main/res/values/strings.xml b/src/main/res/values/strings.xml index 8c472f3b..d13dc92a 100644 --- a/src/main/res/values/strings.xml +++ b/src/main/res/values/strings.xml @@ -139,4 +139,12 @@ Sent: Received: + + + KCP + Enable Shadowsocks over KCP + KCP Port + %d (port number of the remote KCPTUN server) + KCP Parameters (CLI) + The command line of KCPTUN diff --git a/src/main/res/xml/pref_all.xml b/src/main/res/xml/pref_all.xml index c6689417..6c85273b 100644 --- a/src/main/res/xml/pref_all.xml +++ b/src/main/res/xml/pref_all.xml @@ -77,6 +77,31 @@ + + + + + + + + + + { + val cm = context.getSystemService(Context.CONNECTIVITY_SERVICE).asInstanceOf[ConnectivityManager] + val activeNetwork = cm.getActiveNetworkInfo() + val isConnected = activeNetwork != null && activeNetwork.isConnected() + + if (isConnected && profile.kcp && kcptunProcess != null) kcptunProcess.restart() + } + var networkReceiverRegistered: Boolean = _ + val binder = new IShadowsocksService.Stub { override def getState: Int = { state @@ -162,6 +174,14 @@ trait BaseService extends Service { closeReceiverRegistered = true } + if (profile.kcp && !networkReceiverRegistered) { + // register network change receiver + val filter = new IntentFilter() + filter.addAction(ConnectivityManager.CONNECTIVITY_ACTION) + registerReceiver(networkReceiver, filter) + networkReceiverRegistered = true + } + app.track(getClass.getSimpleName, "start") changeState(State.CONNECTING) @@ -177,6 +197,10 @@ trait BaseService extends Service { unregisterReceiver(closeReceiver) closeReceiverRegistered = false } + if (networkReceiverRegistered) { + unregisterReceiver(networkReceiver) + networkReceiverRegistered = false + } // Make sure update total traffic when stopping the runner updateTrafficTotal(TrafficMonitor.txTotal, TrafficMonitor.rxTotal) diff --git a/src/main/scala/com/github/shadowsocks/GuardedProcess.scala b/src/main/scala/com/github/shadowsocks/GuardedProcess.scala index 024c1b79..c3eec7dc 100644 --- a/src/main/scala/com/github/shadowsocks/GuardedProcess.scala +++ b/src/main/scala/com/github/shadowsocks/GuardedProcess.scala @@ -56,6 +56,7 @@ class GuardedProcess(cmd: Seq[String]) extends Process { @volatile private var guardThread: Thread = _ @volatile private var isDestroyed: Boolean = _ @volatile private var process: Process = _ + @volatile private var isRestart = false def start(onRestartCallback: () => Unit = null): GuardedProcess = { val semaphore = new Semaphore(1) @@ -76,9 +77,13 @@ class GuardedProcess(cmd: Seq[String]) extends Process { semaphore.release process.waitFor - if (currentTimeMillis - startTime < 1000) { - Log.w(TAG, "process exit too fast, stop guard: " + cmd) - isDestroyed = true + if (isRestart) { + isRestart = false + } else { + if (currentTimeMillis - startTime < 1000) { + Log.w(TAG, "process exit too fast, stop guard: " + cmd) + isDestroyed = true + } } } } catch { @@ -108,6 +113,11 @@ class GuardedProcess(cmd: Seq[String]) extends Process { } } + def restart() { + isRestart = true + process.destroy() + } + def exitValue: Int = throw new UnsupportedOperationException def getErrorStream: InputStream = throw new UnsupportedOperationException def getInputStream: InputStream = throw new UnsupportedOperationException diff --git a/src/main/scala/com/github/shadowsocks/Shadowsocks.scala b/src/main/scala/com/github/shadowsocks/Shadowsocks.scala index 930e2c91..4da220af 100644 --- a/src/main/scala/com/github/shadowsocks/Shadowsocks.scala +++ b/src/main/scala/com/github/shadowsocks/Shadowsocks.scala @@ -94,7 +94,7 @@ object Shadowsocks { private final val TAG = "Shadowsocks" private final val REQUEST_CONNECT = 1 private val EXECUTABLES = Array(Executable.PDNSD, Executable.REDSOCKS, Executable.SS_TUNNEL, Executable.SS_LOCAL, - Executable.TUN2SOCKS) + Executable.TUN2SOCKS, Executable.KCPTUN) } class Shadowsocks extends AppCompatActivity with ServiceBoundContext { @@ -286,7 +286,7 @@ class Shadowsocks extends AppCompatActivity with ServiceBoundContext { def crashRecovery() { val cmd = new ArrayBuffer[String]() - for (task <- Array("ss-local", "ss-tunnel", "pdnsd", "redsocks", "tun2socks")) { + for (task <- Array("ss-local", "ss-tunnel", "pdnsd", "redsocks", "tun2socks", "kcptun")) { cmd.append("killall %s".formatLocal(Locale.ENGLISH, task)) cmd.append("rm -f %1$s/%2$s-nat.conf %1$s/%2$s-vpn.conf" .formatLocal(Locale.ENGLISH, getApplicationInfo.dataDir, task)) @@ -481,7 +481,7 @@ class Shadowsocks extends AppCompatActivity with ServiceBoundContext { private def updatePreferenceScreen(profile: Profile) { if (profile.host == "198.199.101.152") if (adView == null) { adView = new AdView(this) - adView.setAdUnitId("ca-app-pub-9097031975646651/7760346322") + adView.setAdUnitId("ca-app-pub-4984938374218635/1433320509") adView.setAdSize(AdSize.SMART_BANNER) preferences.getView.asInstanceOf[ViewGroup].addView(adView, 1) adView.loadAd(new AdRequest.Builder().build()) diff --git a/src/main/scala/com/github/shadowsocks/ShadowsocksNatService.scala b/src/main/scala/com/github/shadowsocks/ShadowsocksNatService.scala index 40b1b36a..a9b00af3 100644 --- a/src/main/scala/com/github/shadowsocks/ShadowsocksNatService.scala +++ b/src/main/scala/com/github/shadowsocks/ShadowsocksNatService.scala @@ -84,9 +84,15 @@ class ShadowsocksNatService extends BaseService { }) } - val conf = ConfigUtils + val conf = if (profile.kcp) { + ConfigUtils + .SHADOWSOCKS.formatLocal(Locale.ENGLISH, "127.0.0.1", profile.localPort + 90, profile.localPort, + profile.password, profile.method, 600) + } else { + ConfigUtils .SHADOWSOCKS.formatLocal(Locale.ENGLISH, profile.host, profile.remotePort, profile.localPort, profile.password, profile.method, 600) + } Utils.printToFile(new File(getApplicationInfo.dataDir + "/ss-local-nat.conf"))(p => { p.println(conf) }) @@ -109,10 +115,23 @@ class ShadowsocksNatService extends BaseService { sslocalProcess = new GuardedProcess(cmd).start() } - def startTunnel() { + def startKcptunDaemon() { + + val cmd = new ArrayBuffer[String] + cmd += (getApplicationInfo.dataDir + "/kcptun" + , "-r", profile.host + ":" + profile.kcpPort + , "-l", "127.0.0.1:" + (profile.localPort + 90) + , profile.kcpcli) + + if (BuildConfig.DEBUG) Log.d(TAG, cmd.mkString(" ")) + + kcptunProcess = new GuardedProcess(cmd.mkString(" ").split(" ").toSeq).start() + } + + def startDNSTunnel() { if (profile.udpdns) { val conf = ConfigUtils - .SHADOWSOCKS.formatLocal(Locale.ENGLISH, profile.host, profile.remotePort, 8153, + .SHADOWSOCKS.formatLocal(Locale.ENGLISH, profile.host, profile.remotePort, profile.localPort + 53, profile.password, profile.method, 10) Utils.printToFile(new File(getApplicationInfo.dataDir + "/ss-tunnel-nat.conf"))(p => { p.println(conf) @@ -123,11 +142,10 @@ class ShadowsocksNatService extends BaseService { , "-t" , "10" , "-b" , "127.0.0.1" , "-L" , "8.8.8.8:53" + , "-l" , (profile.localPort + 53).toString , "-P" , getApplicationInfo.dataDir , "-c" , getApplicationInfo.dataDir + "/ss-tunnel-nat.conf") - cmd += ("-l" , "8153") - if (profile.auth) cmd += "-A" if (BuildConfig.DEBUG) Log.d(TAG, cmd.mkString(" ")) @@ -135,18 +153,23 @@ class ShadowsocksNatService extends BaseService { sstunnelProcess = new GuardedProcess(cmd).start() } else { - val conf = ConfigUtils - .SHADOWSOCKS.formatLocal(Locale.ENGLISH, profile.host, profile.remotePort, 8163, + val conf = if (profile.kcp) { + ConfigUtils + .SHADOWSOCKS.formatLocal(Locale.ENGLISH, "127.0.0.1", profile.localPort + 90, + profile.localPort + 63, profile.password, profile.method, 10) + } else { + ConfigUtils + .SHADOWSOCKS.formatLocal(Locale.ENGLISH, profile.host, profile.remotePort, profile.localPort + 63, profile.password, profile.method, 10) + } Utils.printToFile(new File(getApplicationInfo.dataDir + "/ss-tunnel-nat.conf"))(p => { p.println(conf) }) val cmdBuf = new ArrayBuffer[String] cmdBuf += (getApplicationInfo.dataDir + "/ss-tunnel" - , "-u" , "-t" , "10" , "-b" , "127.0.0.1" - , "-l" , "8163" + , "-l" , (profile.localPort + 63).toString , "-L" , "8.8.8.8:53" , "-P", getApplicationInfo.dataDir , "-c" , getApplicationInfo.dataDir + "/ss-tunnel-nat.conf") @@ -163,10 +186,10 @@ class ShadowsocksNatService extends BaseService { val conf = if (profile.route == Route.BYPASS_CHN || profile.route == Route.BYPASS_LAN_CHN) { ConfigUtils.PDNSD_DIRECT.formatLocal(Locale.ENGLISH, getApplicationInfo.dataDir, - "127.0.0.1", 8153, getBlackList, 8163, "") + "127.0.0.1", profile.localPort + 53, getBlackList, profile.localPort + 63, "") } else { ConfigUtils.PDNSD_LOCAL.formatLocal(Locale.ENGLISH, getApplicationInfo.dataDir, - "127.0.0.1", 8153, 8163, "") + "127.0.0.1", profile.localPort + 53, profile.localPort + 63, "") } Utils.printToFile(new File(getApplicationInfo.dataDir + "/pdnsd-nat.conf"))(p => { @@ -194,10 +217,13 @@ class ShadowsocksNatService extends BaseService { /** Called when the activity is first created. */ def handleConnection { - startTunnel() - if (!profile.udpdns) startDnsDaemon() + startDNSTunnel() startRedsocksDaemon() startShadowsocksDaemon() + + if (!profile.udpdns) startDnsDaemon() + if (profile.kcp) startKcptunDaemon() + setupIptables() } @@ -212,6 +238,10 @@ class ShadowsocksNatService extends BaseService { } def killProcesses() { + if (kcptunProcess != null) { + kcptunProcess.destroy() + kcptunProcess = null + } if (sslocalProcess != null) { sslocalProcess.destroy() sslocalProcess = null @@ -247,7 +277,8 @@ class ShadowsocksNatService extends BaseService { init_sb.append(cmd_bypass.replace("-p tcp -d 0.0.0.0", "-m owner --uid-owner " + myUid)) init_sb.append(cmd_bypass.replace("-d 0.0.0.0", "--dport 53")) - init_sb.append("iptables -t nat -A OUTPUT -p udp --dport 53 -j DNAT --to-destination 127.0.0.1:8153") + init_sb.append("iptables -t nat -A OUTPUT -p udp --dport 53 -j DNAT --to-destination 127.0.0.1:" + + (profile.localPort + 53)) if (!profile.proxyApps || profile.bypass) { http_sb.append(CMD_IPTABLES_DNAT_ADD_SOCKS) diff --git a/src/main/scala/com/github/shadowsocks/ShadowsocksSettings.scala b/src/main/scala/com/github/shadowsocks/ShadowsocksSettings.scala index cfa2c781..324b7855 100644 --- a/src/main/scala/com/github/shadowsocks/ShadowsocksSettings.scala +++ b/src/main/scala/com/github/shadowsocks/ShadowsocksSettings.scala @@ -18,7 +18,7 @@ object ShadowsocksSettings { // Constants private final val TAG = "ShadowsocksSettings" private val PROXY_PREFS = Array(Key.name, Key.host, Key.remotePort, Key.localPort, Key.password, Key.method, - Key.auth) + Key.auth, Key.kcp, Key.kcpPort, Key.kcpcli) private val FEATURE_PREFS = Array(Key.route, Key.proxyApps, Key.udpdns, Key.ipv6) // Helper functions @@ -36,8 +36,10 @@ object ShadowsocksSettings { } def updateSummaryEditTextPreference(pref: Preference, value: String) { - pref.setSummary(value) - pref.asInstanceOf[SummaryEditTextPreference].setText(value) + if (value != null) { + pref.setSummary(value) + pref.asInstanceOf[SummaryEditTextPreference].setText(value) + } } def updateSwitchPreference(pref: Preference, value: Boolean) { @@ -57,6 +59,9 @@ object ShadowsocksSettings { case Key.udpdns => updateSwitchPreference(pref, profile.udpdns) case Key.auth => updateSwitchPreference(pref, profile.auth) case Key.ipv6 => updateSwitchPreference(pref, profile.ipv6) + case Key.kcp => updateSwitchPreference(pref, profile.kcp) + case Key.kcpPort => updateNumberPickerPreference(pref, profile.kcpPort) + case Key.kcpcli => updateSummaryEditTextPreference(pref, profile.kcpcli) } } } @@ -103,6 +108,19 @@ class ShadowsocksSettings extends PreferenceFragment with OnSharedPreferenceChan app.profileManager.updateProfile(profile) }) + findPreference(Key.kcp).setOnPreferenceChangeListener((_, value) => { + profile.kcp = value.asInstanceOf[Boolean] + app.profileManager.updateProfile(profile) + }) + findPreference(Key.kcpPort).setOnPreferenceChangeListener((_, value) => { + profile.kcpPort = value.asInstanceOf[Int] + app.profileManager.updateProfile(profile) + }) + findPreference(Key.kcpcli).setOnPreferenceChangeListener((_, value) => { + profile.kcpcli = value.asInstanceOf[String] + app.profileManager.updateProfile(profile) + }) + isProxyApps = findPreference(Key.proxyApps).asInstanceOf[SwitchPreference] isProxyApps.setOnPreferenceClickListener(_ => { startActivity(new Intent(activity, classOf[AppManager])) diff --git a/src/main/scala/com/github/shadowsocks/ShadowsocksVpnService.scala b/src/main/scala/com/github/shadowsocks/ShadowsocksVpnService.scala index 213e313a..c9902f56 100644 --- a/src/main/scala/com/github/shadowsocks/ShadowsocksVpnService.scala +++ b/src/main/scala/com/github/shadowsocks/ShadowsocksVpnService.scala @@ -109,6 +109,10 @@ class ShadowsocksVpnService extends VpnService with BaseService { } def killProcesses() { + if (kcptunProcess != null) { + kcptunProcess.destroy() + kcptunProcess = null + } if (sslocalProcess != null) { sslocalProcess.destroy() sslocalProcess = null @@ -177,18 +181,65 @@ class ShadowsocksVpnService extends VpnService with BaseService { /** Called when the activity is first created. */ def handleConnection: Boolean = { + + val fd = startVpn() + if (!sendFd(fd)) return false + + if (profile.kcp) { + startKcptunDaemon() + } + startShadowsocksDaemon() + + if (profile.udpdns && profile.kcp) { + startShadowsocksUDPDaemon() + } + if (!profile.udpdns) { startDnsDaemon() startDnsTunnel() } - val fd = startVpn() - sendFd(fd) + true + } + + def startKcptunDaemon() { + val cmd = new ArrayBuffer[String] + cmd += (getApplicationInfo.dataDir + "/kcptun" + , "-r", profile.host + ":" + profile.kcpPort + , "-l", "127.0.0.1:" + (profile.localPort + 90) + , "--path", getApplicationInfo.dataDir + "/protect_path" + , profile.kcpcli) + + if (BuildConfig.DEBUG) + Log.d(TAG, cmd.mkString(" ")) + + kcptunProcess = new GuardedProcess(cmd.mkString(" ").split(" ").toSeq).start() + } + + def startShadowsocksUDPDaemon() { + val conf = ConfigUtils + .SHADOWSOCKS.formatLocal(Locale.ENGLISH, profile.host, profile.remotePort, profile.localPort, + profile.password, profile.method, 600) + Utils.printToFile(new File(getApplicationInfo.dataDir + "/ss-local-udp-vpn.conf"))(p => { + p.println(conf) + }) + + val cmd = new ArrayBuffer[String] + cmd += (getApplicationInfo.dataDir + "/ss-local", "-V", "-U" + , "-b", "127.0.0.1" + , "-t", "600" + , "-P", getApplicationInfo.dataDir + , "-c", getApplicationInfo.dataDir + "/ss-local-udp-vpn.conf") + + if (profile.auth) cmd += "-A" + + if (BuildConfig.DEBUG) Log.d(TAG, cmd.mkString(" ")) + + sstunnelProcess = new GuardedProcess(cmd).start() } def startShadowsocksDaemon() { - if (profile.route != Route.ALL) { val acl: Array[Array[String]] = profile.route match { case Route.BYPASS_LAN => Array(getResources.getStringArray(R.array.private_route)) @@ -201,15 +252,21 @@ class ShadowsocksVpnService extends VpnService with BaseService { }) } - val conf = ConfigUtils + val conf = if (profile.kcp) { + ConfigUtils + .SHADOWSOCKS.formatLocal(Locale.ENGLISH, "127.0.0.1", profile.localPort + 90, profile.localPort, + profile.password, profile.method, 600) + } else { + ConfigUtils .SHADOWSOCKS.formatLocal(Locale.ENGLISH, profile.host, profile.remotePort, profile.localPort, profile.password, profile.method, 600) + } Utils.printToFile(new File(getApplicationInfo.dataDir + "/ss-local-vpn.conf"))(p => { p.println(conf) }) val cmd = new ArrayBuffer[String] - cmd += (getApplicationInfo.dataDir + "/ss-local", "-V", "-u" + cmd += (getApplicationInfo.dataDir + "/ss-local", "-V" , "-b", "127.0.0.1" , "-t", "600" , "-P", getApplicationInfo.dataDir @@ -217,6 +274,8 @@ class ShadowsocksVpnService extends VpnService with BaseService { if (profile.auth) cmd += "-A" + if (profile.udpdns && !profile.kcp) cmd += "-u" + if (profile.route != Route.ALL) { cmd += "--acl" cmd += (getApplicationInfo.dataDir + "/acl.list") @@ -228,19 +287,23 @@ class ShadowsocksVpnService extends VpnService with BaseService { } def startDnsTunnel() = { - val conf = ConfigUtils - .SHADOWSOCKS.formatLocal(Locale.ENGLISH, profile.host, profile.remotePort, 8163, + val conf = if (profile.kcp) { + ConfigUtils + .SHADOWSOCKS.formatLocal(Locale.ENGLISH, "127.0.0.1", profile.localPort + 90, profile.localPort + 63, profile.password, profile.method, 10) + } else { + ConfigUtils + .SHADOWSOCKS.formatLocal(Locale.ENGLISH, profile.host, profile.remotePort, profile.localPort + 63, + profile.password, profile.method, 10) + } Utils.printToFile(new File(getApplicationInfo.dataDir + "/ss-tunnel-vpn.conf"))(p => { p.println(conf) }) val cmd = new ArrayBuffer[String] cmd += (getApplicationInfo.dataDir + "/ss-tunnel" , "-V" - , "-u" , "-t", "10" , "-b", "127.0.0.1" - , "-l", "8163" , "-L", "8.8.8.8:53" , "-P", getApplicationInfo.dataDir , "-c", getApplicationInfo.dataDir + "/ss-tunnel-vpn.conf") @@ -257,10 +320,10 @@ class ShadowsocksVpnService extends VpnService with BaseService { val conf = { if (profile.route == Route.BYPASS_CHN || profile.route == Route.BYPASS_LAN_CHN) { ConfigUtils.PDNSD_DIRECT.formatLocal(Locale.ENGLISH, getApplicationInfo.dataDir, - "0.0.0.0", 8153, getBlackList, 8163, ipv6) + "0.0.0.0", profile.localPort + 53, getBlackList, profile.localPort + 63, ipv6) } else { ConfigUtils.PDNSD_LOCAL.formatLocal(Locale.ENGLISH, getApplicationInfo.dataDir, - "0.0.0.0", 8153, 8163, ipv6) + "0.0.0.0", profile.localPort + 53, profile.localPort + 63, ipv6) } } Utils.printToFile(new File(getApplicationInfo.dataDir + "/pdnsd-vpn.conf"))(p => { @@ -354,7 +417,8 @@ class ShadowsocksVpnService extends VpnService with BaseService { if (profile.udpdns) cmd += " --enable-udprelay" else - cmd += " --dnsgw %s:8153".formatLocal(Locale.ENGLISH, PRIVATE_VLAN.formatLocal(Locale.ENGLISH, "1")) + cmd += " --dnsgw %s:%d".formatLocal(Locale.ENGLISH, PRIVATE_VLAN.formatLocal(Locale.ENGLISH, "1"), + profile.localPort + 53) if (BuildConfig.DEBUG) Log.d(TAG, cmd) diff --git a/src/main/scala/com/github/shadowsocks/database/DBHelper.scala b/src/main/scala/com/github/shadowsocks/database/DBHelper.scala index d513fe58..acaefebf 100644 --- a/src/main/scala/com/github/shadowsocks/database/DBHelper.scala +++ b/src/main/scala/com/github/shadowsocks/database/DBHelper.scala @@ -64,7 +64,7 @@ object DBHelper { } class DBHelper(val context: Context) - extends OrmLiteSqliteOpenHelper(context, DBHelper.PROFILE, null, 16) { + extends OrmLiteSqliteOpenHelper(context, DBHelper.PROFILE, null, 18) { import DBHelper._ lazy val profileDao: Dao[Profile, Int] = getDao(classOf[Profile]) @@ -125,6 +125,15 @@ class DBHelper(val context: Context) if (oldVersion < 16) { profileDao.executeRawNoArgs("UPDATE `profile` SET route = 'bypass-lan-china' WHERE route = 'bypass-china'") } + + if (oldVersion < 17) { + profileDao.executeRawNoArgs("ALTER TABLE `profile` ADD COLUMN kcp SMALLINT;") + profileDao.executeRawNoArgs("ALTER TABLE `profile` ADD COLUMN kcpcli VARCHAR;") + } + + if (oldVersion < 18) { + profileDao.executeRawNoArgs("ALTER TABLE `profile` ADD COLUMN kcpPort INTEGER;") + } } } } diff --git a/src/main/scala/com/github/shadowsocks/database/Profile.scala b/src/main/scala/com/github/shadowsocks/database/Profile.scala index 19570fd3..07dc1b55 100644 --- a/src/main/scala/com/github/shadowsocks/database/Profile.scala +++ b/src/main/scala/com/github/shadowsocks/database/Profile.scala @@ -99,6 +99,15 @@ class Profile { @DatabaseField var userOrder: Long = _ + @DatabaseField + var kcp: Boolean = false + + @DatabaseField + var kcpPort: Int = 8399 + + @DatabaseField + var kcpcli: String = "" + override def toString = "ss://" + Base64.encodeToString("%s%s:%s@%s:%d".formatLocal(Locale.ENGLISH, method, if (auth) "-auth" else "", password, host, remotePort).getBytes, Base64.NO_PADDING | Base64.NO_WRAP) diff --git a/src/main/scala/com/github/shadowsocks/utils/Constants.scala b/src/main/scala/com/github/shadowsocks/utils/Constants.scala index ef784116..f3211440 100644 --- a/src/main/scala/com/github/shadowsocks/utils/Constants.scala +++ b/src/main/scala/com/github/shadowsocks/utils/Constants.scala @@ -45,6 +45,7 @@ object Executable { val SS_LOCAL = "ss-local" val SS_TUNNEL = "ss-tunnel" val TUN2SOCKS = "tun2socks" + val KCPTUN = "kcptun" } object ConfigUtils { @@ -166,6 +167,10 @@ object Key { val localPort = "localPortNum" val profileTip = "profileTip" + + val kcp = "kcp" + val kcpPort = "kcpPort" + val kcpcli = "kcpcli" } object State { diff --git a/travis-ci/setup.sh b/travis-ci/setup.sh index 7511641d..078c26f9 100755 --- a/travis-ci/setup.sh +++ b/travis-ci/setup.sh @@ -1,6 +1,7 @@ #!/bin/bash export ARCH=`uname -m` +export GOROOT_BOOTSTRAP=$HOME/.android/go export ANDROID_NDK_HOME=$HOME/.android/android-ndk-r12b export ANDROID_HOME=$HOME/.android/android-sdk-linux export PATH=${ANDROID_NDK_HOME}:${ANDROID_HOME}/tools:${ANDROID_HOME}/platform-tools:${PATH} @@ -21,5 +22,13 @@ if [ ! -d "$ANDROID_NDK_HOME" ]; then popd fi +if [ ! -d "$GOROOT_BOOTSTRAP" ]; then + mkdir -p $GOROOT_BOOTSTRAP + pushd $HOME/.android + wget https://storage.googleapis.com/golang/go1.6.3.linux-amd64.tar.gz + tar xf go1.6.3.linux-amd64.tar.gz + popd +fi + ( sleep 5 && while [ 1 ]; do sleep 1; echo y; done ) | android update sdk --filter tools,platform-tools,build-tools-24.0.0,android-23,android-24,extra-google-m2repository --no-ui -a ( sleep 5 && while [ 1 ]; do sleep 1; echo y; done ) | android update sdk --filter extra-android-m2repository --no-ui -a