diff --git a/src/main/res/values-zh/strings.xml b/src/main/res/values-zh/strings.xml
index c3910a87..b79e5088 100755
--- a/src/main/res/values-zh/strings.xml
+++ b/src/main/res/values-zh/strings.xml
@@ -34,6 +34,8 @@
随系统启动后台服务
后台服务已开始运行。
无法连接远程服务器
+ NAT 模式需要 ROOT 权限
+ 切换到 VPN 模式
停止服务
正在关闭……
后台服务启动失败:%s
@@ -52,14 +54,15 @@
关于
影梭(shadowsocks)%s
刷新 DNS 缓存
- 需要 ROOT 权限
+ 只需在 NAT 模式下使用
+ 只需在 NAT 模式下使用,需要 ROOT 权限
+ 在没有 ROOT 权限的情况下无法刷新 DNS 缓存
刷新中……
二维码
添加配置文件
NAT 模式 (仅限调试)
- 从 VPN 模式切换为 NAT 模式
- NAT 模式已禁用,需要 ROOT 权限
+ 从 VPN 模式切换为 NAT 模式,需要 ROOT 权限
UDP 转发
由远程服务器转发 UDP 协议的数据包
diff --git a/src/main/res/values/strings.xml b/src/main/res/values/strings.xml
index 7c154a9f..d3d7696f 100644
--- a/src/main/res/values/strings.xml
+++ b/src/main/res/values/strings.xml
@@ -10,8 +10,7 @@
Profile
Switch to another profile or add new profiles
NAT mode (deprecated)
- Use NAT mode instead of VPN mode
- NAT mode is disabled without root
+ Use NAT mode instead of VPN mode. Requires ROOT permission.
Network Traffic
Sent: \t\t\t\t\t%3$s\t↑\t%1$s/s\nReceived: \t%4$s\t↓\t%2$s/s
\n%1$s↑\t%2$s↓
@@ -61,6 +60,8 @@
Shadowsocks started.
Failed to connect the remote server
+ NAT mode requires ROOT permission
+ Switch to VPN mode
Stop the service
Shutting down…
%s
@@ -84,7 +85,9 @@
Shadowsocks %s
QR Code
Flush DNS cache
- Requires ROOT permission
+ Only useful in NAT mode
+ Only useful in NAT mode. Requires ROOT permission.
+ Flush DNS cache cannot proceed without ROOT permission
Flushing
Add Profile
Export to Clipboard
diff --git a/src/main/res/xml/pref_all.xml b/src/main/res/xml/pref_all.xml
index 20f23e2a..c5d7a3b5 100644
--- a/src/main/res/xml/pref_all.xml
+++ b/src/main/res/xml/pref_all.xml
@@ -83,12 +83,14 @@
android:summary="@string/auto_connect_summary"
android:title="@string/auto_connect"/>
-
+
-
+
diff --git a/src/main/scala/com/github/shadowsocks/AppManager.scala b/src/main/scala/com/github/shadowsocks/AppManager.scala
index d08e8acb..81c17db3 100644
--- a/src/main/scala/com/github/shadowsocks/AppManager.scala
+++ b/src/main/scala/com/github/shadowsocks/AppManager.scala
@@ -58,8 +58,6 @@ import com.github.shadowsocks.utils.{Key, Utils}
import scala.collection.JavaConversions._
import scala.collection.mutable
-import scala.concurrent.ExecutionContext.Implicits.global
-import scala.concurrent.Future
import scala.language.implicitConversions
object AppManager {
@@ -249,7 +247,7 @@ class AppManager extends AppCompatActivity with OnCheckedChangeListener with OnC
}
def loadAppsAsync() {
- Future {
+ ThrowableFuture {
while (!appsLoading) loadApps()
appsLoading = false
handler.post(() => {
diff --git a/src/main/scala/com/github/shadowsocks/Shadowsocks.scala b/src/main/scala/com/github/shadowsocks/Shadowsocks.scala
index 37895f30..881c999e 100644
--- a/src/main/scala/com/github/shadowsocks/Shadowsocks.scala
+++ b/src/main/scala/com/github/shadowsocks/Shadowsocks.scala
@@ -65,8 +65,6 @@ import com.github.shadowsocks.utils._
import com.google.android.gms.ads.{AdRequest, AdSize, AdView}
import scala.collection.mutable.ArrayBuffer
-import scala.concurrent.ExecutionContext.Implicits.global
-import scala.concurrent.Future
object Typefaces {
def get(c: Context, assetPath: String): Typeface = {
@@ -182,8 +180,13 @@ class Shadowsocks
handler.postDelayed(() => fabProgressCircle.hide(), 1000)
fab.setEnabled(true)
changeSwitch(checked = false)
- if (m != null) Snackbar.make(findViewById(android.R.id.content),
- getString(R.string.vpn_error).formatLocal(Locale.ENGLISH, m), Snackbar.LENGTH_LONG).show
+ if (m != null) {
+ val snackbar = Snackbar.make(findViewById(android.R.id.content),
+ getString(R.string.vpn_error).formatLocal(Locale.ENGLISH, m), Snackbar.LENGTH_LONG)
+ if (m == getString(R.string.nat_no_root)) snackbar.setAction(R.string.switch_to_vpn,
+ (_ => preferences.natSwitch.setChecked(false)): View.OnClickListener)
+ snackbar.show
+ }
setPreferenceEnabled(enabled = true)
case State.STOPPING =>
fab.setBackgroundTintList(greyTint)
@@ -317,10 +320,10 @@ class Shadowsocks
cmd.append("chmod 666 %s%s-vpn.pid".formatLocal(Locale.ENGLISH, Path.BASE, task))
}
- if (!ShadowsocksApplication.isVpnEnabled) {
- Console.runRootCommand(cmd.toArray)
- } else {
+ if (ShadowsocksApplication.isVpnEnabled) {
Console.runCommand(cmd.toArray)
+ } else {
+ Console.runRootCommand(cmd.toArray)
}
cmd.clear()
@@ -341,8 +344,7 @@ class Shadowsocks
cmd.append("rm -f %s%s-vpn.pid".formatLocal(Locale.ENGLISH, Path.BASE, task))
cmd.append("rm -f %s%s-vpn.conf".formatLocal(Locale.ENGLISH, Path.BASE, task))
}
- Console.runCommand(cmd.toArray)
- if (!ShadowsocksApplication.isVpnEnabled) {
+ if (ShadowsocksApplication.isVpnEnabled) Console.runCommand(cmd.toArray) else {
Console.runRootCommand(cmd.toArray)
Console.runRootCommand(Utils.getIptables + " -t nat -F OUTPUT")
}
@@ -361,7 +363,7 @@ class Shadowsocks
}
def prepareStartService() {
- Future {
+ ThrowableFuture {
if (ShadowsocksApplication.isVpnEnabled) {
val intent = VpnService.prepare(this)
if (intent != null) {
@@ -534,18 +536,6 @@ class Shadowsocks
handler.removeCallbacksAndMessages(null)
}
- def copyToSystem() {
- val ab = new ArrayBuffer[String]
- ab.append("mount -o rw,remount -t yaffs2 /dev/block/mtdblock3 /system")
- for (executable <- Shadowsocks.EXECUTABLES) {
- ab.append("cp %s%s /system/bin/".formatLocal(Locale.ENGLISH, Path.BASE, executable))
- ab.append("chmod 755 /system/bin/" + executable)
- ab.append("chown root:shell /system/bin/" + executable)
- }
- ab.append("mount -o ro,remount -t yaffs2 /dev/block/mtdblock3 /system")
- Console.runRootCommand(ab.toArray)
- }
-
def install() {
copyAssets(System.getABI)
@@ -565,7 +555,7 @@ class Shadowsocks
def recovery() {
serviceStop()
val h = showProgress(R.string.recovering)
- Future {
+ ThrowableFuture {
reset()
h.sendEmptyMessage(0)
}
@@ -573,8 +563,9 @@ class Shadowsocks
def flushDnsCache() {
val h = showProgress(R.string.flushing)
- Future {
- Utils.toggleAirplaneMode(getBaseContext)
+ ThrowableFuture {
+ if (!Utils.toggleAirplaneMode(getBaseContext)) h.post(() => Snackbar.make(findViewById(android.R.id.content),
+ R.string.flush_dnscache_no_root, Snackbar.LENGTH_LONG).show)
h.sendEmptyMessage(0)
}
}
@@ -595,7 +586,7 @@ class Shadowsocks
def checkText(key: String): Boolean = {
val text = ShadowsocksApplication.settings.getString(key, "")
if (text != null && text.length > 0) return true
- Snackbar.make(findViewById(android.R.id.content), getString(R.string.proxy_empty), Snackbar.LENGTH_LONG).show
+ Snackbar.make(findViewById(android.R.id.content), R.string.proxy_empty, Snackbar.LENGTH_LONG).show
false
}
diff --git a/src/main/scala/com/github/shadowsocks/ShadowsocksApplication.scala b/src/main/scala/com/github/shadowsocks/ShadowsocksApplication.scala
index 7bd76fe6..07eb2a60 100644
--- a/src/main/scala/com/github/shadowsocks/ShadowsocksApplication.scala
+++ b/src/main/scala/com/github/shadowsocks/ShadowsocksApplication.scala
@@ -46,7 +46,7 @@ import android.app.Application
import android.content.pm.PackageManager
import android.preference.PreferenceManager
import com.github.shadowsocks.database.{DBHelper, ProfileManager}
-import com.github.shadowsocks.utils.{Console, Key, Utils}
+import com.github.shadowsocks.utils.{Key, Utils}
import com.google.android.gms.analytics.{GoogleAnalytics, HitBuilders}
import com.google.android.gms.common.api.ResultCallback
import com.google.android.gms.tagmanager.{ContainerHolder, TagManager}
@@ -60,8 +60,7 @@ object ShadowsocksApplication {
lazy val settings = PreferenceManager.getDefaultSharedPreferences(instance)
lazy val profileManager = new ProfileManager(settings, dbHelper)
- val isRoot = Console.isRoot
- def isVpnEnabled = !(isRoot && settings.getBoolean(Key.isNAT, !Utils.isLollipopOrAbove))
+ def isVpnEnabled = !settings.getBoolean(Key.isNAT, false)
def getVersionName = try {
instance.getPackageManager.getPackageInfo(instance.getPackageName, 0).versionName
diff --git a/src/main/scala/com/github/shadowsocks/ShadowsocksNatService.scala b/src/main/scala/com/github/shadowsocks/ShadowsocksNatService.scala
index 865cc586..26374ffc 100644
--- a/src/main/scala/com/github/shadowsocks/ShadowsocksNatService.scala
+++ b/src/main/scala/com/github/shadowsocks/ShadowsocksNatService.scala
@@ -55,8 +55,6 @@ import com.github.shadowsocks.utils._
import scala.collection.JavaConversions._
import scala.collection.mutable.ArrayBuffer
-import scala.concurrent.ExecutionContext.Implicits.global
-import scala.concurrent.Future
class ShadowsocksNatService extends BaseService {
@@ -144,7 +142,7 @@ class ShadowsocksNatService extends BaseService {
})
Console.runRootCommand(cmdBuf.toArray)
} else {
- Console.runRootCommand(Array("ndc resolver flushdefaultif", "ndc resolver flushif wlan0"))
+ Console.runRootCommand("ndc resolver flushdefaultif", "ndc resolver flushif wlan0")
}
}
@@ -384,12 +382,17 @@ class ShadowsocksNatService extends BaseService {
}
}
}
- Console.runRootCommand(init_sb.toArray)
- Console.runRootCommand(http_sb.toArray)
+ Console.runRootCommand((init_sb ++ http_sb).toArray)
}
override def startRunner(config: Config) {
+ changeState(State.CONNECTING)
+ if (!Console.isRoot) {
+ changeState(State.STOPPED, getString(R.string.nat_no_root))
+ return
+ }
+
super.startRunner(config)
// register close receiver
@@ -404,10 +407,7 @@ class ShadowsocksNatService extends BaseService {
ShadowsocksApplication.track(TAG, "start")
- changeState(State.CONNECTING)
-
- Future {
-
+ ThrowableFuture {
if (config.proxy == "198.199.101.152") {
val holder = ShadowsocksApplication.containerHolder
try {
@@ -465,7 +465,7 @@ class ShadowsocksNatService extends BaseService {
closeReceiver = null
}
- notification.destroy()
+ if (notification != null) notification.destroy()
ShadowsocksApplication.track(TAG, "stop")
diff --git a/src/main/scala/com/github/shadowsocks/ShadowsocksSettings.scala b/src/main/scala/com/github/shadowsocks/ShadowsocksSettings.scala
index b63c7d81..cc67d043 100644
--- a/src/main/scala/com/github/shadowsocks/ShadowsocksSettings.scala
+++ b/src/main/scala/com/github/shadowsocks/ShadowsocksSettings.scala
@@ -14,6 +14,7 @@ import com.github.shadowsocks.utils.Key
// TODO: Move related logic here
class ShadowsocksSettings extends PreferenceFragment with OnSharedPreferenceChangeListener {
private lazy val activity = getActivity.asInstanceOf[Shadowsocks]
+ lazy val natSwitch = findPreference(Key.isNAT).asInstanceOf[SwitchPreference]
private var isProxyApps: SwitchPreference = _
@@ -35,10 +36,8 @@ class ShadowsocksSettings extends PreferenceFragment with OnSharedPreferenceChan
})
val flush = findPreference("flush_dnscache")
- if (Build.VERSION.SDK_INT >= 17 && !ShadowsocksApplication.isRoot) {
- flush.setEnabled(false)
- flush.setSummary(R.string.flush_dnscache_summary_disabled)
- } else flush.setOnPreferenceClickListener((preference: Preference) => {
+ if (Build.VERSION.SDK_INT < 17) flush.setSummary(R.string.flush_dnscache_summary)
+ flush.setOnPreferenceClickListener(_ => {
ShadowsocksApplication.track(Shadowsocks.TAG, "flush_dnscache")
activity.flushDnsCache()
true
@@ -73,12 +72,10 @@ class ShadowsocksSettings extends PreferenceFragment with OnSharedPreferenceChan
}
def onSharedPreferenceChanged(sharedPreferences: SharedPreferences, key: String) = key match {
- case Key.isNAT => if (ShadowsocksApplication.isRoot && activity != null) {
- activity.handler.post(() => {
- activity.deattachService
- activity.attachService
- })
- }
+ case Key.isNAT => activity.handler.post(() => {
+ activity.deattachService
+ activity.attachService
+ })
case _ =>
}
}
diff --git a/src/main/scala/com/github/shadowsocks/ShadowsocksVpnService.scala b/src/main/scala/com/github/shadowsocks/ShadowsocksVpnService.scala
index 3140baaf..ac832422 100644
--- a/src/main/scala/com/github/shadowsocks/ShadowsocksVpnService.scala
+++ b/src/main/scala/com/github/shadowsocks/ShadowsocksVpnService.scala
@@ -55,8 +55,6 @@ import org.apache.commons.net.util.SubnetUtils
import scala.collection.JavaConversions._
import scala.collection.mutable.ArrayBuffer
-import scala.concurrent.ExecutionContext.Implicits.global
-import scala.concurrent.Future
class ShadowsocksVpnService extends VpnService with BaseService {
val TAG = "ShadowsocksVpnService"
@@ -122,7 +120,7 @@ class ShadowsocksVpnService extends VpnService with BaseService {
vpnThread = null
}
- notification.destroy()
+ if (notification != null) notification.destroy()
// channge the state
changeState(State.STOPPING)
@@ -213,7 +211,7 @@ class ShadowsocksVpnService extends VpnService with BaseService {
changeState(State.CONNECTING)
- Future {
+ ThrowableFuture {
if (config.proxy == "198.199.101.152") {
val holder = ShadowsocksApplication.containerHolder
try {
diff --git a/src/main/scala/com/github/shadowsocks/package.scala b/src/main/scala/com/github/shadowsocks/package.scala
new file mode 100644
index 00000000..a6db148d
--- /dev/null
+++ b/src/main/scala/com/github/shadowsocks/package.scala
@@ -0,0 +1,13 @@
+package com.github
+
+import scala.concurrent.ExecutionContext.Implicits.global
+import scala.concurrent.Future
+
+/**
+ * @author Mygod
+ */
+package object shadowsocks {
+ def ThrowableFuture[T](f: => T) = Future(f) onFailure {
+ case e: Throwable => e.printStackTrace()
+ }
+}
diff --git a/src/main/scala/com/github/shadowsocks/preferences/NatSwitchPreference.scala b/src/main/scala/com/github/shadowsocks/preferences/NatSwitchPreference.scala
deleted file mode 100644
index 4471c329..00000000
--- a/src/main/scala/com/github/shadowsocks/preferences/NatSwitchPreference.scala
+++ /dev/null
@@ -1,27 +0,0 @@
-package com.github.shadowsocks.preferences
-
-import android.content.Context
-import android.content.res.TypedArray
-import android.preference.SwitchPreference
-import android.util.AttributeSet
-import com.github.shadowsocks.{R, ShadowsocksApplication}
-
-/**
- * TODO: No animations?
- * @author Mygod
- */
-final class NatSwitchPreference(context: Context, attrs: AttributeSet) extends SwitchPreference(context, attrs) {
- private var disabled = false
-
- override def isEnabled = super.isEnabled && !disabled
- override def setEnabled(b: Boolean) = super.setEnabled(b && !disabled)
-
- if (!ShadowsocksApplication.isRoot) {
- disabled = true
- setEnabled(false)
- setSummary(R.string.nat_summary_no_root)
- }
-
- override def onGetDefaultValue(a: TypedArray, index: Int) =
- (!ShadowsocksApplication.isVpnEnabled).asInstanceOf[AnyRef]
-}
diff --git a/src/main/scala/com/github/shadowsocks/utils/Console.scala b/src/main/scala/com/github/shadowsocks/utils/Console.scala
index 60edd8f7..042dc9ff 100644
--- a/src/main/scala/com/github/shadowsocks/utils/Console.scala
+++ b/src/main/scala/com/github/shadowsocks/utils/Console.scala
@@ -75,17 +75,11 @@ object Console {
shell.close()
}
- def runRootCommand(command: String): String = runRootCommand(Array(command))
- def runRootCommand(command: String, context: String): String = runRootCommand(Array(command), context)
- def runRootCommand(commands: Array[String]): String = runRootCommand(commands, "u:r:init_shell:s0")
-
- def runRootCommand(commands: Array[String], context: String): String = {
- if (!isRoot) {
- return null
- }
- val shell = openRootShell(context)
+ def runRootCommand(commands: String*): String = runRootCommand(commands.toArray)
+ def runRootCommand(commands: Array[String]): String = {
+ val shell = openRootShell("u:r:init_shell:s0")
val sb = new StringBuilder
- shell.addCommand(commands, 0, ((commandCode: Int, exitCode: Int, output: util.List[String]) => {
+ shell.addCommand(commands, 0, ((_, exitCode, output) => {
if (exitCode < 0) {
shell.close()
} else {
diff --git a/src/main/scala/com/github/shadowsocks/utils/Utils.scala b/src/main/scala/com/github/shadowsocks/utils/Utils.scala
index f0f7301d..3754efaa 100644
--- a/src/main/scala/com/github/shadowsocks/utils/Utils.scala
+++ b/src/main/scala/com/github/shadowsocks/utils/Utils.scala
@@ -140,12 +140,14 @@ object Utils {
}
// Blocked > 3 seconds
- def toggleAirplaneMode(context: Context) {
- if (ShadowsocksApplication.isRoot) {
- Console.runRootCommand(Array("ndc resolver flushdefaultif", "ndc resolver flushif wlan0"))
+ def toggleAirplaneMode(context: Context) = {
+ if (Console.isRoot) {
+ Console.runRootCommand("ndc resolver flushdefaultif", "ndc resolver flushif wlan0")
+ true
} else if (Build.VERSION.SDK_INT < 17) {
toggleBelowApiLevel17(context)
- }
+ true
+ } else false
}
//noinspection ScalaDeprecation
@@ -305,8 +307,7 @@ object Utils {
var compatible: Boolean = false
var version: Boolean = false
- val command = Array(iptables + " --version", iptables + " -L -t nat -n")
- val lines = Console.runRootCommand(command)
+ val lines = Console.runRootCommand(iptables + " --version", iptables + " -L -t nat -n")
if (lines == null) return
if (lines.contains("OUTPUT")) {
@@ -384,9 +385,7 @@ object Utils {
hasRedirectSupport = 1
val command = Utils.getIptables + " -t nat -A OUTPUT -p udp --dport 54 -j REDIRECT --to 8154"
- val lines = Console.runRootCommand(command)
-
- Console.runRootCommand(command.replace("-A", "-D"))
+ val lines = Console.runRootCommand(command, command.replace("-A", "-D"))
if (lines == null) return
if (lines.contains("No chain/target/match")) {
hasRedirectSupport = 0