Преглед изворни кода

Impl shadowsocks-rust and plugin

世界 пре 4 година
родитељ
комит
057d1b83ee
37 измењених фајлова са 1546 додато и 46 уклоњено
  1. 1 1
      .gitignore
  2. 3 0
      .gitmodules
  3. 1 0
      .idea/gradle.xml
  4. 5 0
      .idea/jarRepositories.xml
  5. 0 2
      app/build.gradle
  6. 68 0
      app/src/main/java/com/github/shadowsocks/plugin/AlertDialogFragment.kt
  7. 69 0
      app/src/main/java/com/github/shadowsocks/plugin/ConfigurationActivity.kt
  8. 46 0
      app/src/main/java/com/github/shadowsocks/plugin/HelpActivity.kt
  9. 37 0
      app/src/main/java/com/github/shadowsocks/plugin/HelpCallback.kt
  10. 31 0
      app/src/main/java/com/github/shadowsocks/plugin/NativePlugin.kt
  11. 102 0
      app/src/main/java/com/github/shadowsocks/plugin/NativePluginProvider.kt
  12. 10 0
      app/src/main/java/com/github/shadowsocks/plugin/NoPlugin.kt
  13. 50 0
      app/src/main/java/com/github/shadowsocks/plugin/OptionsCapableActivity.kt
  14. 54 0
      app/src/main/java/com/github/shadowsocks/plugin/PathProvider.kt
  15. 41 0
      app/src/main/java/com/github/shadowsocks/plugin/Plugin.kt
  16. 63 0
      app/src/main/java/com/github/shadowsocks/plugin/PluginConfiguration.kt
  17. 149 0
      app/src/main/java/com/github/shadowsocks/plugin/PluginContract.kt
  18. 50 0
      app/src/main/java/com/github/shadowsocks/plugin/PluginList.kt
  19. 242 0
      app/src/main/java/com/github/shadowsocks/plugin/PluginManager.kt
  20. 109 0
      app/src/main/java/com/github/shadowsocks/plugin/PluginOptions.kt
  21. 57 0
      app/src/main/java/com/github/shadowsocks/plugin/ResolvedPlugin.kt
  22. 37 0
      app/src/main/java/com/github/shadowsocks/plugin/Utils.kt
  23. 78 0
      app/src/main/java/com/github/shadowsocks/plugin/fragment/AlertDialogFragment.kt
  24. 33 0
      app/src/main/java/com/github/shadowsocks/plugin/fragment/Utils.kt
  25. 5 4
      app/src/main/java/io/nekohasekai/sagernet/bg/BaseService.kt
  26. 66 7
      app/src/main/java/io/nekohasekai/sagernet/bg/ProxyInstance.kt
  27. 1 2
      app/src/main/java/io/nekohasekai/sagernet/bg/VpnService.kt
  28. 1 1
      app/src/main/java/io/nekohasekai/sagernet/database/DataStore.kt
  29. 10 0
      app/src/main/java/io/nekohasekai/sagernet/database/ProxyEntity.kt
  30. 36 22
      app/src/main/java/io/nekohasekai/sagernet/fmt/v2ray/VMessFmt.kt
  31. 9 0
      bin/libs/shadowsocks.sh
  32. 6 6
      build.gradle
  33. 3 1
      settings.gradle
  34. 50 0
      shadowsocks/build.gradle.kts
  35. 4 0
      shadowsocks/src/main/AndroidManifest.xml
  36. 18 0
      shadowsocks/src/main/rust/linker-wrapper.py
  37. 1 0
      shadowsocks/src/main/rust/shadowsocks-rust

+ 1 - 1
.gitignore

@@ -8,7 +8,7 @@
 /.idea/navEditor.xml
 /.idea/assetWizardSettings.xml
 .DS_Store
-/build
+build/
 /captures
 .externalNativeBuild
 .cxx

+ 3 - 0
.gitmodules

@@ -7,3 +7,6 @@
 [submodule "v2ray"]
 	path = v2ray
 	url = https://github.com/nekohasekai/AndroidLibV2rayLite
+[submodule "shadowsocks/src/main/rust/shadowsocks-rust"]
+	path = shadowsocks/src/main/rust/shadowsocks-rust
+	url = https://github.com/shadowsocks/shadowsocks-rust.git

+ 1 - 0
.idea/gradle.xml

@@ -14,6 +14,7 @@
           <set>
             <option value="$PROJECT_DIR$" />
             <option value="$PROJECT_DIR$/app" />
+            <option value="$PROJECT_DIR$/shadowsocks" />
           </set>
         </option>
         <option name="resolveModulePerSourceSet" value="false" />

+ 5 - 0
.idea/jarRepositories.xml

@@ -26,5 +26,10 @@
       <option name="name" value="maven" />
       <option name="url" value="https://jitpack.io" />
     </remote-repository>
+    <remote-repository>
+      <option name="id" value="MavenRepo" />
+      <option name="name" value="MavenRepo" />
+      <option name="url" value="https://repo.maven.apache.org/maven2/" />
+    </remote-repository>
   </component>
 </project>

+ 0 - 2
app/build.gradle

@@ -3,10 +3,8 @@ plugins {
     id "kotlin-android"
     id "kotlin-kapt"
     id "kotlin-parcelize"
-
 }
 
-
 def keystorePwd = null
 def alias = null
 def pwd = null

+ 68 - 0
app/src/main/java/com/github/shadowsocks/plugin/AlertDialogFragment.kt

@@ -0,0 +1,68 @@
+/*******************************************************************************
+ *                                                                             *
+ *  Copyright (C) 2019 by Max Lv <[email protected]>                          *
+ *  Copyright (C) 2019 by Mygod Studio <[email protected]>  *
+ *                                                                             *
+ *  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.plugin
+
+import android.app.Activity
+import android.content.DialogInterface
+import android.content.Intent
+import android.os.Bundle
+import android.os.Parcelable
+import androidx.appcompat.app.AlertDialog
+import androidx.appcompat.app.AppCompatDialogFragment
+import androidx.fragment.app.Fragment
+
+/**
+ * Based on: https://android.googlesource.com/platform/packages/apps/ExactCalculator/+/8c43f06/src/com/android/calculator2/AlertDialogFragment.java
+ */
+@Suppress("DEPRECATION")
+@Deprecated("Related APIs are deprecated in AndroidX", ReplaceWith("fragment.AlertDialogFragment"))
+abstract class AlertDialogFragment<Arg : Parcelable, Ret : Parcelable> :
+        AppCompatDialogFragment(), DialogInterface.OnClickListener {
+    companion object {
+        private const val KEY_ARG = "arg"
+        private const val KEY_RET = "ret"
+        fun <T : Parcelable> getRet(data: Intent) = data.extras!!.getParcelable<T>(KEY_RET)!!
+    }
+    protected abstract fun AlertDialog.Builder.prepare(listener: DialogInterface.OnClickListener)
+
+    protected val arg by lazy { requireArguments().getParcelable<Arg>(KEY_ARG)!! }
+    protected open fun ret(which: Int): Ret? = null
+    fun withArg(arg: Arg) = apply { arguments = Bundle().apply { putParcelable(KEY_ARG, arg) } }
+
+    override fun onCreateDialog(savedInstanceState: Bundle?): AlertDialog =
+            AlertDialog.Builder(requireContext()).also { it.prepare(this) }.create()
+
+    override fun onClick(dialog: DialogInterface?, which: Int) {
+        targetFragment?.onActivityResult(targetRequestCode, which, ret(which)?.let {
+            Intent().replaceExtras(Bundle().apply { putParcelable(KEY_RET, it) })
+        })
+    }
+
+    override fun onDismiss(dialog: DialogInterface) {
+        super.onDismiss(dialog)
+        onClick(dialog, Activity.RESULT_CANCELED)
+    }
+
+    fun show(target: Fragment, requestCode: Int = 0, tag: String = javaClass.simpleName) {
+        setTargetFragment(target, requestCode)
+        showAllowingStateLoss(target.fragmentManager ?: return, tag)
+    }
+}

+ 69 - 0
app/src/main/java/com/github/shadowsocks/plugin/ConfigurationActivity.kt

@@ -0,0 +1,69 @@
+/*******************************************************************************
+ *                                                                             *
+ *  Copyright (C) 2017 by Max Lv <[email protected]>                          *
+ *  Copyright (C) 2017 by Mygod Studio <[email protected]>  *
+ *                                                                             *
+ *  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.plugin
+
+import android.app.Activity
+import android.content.Intent
+
+/**
+ * Base class for configuration activity. A configuration activity is started when user wishes to configure the
+ * selected plugin. To create a configuration activity, extend this class, implement abstract methods, invoke
+ * `saveChanges(options)` and `discardChanges()` when appropriate, and add it to your manifest like this:
+ *
+ * <pre class="prettyprint">&lt;manifest&gt;
+ *    ...
+ *    &lt;application&gt;
+ *        ...
+ *        &lt;activity android:name=".ConfigureActivity"&gt;
+ *            &lt;intent-filter&gt;
+ *                &lt;action android:name="com.github.shadowsocks.plugin.ACTION_CONFIGURE"/&gt;
+ *                &lt;category android:name="android.intent.category.DEFAULT"/&gt;
+ *                &lt;data android:scheme="plugin"
+ *                         android:host="com.github.shadowsocks"
+ *                         android:path="/$PLUGIN_ID"/&gt;
+ *            &lt;/intent-filter&gt;
+ *        &lt;/activity&gt;
+ *        ...
+ *    &lt;/application&gt;
+ *&lt;/manifest&gt;</pre>
+ */
+abstract class ConfigurationActivity : OptionsCapableActivity() {
+    /**
+     * Equivalent to setResult(RESULT_CANCELED).
+     */
+    fun discardChanges() = setResult(Activity.RESULT_CANCELED)
+
+    /**
+     * Equivalent to setResult(RESULT_OK, args_with_correct_format).
+     *
+     * @param options PluginOptions to save.
+     */
+    fun saveChanges(options: PluginOptions) =
+            setResult(Activity.RESULT_OK, Intent().putExtra(PluginContract.EXTRA_OPTIONS, options.toString()))
+
+    /**
+     * Finish this activity and request manual editor to pop up instead.
+     */
+    fun fallbackToManualEditor() {
+        setResult(PluginContract.RESULT_FALLBACK)
+        finish()
+    }
+}

+ 46 - 0
app/src/main/java/com/github/shadowsocks/plugin/HelpActivity.kt

@@ -0,0 +1,46 @@
+/*******************************************************************************
+ *                                                                             *
+ *  Copyright (C) 2017 by Max Lv <[email protected]>                          *
+ *  Copyright (C) 2017 by Mygod Studio <[email protected]>  *
+ *                                                                             *
+ *  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.plugin
+
+/**
+ * Base class for a help activity. A help activity is started when user taps help when configuring options for your
+ * plugin. To create a help activity, just extend this class, and add it to your manifest like this:
+ *
+ * <pre class="prettyprint">&lt;manifest&gt;
+ *    ...
+ *    &lt;application&gt;
+ *        ...
+ *        &lt;activity android:name=".HelpActivity"&gt;
+ *            &lt;intent-filter&gt;
+ *                &lt;action android:name="com.github.shadowsocks.plugin.ACTION_HELP"/&gt;
+ *                &lt;category android:name="android.intent.category.DEFAULT"/&gt;
+ *                &lt;data android:scheme="plugin"
+ *                         android:host="com.github.shadowsocks"
+ *                         android:path="/$PLUGIN_ID"/&gt;
+ *            &lt;/intent-filter&gt;
+ *        &lt;/activity&gt;
+ *        ...
+ *    &lt;/application&gt;
+ *&lt;/manifest&gt;</pre>
+ */
+abstract class HelpActivity : OptionsCapableActivity() {
+    override fun onInitializePluginOptions(options: PluginOptions) { }
+}

+ 37 - 0
app/src/main/java/com/github/shadowsocks/plugin/HelpCallback.kt

@@ -0,0 +1,37 @@
+/*******************************************************************************
+ *                                                                             *
+ *  Copyright (C) 2017 by Max Lv <[email protected]>                          *
+ *  Copyright (C) 2017 by Mygod Studio <[email protected]>  *
+ *                                                                             *
+ *  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.plugin
+
+import android.content.Intent
+
+/**
+ * HelpCallback is an HelpActivity but you just need to produce a CharSequence help message instead of having to
+ * provide UI. To create a help callback, just extend this class, implement abstract methods, and add it to your
+ * manifest following the same procedure as adding a HelpActivity.
+ */
+abstract class HelpCallback : HelpActivity() {
+    abstract fun produceHelpMessage(options: PluginOptions): CharSequence
+
+    override fun onInitializePluginOptions(options: PluginOptions) {
+        setResult(RESULT_OK, Intent().putExtra(PluginContract.EXTRA_HELP_MESSAGE, produceHelpMessage(options)))
+        finish()
+    }
+}

+ 31 - 0
app/src/main/java/com/github/shadowsocks/plugin/NativePlugin.kt

@@ -0,0 +1,31 @@
+/*******************************************************************************
+ *                                                                             *
+ *  Copyright (C) 2017 by Max Lv <[email protected]>                          *
+ *  Copyright (C) 2017 by Mygod Studio <[email protected]>  *
+ *                                                                             *
+ *  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.plugin
+
+import android.content.pm.ResolveInfo
+
+class NativePlugin(resolveInfo: ResolveInfo) : ResolvedPlugin(resolveInfo) {
+    init {
+        check(resolveInfo.providerInfo != null)
+    }
+
+    override val componentInfo get() = resolveInfo.providerInfo!!
+}

+ 102 - 0
app/src/main/java/com/github/shadowsocks/plugin/NativePluginProvider.kt

@@ -0,0 +1,102 @@
+/*******************************************************************************
+ *                                                                             *
+ *  Copyright (C) 2017 by Max Lv <[email protected]>                          *
+ *  Copyright (C) 2017 by Mygod Studio <[email protected]>  *
+ *                                                                             *
+ *  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.plugin
+
+import android.content.ContentProvider
+import android.content.ContentValues
+import android.database.Cursor
+import android.database.MatrixCursor
+import android.net.Uri
+import android.os.Bundle
+import android.os.ParcelFileDescriptor
+import androidx.core.os.bundleOf
+
+/**
+ * Base class for a native plugin provider. A native plugin provider offers read-only access to files that are required
+ * to run a plugin, such as binary files and other configuration files. To create a native plugin provider, extend this
+ * class, implement the abstract methods, and add it to your manifest like this:
+ *
+ * <pre class="prettyprint">&lt;manifest&gt;
+ *    ...
+ *    &lt;application&gt;
+ *        ...
+ *        &lt;provider android:name="com.github.shadowsocks.$PLUGIN_ID.BinaryProvider"
+ *                     android:authorities="com.github.shadowsocks.plugin.$PLUGIN_ID.BinaryProvider"&gt;
+ *            &lt;intent-filter&gt;
+ *                &lt;category android:name="com.github.shadowsocks.plugin.ACTION_NATIVE_PLUGIN" /&gt;
+ *            &lt;/intent-filter&gt;
+ *        &lt;/provider&gt;
+ *        ...
+ *    &lt;/application&gt;
+ *&lt;/manifest&gt;</pre>
+ */
+abstract class NativePluginProvider : ContentProvider() {
+    override fun getType(uri: Uri): String? = "application/x-elf"
+
+    override fun onCreate(): Boolean = true
+
+    /**
+     * Provide all files needed for native plugin.
+     *
+     * @param provider A helper object to use to add files.
+     */
+    protected abstract fun populateFiles(provider: PathProvider)
+
+    override fun query(uri: Uri, projection: Array<out String>?, selection: String?, selectionArgs: Array<out String>?,
+                       sortOrder: String?): Cursor? {
+        check(selection == null && selectionArgs == null && sortOrder == null)
+        val result = MatrixCursor(projection)
+        populateFiles(PathProvider(uri, result))
+        return result
+    }
+
+    /**
+     * Returns executable entry absolute path.
+     * This is used for fast mode initialization where ss-local launches your native binary at the path given directly.
+     * In order for this to work, plugin app is encouraged to have the following in its AndroidManifest.xml:
+     *  - android:installLocation="internalOnly" for <manifest>
+     *  - android:extractNativeLibs="true" for <application>
+     *
+     * Default behavior is throwing UnsupportedOperationException. If you don't wish to use this feature, use the
+     * default behavior.
+     *
+     * @return Absolute path for executable entry.
+     */
+    open fun getExecutable(): String = throw UnsupportedOperationException()
+
+    abstract fun openFile(uri: Uri): ParcelFileDescriptor
+    override fun openFile(uri: Uri, mode: String): ParcelFileDescriptor {
+        check(mode == "r")
+        return openFile(uri)
+    }
+
+    override fun call(method: String, arg: String?, extras: Bundle?): Bundle? = when (method) {
+        PluginContract.METHOD_GET_EXECUTABLE -> bundleOf(Pair(PluginContract.EXTRA_ENTRY, getExecutable()))
+        else -> super.call(method, arg, extras)
+    }
+
+    // Methods that should not be used
+    override fun insert(uri: Uri, values: ContentValues?): Uri? = throw UnsupportedOperationException()
+    override fun update(uri: Uri, values: ContentValues?, selection: String?, selectionArgs: Array<out String>?): Int =
+            throw UnsupportedOperationException()
+    override fun delete(uri: Uri, selection: String?, selectionArgs: Array<out String>?): Int =
+            throw UnsupportedOperationException()
+}

+ 10 - 0
app/src/main/java/com/github/shadowsocks/plugin/NoPlugin.kt

@@ -0,0 +1,10 @@
+package com.github.shadowsocks.plugin
+
+import io.nekohasekai.sagernet.R
+import io.nekohasekai.sagernet.SagerNet
+
+
+object NoPlugin : Plugin() {
+    override val id: String get() = ""
+    override val label: CharSequence get() = SagerNet.application.getText(R.string.plugin_disabled)
+}

+ 50 - 0
app/src/main/java/com/github/shadowsocks/plugin/OptionsCapableActivity.kt

@@ -0,0 +1,50 @@
+/*******************************************************************************
+ *                                                                             *
+ *  Copyright (C) 2017 by Max Lv <[email protected]>                          *
+ *  Copyright (C) 2017 by Mygod Studio <[email protected]>  *
+ *                                                                             *
+ *  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.plugin
+
+import android.content.Intent
+import android.os.Bundle
+import android.widget.Toast
+import androidx.appcompat.app.AppCompatActivity
+
+/**
+ * Activity that's capable of getting EXTRA_OPTIONS input.
+ */
+abstract class OptionsCapableActivity : AppCompatActivity() {
+    protected fun pluginOptions(intent: Intent = this.intent) = try {
+        PluginOptions("", intent.getStringExtra(PluginContract.EXTRA_OPTIONS))
+    } catch (exc: IllegalArgumentException) {
+        Toast.makeText(this, exc.message, Toast.LENGTH_SHORT).show()
+        PluginOptions()
+    }
+
+    /**
+     * Populate args to your user interface.
+     *
+     * @param options PluginOptions parsed.
+     */
+    protected abstract fun onInitializePluginOptions(options: PluginOptions = pluginOptions())
+
+    override fun onPostCreate(savedInstanceState: Bundle?) {
+        super.onPostCreate(savedInstanceState)
+        if (savedInstanceState == null) onInitializePluginOptions()
+    }
+}

+ 54 - 0
app/src/main/java/com/github/shadowsocks/plugin/PathProvider.kt

@@ -0,0 +1,54 @@
+/*******************************************************************************
+ *                                                                             *
+ *  Copyright (C) 2017 by Max Lv <[email protected]>                          *
+ *  Copyright (C) 2017 by Mygod Studio <[email protected]>  *
+ *                                                                             *
+ *  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.plugin
+
+import android.database.MatrixCursor
+import android.net.Uri
+import java.io.File
+
+/**
+ * Helper class to provide relative paths of files to copy.
+ */
+class PathProvider internal constructor(baseUri: Uri, private val cursor: MatrixCursor) {
+    private val basePath = baseUri.path?.trim('/') ?: ""
+
+    fun addPath(path: String, mode: Int = 0b110100100): PathProvider {
+        val trimmed = path.trim('/')
+        if (trimmed.startsWith(basePath)) cursor.newRow()
+                .add(PluginContract.COLUMN_PATH, trimmed)
+                .add(PluginContract.COLUMN_MODE, mode)
+        return this
+    }
+    fun addTo(file: File, to: String = "", mode: Int = 0b110100100): PathProvider {
+        var sub = to + file.name
+        if (basePath.startsWith(sub)) if (file.isDirectory) {
+            sub += '/'
+            file.listFiles()!!.forEach { addTo(it, sub, mode) }
+        } else addPath(sub, mode)
+        return this
+    }
+    fun addAt(file: File, at: String = "", mode: Int = 0b110100100): PathProvider {
+        if (basePath.startsWith(at)) {
+            if (file.isDirectory) file.listFiles()!!.forEach { addTo(it, at, mode) } else addPath(at, mode)
+        }
+        return this
+    }
+}

+ 41 - 0
app/src/main/java/com/github/shadowsocks/plugin/Plugin.kt

@@ -0,0 +1,41 @@
+/*******************************************************************************
+ *                                                                             *
+ *  Copyright (C) 2017 by Max Lv <[email protected]>                          *
+ *  Copyright (C) 2017 by Mygod Studio <[email protected]>  *
+ *                                                                             *
+ *  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.plugin
+
+import android.graphics.drawable.Drawable
+
+abstract class Plugin {
+    abstract val id: String
+    open val idAliases get() = emptyArray<String>()
+    abstract val label: CharSequence
+    open val icon: Drawable? get() = null
+    open val defaultConfig: String? get() = null
+    open val packageName: String get() = ""
+    open val trusted: Boolean get() = true
+    open val directBootAware: Boolean get() = true
+
+    override fun equals(other: Any?): Boolean {
+        if (this === other) return true
+        if (javaClass != other?.javaClass) return false
+        return id == (other as Plugin).id
+    }
+    override fun hashCode() = id.hashCode()
+}

+ 63 - 0
app/src/main/java/com/github/shadowsocks/plugin/PluginConfiguration.kt

@@ -0,0 +1,63 @@
+/*******************************************************************************
+ *                                                                             *
+ *  Copyright (C) 2017 by Max Lv <[email protected]>                          *
+ *  Copyright (C) 2017 by Mygod Studio <[email protected]>  *
+ *                                                                             *
+ *  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.plugin
+
+import io.nekohasekai.sagernet.ktx.Logs
+import io.nekohasekai.sagernet.utils.Commandline
+import java.util.*
+
+class PluginConfiguration(val pluginsOptions: Map<String, PluginOptions>, val selected: String) {
+    private constructor(plugins: List<PluginOptions>) : this(
+            plugins.filter { it.id.isNotEmpty() }.associateBy { it.id },
+            if (plugins.isEmpty()) "" else plugins[0].id)
+    constructor(plugin: String) : this(plugin.split('\n').map { line ->
+        if (line.startsWith("kcptun ")) {
+            val opt = PluginOptions()
+            opt.id = "kcptun"
+            try {
+                val iterator = Commandline.translateCommandline(line).drop(1).iterator()
+                while (iterator.hasNext()) {
+                    val option = iterator.next()
+                    when {
+                        option == "--nocomp" -> opt["nocomp"] = null
+                        option.startsWith("--") -> opt[option.substring(2)] = iterator.next()
+                        else -> throw IllegalArgumentException("Unknown kcptun parameter: $option")
+                    }
+                }
+            } catch (exc: Exception) {
+                Logs.w(exc)
+            }
+            opt
+        } else PluginOptions(line)
+    })
+
+    fun getOptions(
+            id: String = selected,
+            defaultConfig: () -> String? = { PluginManager.fetchPlugins().lookup[id]?.defaultConfig }
+    ) = if (id.isEmpty()) PluginOptions() else pluginsOptions[id] ?: PluginOptions(id, defaultConfig())
+
+    override fun toString(): String {
+        val result = LinkedList<PluginOptions>()
+        for ((id, opt) in pluginsOptions) if (id == this.selected) result.addFirst(opt) else result.addLast(opt)
+        if (!pluginsOptions.contains(selected)) result.addFirst(getOptions())
+        return result.joinToString("\n") { it.toString(false) }
+    }
+}

+ 149 - 0
app/src/main/java/com/github/shadowsocks/plugin/PluginContract.kt

@@ -0,0 +1,149 @@
+/*******************************************************************************
+ *                                                                             *
+ *  Copyright (C) 2017 by Max Lv <[email protected]>                          *
+ *  Copyright (C) 2017 by Mygod Studio <[email protected]>  *
+ *                                                                             *
+ *  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.plugin
+
+/**
+ * The contract between the plugin provider and host. Contains definitions for the supported actions, extras, etc.
+ *
+ * This class is written in Java to keep Java interoperability.
+ */
+object PluginContract {
+    /**
+     * ContentProvider Action: Used for NativePluginProvider.
+     *
+     * Constant Value: "com.github.shadowsocks.plugin.ACTION_NATIVE_PLUGIN"
+     */
+    const val ACTION_NATIVE_PLUGIN = "com.github.shadowsocks.plugin.ACTION_NATIVE_PLUGIN"
+
+    /**
+     * Activity Action: Used for ConfigurationActivity.
+     *
+     * Constant Value: "com.github.shadowsocks.plugin.ACTION_CONFIGURE"
+     */
+    const val ACTION_CONFIGURE = "com.github.shadowsocks.plugin.ACTION_CONFIGURE"
+    /**
+     * Activity Action: Used for HelpActivity or HelpCallback.
+     *
+     * Constant Value: "com.github.shadowsocks.plugin.ACTION_HELP"
+     */
+    const val ACTION_HELP = "com.github.shadowsocks.plugin.ACTION_HELP"
+
+    /**
+     * The lookup key for a string that provides the plugin entry binary.
+     *
+     * Example: "/data/data/com.github.shadowsocks.plugin.obfs_local/lib/libobfs-local.so"
+     *
+     * Constant Value: "com.github.shadowsocks.plugin.EXTRA_ENTRY"
+     */
+    const val EXTRA_ENTRY = "com.github.shadowsocks.plugin.EXTRA_ENTRY"
+    /**
+     * The lookup key for a string that provides the options as a string.
+     *
+     * Example: "obfs=http;obfs-host=www.baidu.com"
+     *
+     * Constant Value: "com.github.shadowsocks.plugin.EXTRA_OPTIONS"
+     */
+    const val EXTRA_OPTIONS = "com.github.shadowsocks.plugin.EXTRA_OPTIONS"
+    /**
+     * The lookup key for a CharSequence that provides user relevant help message.
+     *
+     * Example: "obfs=<http></http>|tls>            Enable obfuscating: HTTP or TLS (Experimental).
+     * obfs-host=<host_name>      Hostname for obfuscating (Experimental)."
+     *
+     * Constant Value: "com.github.shadowsocks.plugin.EXTRA_HELP_MESSAGE"
+    </host_name> */
+    const val EXTRA_HELP_MESSAGE = "com.github.shadowsocks.plugin.EXTRA_HELP_MESSAGE"
+
+    /**
+     * The metadata key to retrieve plugin version. Required for plugin applications.
+     *
+     * Constant Value: "com.github.shadowsocks.plugin.version"
+     */
+    const val METADATA_KEY_VERSION = "com.github.shadowsocks.plugin.version"
+
+    /**
+     * The metadata key to retrieve plugin id. Required for plugins.
+     *
+     * Constant Value: "com.github.shadowsocks.plugin.id"
+     */
+    const val METADATA_KEY_ID = "com.github.shadowsocks.plugin.id"
+    /**
+     * The metadata key to retrieve plugin id aliases.
+     * Can be a string (representing one alias) or a resource to a string or string array.
+     *
+     * Constant Value: "com.github.shadowsocks.plugin.id.aliases"
+     */
+    const val METADATA_KEY_ID_ALIASES = "com.github.shadowsocks.plugin.id.aliases"
+    /**
+     * The metadata key to retrieve default configuration. Default value is empty.
+     *
+     * Constant Value: "com.github.shadowsocks.plugin.default_config"
+     */
+    const val METADATA_KEY_DEFAULT_CONFIG = "com.github.shadowsocks.plugin.default_config"
+    /**
+     * The metadata key to retrieve executable path to your native binary.
+     * This path should be relative to your application's nativeLibraryDir.
+     *
+     * If this is set, the host app will prefer this value and (probably) not launch your app at all (aka faster mode).
+     * In order for this to work, plugin app is encouraged to have the following in its AndroidManifest.xml:
+     *  - android:installLocation="internalOnly" for <manifest>
+     *  - android:extractNativeLibs="true" for <application>
+     *
+     * Do not use this if you plan to do some setup work before giving away your binary path,
+     *  or your native binary is not at a fixed location relative to your application's nativeLibraryDir.
+     *
+     * Since plugin lib: 1.3.0
+     *
+     * Constant Value: "com.github.shadowsocks.plugin.executable_path"
+     */
+    const val METADATA_KEY_EXECUTABLE_PATH = "com.github.shadowsocks.plugin.executable_path"
+
+    const val METHOD_GET_EXECUTABLE = "shadowsocks:getExecutable"
+
+    /** ConfigurationActivity result: fallback to manual edit mode.  */
+    const val RESULT_FALLBACK = 1
+
+    /**
+     * Relative to the file to be copied. This column is required.
+     *
+     * Example: "kcptun", "doc/help.txt"
+     *
+     * Type: String
+     */
+    const val COLUMN_PATH = "path"
+    /**
+     * File mode bits. Default value is 644 in octal.
+     *
+     * Example: 0b110100100 (for 755 in octal)
+     *
+     * Type: Int or String (deprecated)
+     */
+    const val COLUMN_MODE = "mode"
+
+    /**
+     * The scheme for general plugin actions.
+     */
+    const val SCHEME = "plugin"
+    /**
+     * The authority for general plugin actions.
+     */
+    const val AUTHORITY = "com.github.shadowsocks"
+}

+ 50 - 0
app/src/main/java/com/github/shadowsocks/plugin/PluginList.kt

@@ -0,0 +1,50 @@
+/*******************************************************************************
+ *                                                                             *
+ *  Copyright (C) 2020 by Max Lv <[email protected]>                          *
+ *  Copyright (C) 2020 by Mygod Studio <[email protected]>  *
+ *                                                                             *
+ *  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.plugin
+
+import android.content.Intent
+import android.content.pm.PackageManager
+import android.widget.Toast
+import io.nekohasekai.sagernet.SagerNet
+
+class PluginList : ArrayList<Plugin>() {
+    init {
+        add(NoPlugin)
+        addAll(SagerNet.application.packageManager.queryIntentContentProviders(
+                Intent(PluginContract.ACTION_NATIVE_PLUGIN), PackageManager.GET_META_DATA)
+                .filter { it.providerInfo.exported }.map { NativePlugin(it) })
+    }
+
+    val lookup = mutableMapOf<String, Plugin>().apply {
+        for (plugin in this@PluginList) {
+            fun check(old: Plugin?) {
+                if (old != null && old !== plugin) {
+                    val packages = [email protected] { it.id == plugin.id }.joinToString { it.packageName }
+                    val message = "Conflicting plugins found from: $packages"
+                    Toast.makeText(SagerNet.application, message, Toast.LENGTH_LONG).show()
+                    throw IllegalStateException(message)
+                }
+            }
+            check(put(plugin.id, plugin))
+            for (alias in plugin.idAliases) check(put(alias, plugin))
+        }
+    }
+}

+ 242 - 0
app/src/main/java/com/github/shadowsocks/plugin/PluginManager.kt

@@ -0,0 +1,242 @@
+/*******************************************************************************
+ *                                                                             *
+ *  Copyright (C) 2017 by Max Lv <[email protected]>                          *
+ *  Copyright (C) 2017 by Mygod Studio <[email protected]>  *
+ *                                                                             *
+ *  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.plugin
+
+import android.annotation.SuppressLint
+import android.content.BroadcastReceiver
+import android.content.ContentResolver
+import android.content.Intent
+import android.content.pm.ComponentInfo
+import android.content.pm.PackageManager
+import android.content.pm.ProviderInfo
+import android.content.pm.Signature
+import android.database.Cursor
+import android.net.Uri
+import android.os.Build
+import android.system.Os
+import android.util.Base64
+import android.widget.Toast
+import androidx.core.os.bundleOf
+import io.nekohasekai.sagernet.R
+import io.nekohasekai.sagernet.SagerNet
+import io.nekohasekai.sagernet.bg.BaseService
+import io.nekohasekai.sagernet.ktx.Logs
+import io.nekohasekai.sagernet.ktx.listenForPackageChanges
+import io.nekohasekai.sagernet.ktx.signaturesCompat
+import java.io.File
+import java.io.FileNotFoundException
+
+object PluginManager {
+    class PluginNotFoundException(private val plugin: String) : FileNotFoundException(plugin),
+            BaseService.ExpectedException {
+        override fun getLocalizedMessage() = SagerNet.application.getString(R.string.plugin_unknown, plugin)
+    }
+
+    /**
+     * Trusted signatures by the app. Third-party fork should add their public key to their fork if the developer wishes
+     * to publish or has published plugins for this app. You can obtain your public key by executing:
+     *
+     * $ keytool -export -alias key-alias -keystore /path/to/keystore.jks -rfc
+     *
+     * If you don't plan to publish any plugin but is developing/has developed some, it's not necessary to add your
+     * public key yet since it will also automatically trust packages signed by the same signatures, e.g. debug keys.
+     */
+    val trustedSignatures by lazy {
+        SagerNet.packageInfo.signaturesCompat.toSet() +
+                Signature(Base64.decode(  // @Mygod
+                """
+                    |MIIDWzCCAkOgAwIBAgIEUzfv8DANBgkqhkiG9w0BAQsFADBdMQswCQYDVQQGEwJD
+                    |TjEOMAwGA1UECBMFTXlnb2QxDjAMBgNVBAcTBU15Z29kMQ4wDAYDVQQKEwVNeWdv
+                    |ZDEOMAwGA1UECxMFTXlnb2QxDjAMBgNVBAMTBU15Z29kMCAXDTE0MDUwMjA5MjQx
+                    |OVoYDzMwMTMwOTAyMDkyNDE5WjBdMQswCQYDVQQGEwJDTjEOMAwGA1UECBMFTXln
+                    |b2QxDjAMBgNVBAcTBU15Z29kMQ4wDAYDVQQKEwVNeWdvZDEOMAwGA1UECxMFTXln
+                    |b2QxDjAMBgNVBAMTBU15Z29kMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKC
+                    |AQEAjm5ikHoP3w6zavvZU5bRo6Birz41JL/nZidpdww21q/G9APA+IiJMUeeocy0
+                    |L7/QY8MQZABVwNq79LXYWJBcmmFXM9xBPgDqQP4uh9JsvazCI9bvDiMn92mz9HiS
+                    |Sg9V4KGg0AcY0r230KIFo7hz+2QBp1gwAAE97myBfA3pi3IzJM2kWsh4LWkKQMfL
+                    |M6KDhpb4mdDQnHlgi4JWe3SYbLtpB6whnTqjHaOzvyiLspx1tmrb0KVxssry9KoX
+                    |YQzl56scfE/QJX0jJ5qYmNAYRCb4PibMuNSGB2NObDabSOMAdT4JLueOcHZ/x9tw
+                    |agGQ9UdymVZYzf8uqc+29ppKdQIDAQABoyEwHzAdBgNVHQ4EFgQUBK4uJ0cqmnho
+                    |6I72VmOVQMvVCXowDQYJKoZIhvcNAQELBQADggEBABZQ3yNESQdgNJg+NRIcpF9l
+                    |YSKZvrBZ51gyrC7/2ZKMpRIyXruUOIrjuTR5eaONs1E4HI/uA3xG1eeW2pjPxDnO
+                    |zgM4t7EPH6QbzibihoHw1MAB/mzECzY8r11PBhDQlst0a2hp+zUNR8CLbpmPPqTY
+                    |RSo6EooQ7+NBejOXysqIF1q0BJs8Y5s/CaTOmgbL7uPCkzArB6SS/hzXgDk5gw6v
+                    |wkGeOtzcj1DlbUTvt1s5GlnwBTGUmkbLx+YUje+n+IBgMbohLUDYBtUHylRVgMsc
+                    |1WS67kDqeJiiQZvrxvyW6CZZ/MIGI+uAkkj3DqJpaZirkwPgvpcOIrjZy0uFvQM=
+                  """, Base64.DEFAULT)) +
+                Signature(Base64.decode( // @madeye
+                """
+                    |MIICQzCCAaygAwIBAgIETV9OhjANBgkqhkiG9w0BAQUFADBmMQswCQYDVQQGEwJjbjERMA8GA1UE
+                    |CBMIU2hhbmdoYWkxDzANBgNVBAcTBlB1ZG9uZzEUMBIGA1UEChMLRnVkYW4gVW5pdi4xDDAKBgNV
+                    |BAsTA1BQSTEPMA0GA1UEAxMGTWF4IEx2MB4XDTExMDIxOTA1MDA1NFoXDTM2MDIxMzA1MDA1NFow
+                    |ZjELMAkGA1UEBhMCY24xETAPBgNVBAgTCFNoYW5naGFpMQ8wDQYDVQQHEwZQdWRvbmcxFDASBgNV
+                    |BAoTC0Z1ZGFuIFVuaXYuMQwwCgYDVQQLEwNQUEkxDzANBgNVBAMTBk1heCBMdjCBnzANBgkqhkiG
+                    |9w0BAQEFAAOBjQAwgYkCgYEAq6lA8LqdeEI+es9SDX85aIcx8LoL3cc//iRRi+2mFIWvzvZ+bLKr
+                    |4Wd0rhu/iU7OeMm2GvySFyw/GdMh1bqh5nNPLiRxAlZxpaZxLOdRcxuvh5Nc5yzjM+QBv8ECmuvu
+                    |AOvvT3UDmA0AMQjZqSCmxWIxc/cClZ/0DubreBo2st0CAwEAATANBgkqhkiG9w0BAQUFAAOBgQAQ
+                    |Iqonxpwk2ay+Dm5RhFfZyG9SatM/JNFx2OdErU16WzuK1ItotXGVJaxCZv3u/tTwM5aaMACGED5n
+                    |AvHaDGCWynY74oDAopM4liF/yLe1wmZDu6Zo/7fXrH+T03LBgj2fcIkUfN1AA4dvnBo8XWAm9VrI
+                    |1iNuLIssdhDz3IL9Yg==
+                  """, Base64.DEFAULT))
+    }
+
+    private var receiver: BroadcastReceiver? = null
+    private var cachedPlugins: PluginList? = null
+    fun fetchPlugins() = synchronized(this) {
+        if (receiver == null) receiver = SagerNet.application.listenForPackageChanges {
+            synchronized(this) {
+                receiver = null
+                cachedPlugins = null
+            }
+        }
+        if (cachedPlugins == null) cachedPlugins = PluginList()
+        cachedPlugins!!
+    }
+
+    private fun buildUri(id: String) = Uri.Builder()
+            .scheme(PluginContract.SCHEME)
+            .authority(PluginContract.AUTHORITY)
+            .path("/$id")
+            .build()
+    fun buildIntent(id: String, action: String): Intent = Intent(action, buildUri(id))
+
+    data class InitResult(
+            val path: String,
+            val options: PluginOptions,
+            val isV2: Boolean = false,
+    )
+
+    // the following parts are meant to be used by :bg
+    @Throws(Throwable::class)
+    fun init(configuration: PluginConfiguration): InitResult? {
+        if (configuration.selected.isEmpty()) return null
+        var throwable: Throwable? = null
+
+        try {
+            val result = initNative(configuration)
+            if (result != null) return result
+        } catch (t: Throwable) {
+            if (throwable == null) throwable = t else Logs.w(t)
+        }
+
+        // add other plugin types here
+
+        throw throwable ?: PluginNotFoundException(configuration.selected)
+    }
+
+    private fun initNative(configuration: PluginConfiguration): InitResult? {
+        var flags = PackageManager.GET_META_DATA
+        if (Build.VERSION.SDK_INT >= 24) {
+            flags = flags or PackageManager.MATCH_DIRECT_BOOT_UNAWARE or PackageManager.MATCH_DIRECT_BOOT_AWARE
+        }
+        val providers = SagerNet.application.packageManager.queryIntentContentProviders(
+                Intent(PluginContract.ACTION_NATIVE_PLUGIN, buildUri(configuration.selected)), flags)
+                .filter { it.providerInfo.exported }
+        if (providers.isEmpty()) return null
+        if (providers.size > 1) {
+            val message = "Conflicting plugins found from: ${providers.joinToString { it.providerInfo.packageName }}"
+            Toast.makeText(SagerNet.application, message, Toast.LENGTH_LONG).show()
+            throw IllegalStateException(message)
+        }
+        val provider = providers.single().providerInfo
+        val options = configuration.getOptions { provider.loadString(PluginContract.METADATA_KEY_DEFAULT_CONFIG) }
+        val isV2 = provider.applicationInfo.metaData?.getString(PluginContract.METADATA_KEY_VERSION)
+                ?.substringBefore('.')?.toIntOrNull() ?: 0 >= 2
+        var failure: Throwable? = null
+        try {
+            initNativeFaster(provider)?.also { return InitResult(it, options, isV2) }
+        } catch (t: Throwable) {
+            Logs.w("Initializing native plugin faster mode failed")
+            failure = t
+        }
+
+        val uri = Uri.Builder().apply {
+            scheme(ContentResolver.SCHEME_CONTENT)
+            authority(provider.authority)
+        }.build()
+        try {
+            return initNativeFast(SagerNet.application.contentResolver, options, uri)?.let { InitResult(it, options, isV2) }
+        } catch (t: Throwable) {
+            Logs.w("Initializing native plugin fast mode failed")
+            failure?.also { t.addSuppressed(it) }
+            failure = t
+        }
+
+        try {
+            return initNativeSlow(SagerNet.application.contentResolver, options, uri)?.let { InitResult(it, options, isV2) }
+        } catch (t: Throwable) {
+            failure?.also { t.addSuppressed(it) }
+            throw t
+        }
+    }
+
+    private fun initNativeFaster(provider: ProviderInfo): String? {
+        return provider.loadString(PluginContract.METADATA_KEY_EXECUTABLE_PATH)?.let { relativePath ->
+            File(provider.applicationInfo.nativeLibraryDir).resolve(relativePath).apply {
+                check(canExecute())
+            }.absolutePath
+        }
+    }
+
+    private fun initNativeFast(cr: ContentResolver, options: PluginOptions, uri: Uri): String? {
+        return cr.call(uri, PluginContract.METHOD_GET_EXECUTABLE, null,
+                bundleOf(PluginContract.EXTRA_OPTIONS to options.id))?.getString(PluginContract.EXTRA_ENTRY)?.also {
+            check(File(it).canExecute())
+        }
+    }
+
+    @SuppressLint("Recycle")
+    private fun initNativeSlow(cr: ContentResolver, options: PluginOptions, uri: Uri): String? {
+        var initialized = false
+        fun entryNotFound(): Nothing = throw IndexOutOfBoundsException("Plugin entry binary not found")
+        val pluginDir = File(SagerNet.deviceStorage.noBackupFilesDir, "plugin")
+        (cr.query(uri, arrayOf(PluginContract.COLUMN_PATH, PluginContract.COLUMN_MODE), null, null, null)
+                ?: return null).use { cursor ->
+            if (!cursor.moveToFirst()) entryNotFound()
+            pluginDir.deleteRecursively()
+            if (!pluginDir.mkdirs()) throw FileNotFoundException("Unable to create plugin directory")
+            val pluginDirPath = pluginDir.absolutePath + '/'
+            do {
+                val path = cursor.getString(0)
+                val file = File(pluginDir, path)
+                check(file.absolutePath.startsWith(pluginDirPath))
+                cr.openInputStream(uri.buildUpon().path(path).build())!!.use { inStream ->
+                    file.outputStream().use { outStream -> inStream.copyTo(outStream) }
+                }
+                Os.chmod(file.absolutePath, when (cursor.getType(1)) {
+                    Cursor.FIELD_TYPE_INTEGER -> cursor.getInt(1)
+                    Cursor.FIELD_TYPE_STRING -> cursor.getString(1).toInt(8)
+                    else -> throw IllegalArgumentException("File mode should be of type int")
+                })
+                if (path == options.id) initialized = true
+            } while (cursor.moveToNext())
+        }
+        if (!initialized) entryNotFound()
+        return File(pluginDir, options.id).absolutePath
+    }
+
+    fun ComponentInfo.loadString(key: String) = when (val value = metaData.get(key)) {
+        is String -> value
+        is Int -> SagerNet.application.packageManager.getResourcesForApplication(applicationInfo).getString(value)
+        null -> null
+        else -> error("meta-data $key has invalid type ${value.javaClass}")
+    }
+}

+ 109 - 0
app/src/main/java/com/github/shadowsocks/plugin/PluginOptions.kt

@@ -0,0 +1,109 @@
+/*******************************************************************************
+ *                                                                             *
+ *  Copyright (C) 2017 by Max Lv <[email protected]>                          *
+ *  Copyright (C) 2017 by Mygod Studio <[email protected]>  *
+ *                                                                             *
+ *  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.plugin
+
+import java.util.*
+
+/**
+ * Helper class for processing plugin options.
+ *
+ * Based on: https://github.com/apache/ant/blob/588ce1f/src/main/org/apache/tools/ant/types/Commandline.java
+ */
+class PluginOptions : HashMap<String, String?> {
+    var id = ""
+
+    constructor() : super()
+    constructor(initialCapacity: Int) : super(initialCapacity)
+    constructor(initialCapacity: Int, loadFactor: Float) : super(initialCapacity, loadFactor)
+
+    private constructor(options: String?, parseId: Boolean) : this() {
+        @Suppress("NAME_SHADOWING")
+        var parseId = parseId
+        if (options.isNullOrEmpty()) return
+        check(options.all { !it.isISOControl() }) { "No control characters allowed." }
+        val tokenizer = StringTokenizer("$options;", "\\=;", true)
+        val current = StringBuilder()
+        var key: String? = null
+        while (tokenizer.hasMoreTokens()) when (val nextToken = tokenizer.nextToken()) {
+            "\\" -> current.append(tokenizer.nextToken())
+            "=" -> if (key == null) {
+                key = current.toString()
+                current.setLength(0)
+            } else current.append(nextToken)
+            ";" -> {
+                if (key != null) {
+                    put(key, current.toString())
+                    key = null
+                } else if (current.isNotEmpty()) {
+                    if (parseId) id = current.toString() else put(current.toString(), null)
+                }
+                current.setLength(0)
+                parseId = false
+            }
+            else -> current.append(nextToken)
+        }
+    }
+
+    constructor(options: String?) : this(options, true)
+    constructor(id: String, options: String?) : this(options, false) {
+        this.id = id
+    }
+
+    /**
+     * Put but if value is null or default, the entry is deleted.
+     *
+     * @return Old value before put.
+     */
+    fun putWithDefault(key: String, value: String?, default: String? = null) =
+            if (value == null || value == default) remove(key) else put(key, value)
+
+    private fun append(result: StringBuilder, str: String) = str.indices.map { str[it] }.forEach {
+        when (it) {
+            '\\', '=', ';' -> {
+                result.append('\\') // intentionally no break
+                result.append(it)
+            }
+            else -> result.append(it)
+        }
+    }
+
+    fun toString(trimId: Boolean): String {
+        val result = StringBuilder()
+        if (!trimId) if (id.isEmpty()) return "" else append(result, id)
+        for ((key, value) in entries) {
+            if (result.isNotEmpty()) result.append(';')
+            append(result, key)
+            if (value != null) {
+                result.append('=')
+                append(result, value)
+            }
+        }
+        return result.toString()
+    }
+
+    override fun toString(): String = toString(true)
+
+    override fun equals(other: Any?): Boolean {
+        if (this === other) return true
+        return javaClass == other?.javaClass && super.equals(other) && id == (other as PluginOptions).id
+    }
+    override fun hashCode(): Int = Objects.hash(super.hashCode(), id)
+}

+ 57 - 0
app/src/main/java/com/github/shadowsocks/plugin/ResolvedPlugin.kt

@@ -0,0 +1,57 @@
+/*******************************************************************************
+ *                                                                             *
+ *  Copyright (C) 2017 by Max Lv <[email protected]>                          *
+ *  Copyright (C) 2017 by Mygod Studio <[email protected]>  *
+ *                                                                             *
+ *  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.plugin
+
+import android.content.pm.ComponentInfo
+import android.content.pm.ResolveInfo
+import android.graphics.drawable.Drawable
+import android.os.Build
+import com.github.shadowsocks.plugin.PluginManager.loadString
+import io.nekohasekai.sagernet.SagerNet
+import io.nekohasekai.sagernet.ktx.signaturesCompat
+
+abstract class ResolvedPlugin(protected val resolveInfo: ResolveInfo) : Plugin() {
+    protected abstract val componentInfo: ComponentInfo
+
+    override val id by lazy { componentInfo.loadString(PluginContract.METADATA_KEY_ID)!! }
+    override val idAliases: Array<String> by lazy {
+        when (val value = componentInfo.metaData.get(PluginContract.METADATA_KEY_ID_ALIASES)) {
+            is String -> arrayOf(value)
+            is Int -> SagerNet.application.packageManager.getResourcesForApplication(componentInfo.applicationInfo)
+                .run {
+                    when (getResourceTypeName(value)) {
+                        "string" -> arrayOf(getString(value))
+                        else -> getStringArray(value)
+                    }
+                }
+            null -> emptyArray()
+            else -> error("unknown type for plugin meta-data idAliases")
+        }
+    }
+    override val label: CharSequence get() = resolveInfo.loadLabel(SagerNet.application.packageManager)
+    override val icon: Drawable get() = resolveInfo.loadIcon(SagerNet.application.packageManager)
+    override val defaultConfig by lazy { componentInfo.loadString(PluginContract.METADATA_KEY_DEFAULT_CONFIG) }
+    override val packageName: String get() = componentInfo.packageName
+    override val trusted by lazy {
+        SagerNet.application.getPackageInfo(packageName).signaturesCompat.any(PluginManager.trustedSignatures::contains)
+    }
+    override val directBootAware get() = Build.VERSION.SDK_INT < 24 || componentInfo.directBootAware
+}

+ 37 - 0
app/src/main/java/com/github/shadowsocks/plugin/Utils.kt

@@ -0,0 +1,37 @@
+/*******************************************************************************
+ *                                                                             *
+ *  Copyright (C) 2020 by Max Lv <[email protected]>                          *
+ *  Copyright (C) 2020 by Mygod Studio <[email protected]>  *
+ *                                                                             *
+ *  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/>.       *
+ *                                                                             *
+ *******************************************************************************/
+
+@file:JvmName("Utils")
+
+package com.github.shadowsocks.plugin
+
+import android.os.Parcelable
+import androidx.fragment.app.DialogFragment
+import androidx.fragment.app.FragmentManager
+import kotlinx.parcelize.Parcelize
+
+@Parcelize
+class Empty : Parcelable
+
+@JvmOverloads
+@Deprecated("Moved to fragment package", ReplaceWith("fragment.showAllowingStateLoss"))
+fun DialogFragment.showAllowingStateLoss(fragmentManager: FragmentManager, tag: String? = null) {
+    if (!fragmentManager.isStateSaved) show(fragmentManager, tag)
+}

+ 78 - 0
app/src/main/java/com/github/shadowsocks/plugin/fragment/AlertDialogFragment.kt

@@ -0,0 +1,78 @@
+/*******************************************************************************
+ *                                                                             *
+ *  Copyright (C) 2020 by Max Lv <[email protected]>                          *
+ *  Copyright (C) 2020 by Mygod Studio <[email protected]>  *
+ *                                                                             *
+ *  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.plugin.fragment
+
+import android.app.Activity
+import android.content.DialogInterface
+import android.os.Bundle
+import android.os.Parcelable
+import androidx.appcompat.app.AlertDialog
+import androidx.appcompat.app.AppCompatDialogFragment
+import androidx.fragment.app.Fragment
+import androidx.fragment.app.setFragmentResult
+import androidx.fragment.app.setFragmentResultListener
+
+/**
+ * Based on: https://android.googlesource.com/platform/packages/apps/ExactCalculator/+/8c43f06/src/com/android/calculator2/AlertDialogFragment.java
+ */
+abstract class AlertDialogFragment<Arg : Parcelable, Ret : Parcelable?> :
+    AppCompatDialogFragment(), DialogInterface.OnClickListener {
+    companion object {
+        private const val KEY_RESULT = "result"
+        private const val KEY_ARG = "arg"
+        private const val KEY_RET = "ret"
+        private const val KEY_WHICH = "which"
+
+        fun <Ret : Parcelable> setResultListener(fragment: Fragment, requestKey: String,
+                                                 listener: (Int, Ret?) -> Unit) {
+            fragment.setFragmentResultListener(requestKey) { _, bundle ->
+                listener(bundle.getInt(KEY_WHICH, Activity.RESULT_CANCELED), bundle.getParcelable(KEY_RET))
+            }
+        }
+        inline fun <reified T : AlertDialogFragment<*, Ret>, Ret : Parcelable?> setResultListener(
+            fragment: Fragment, noinline listener: (Int, Ret?) -> Unit) =
+            setResultListener(fragment, T::class.java.name, listener)
+    }
+    protected abstract fun AlertDialog.Builder.prepare(listener: DialogInterface.OnClickListener)
+
+    private val resultKey get() = requireArguments().getString(KEY_RESULT)
+    protected val arg by lazy { requireArguments().getParcelable<Arg>(KEY_ARG)!! }
+    protected open fun ret(which: Int): Ret? = null
+
+    private fun args() = arguments ?: Bundle().also { arguments = it }
+    fun arg(arg: Arg) = args().putParcelable(KEY_ARG, arg)
+    fun key(resultKey: String = javaClass.name) = args().putString(KEY_RESULT, resultKey)
+
+    override fun onCreateDialog(savedInstanceState: Bundle?): AlertDialog =
+        AlertDialog.Builder(requireContext()).also { it.prepare(this) }.create()
+
+    override fun onClick(dialog: DialogInterface?, which: Int) {
+        setFragmentResult(resultKey ?: return, Bundle().apply {
+            putInt(KEY_WHICH, which)
+            putParcelable(KEY_RET, ret(which) ?: return@apply)
+        })
+    }
+
+    override fun onDismiss(dialog: DialogInterface) {
+        super.onDismiss(dialog)
+        onClick(null, Activity.RESULT_CANCELED)
+    }
+}

+ 33 - 0
app/src/main/java/com/github/shadowsocks/plugin/fragment/Utils.kt

@@ -0,0 +1,33 @@
+/*******************************************************************************
+ *                                                                             *
+ *  Copyright (C) 2020 by Max Lv <[email protected]>                          *
+ *  Copyright (C) 2020 by Mygod Studio <[email protected]>  *
+ *                                                                             *
+ *  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/>.       *
+ *                                                                             *
+ *******************************************************************************/
+
+@file:JvmName("Utils")
+
+package com.github.shadowsocks.plugin.fragment
+
+import androidx.fragment.app.DialogFragment
+import androidx.fragment.app.FragmentManager
+
+typealias Empty = com.github.shadowsocks.plugin.Empty
+
+@JvmOverloads
+fun DialogFragment.showAllowingStateLoss(fragmentManager: FragmentManager, tag: String? = null) {
+    if (!fragmentManager.isStateSaved) show(fragmentManager, tag)
+}

+ 5 - 4
app/src/main/java/io/nekohasekai/sagernet/bg/BaseService.kt

@@ -8,11 +8,11 @@ import android.os.Build
 import android.os.IBinder
 import android.os.RemoteCallbackList
 import android.os.RemoteException
+import io.nekohasekai.sagernet.Action
+import io.nekohasekai.sagernet.R
 import io.nekohasekai.sagernet.aidl.IShadowsocksService
 import io.nekohasekai.sagernet.aidl.IShadowsocksServiceCallback
 import io.nekohasekai.sagernet.aidl.TrafficStats
-import io.nekohasekai.sagernet.Action
-import io.nekohasekai.sagernet.R
 import io.nekohasekai.sagernet.database.DataStore
 import io.nekohasekai.sagernet.database.SagerDatabase
 import io.nekohasekai.sagernet.ktx.Logs
@@ -201,7 +201,7 @@ class BaseService {
         val isVpnService get() = false
 
         suspend fun startProcesses() {
-            GlobalScope.launch(Dispatchers.IO) { data.proxy!!.start() }
+            data.proxy!!.start()
         }
 
         fun startRunner() {
@@ -220,7 +220,7 @@ class BaseService {
 
         fun stopRunner(restart: Boolean = false, msg: String? = null) {
             if (data.state == State.Stopping) return
-            // channge the state
+            // channge the stated
             data.changeState(State.Stopping)
             GlobalScope.launch(Dispatchers.Main.immediate) {
                 data.connectingJob?.cancelAndJoin() // ensure stop connecting first
@@ -237,6 +237,7 @@ class BaseService {
 
                     data.notification?.destroy()
                     data.notification = null
+                    data.proxy?.shutdown(this)
                     data.binder.trafficPersisted(listOfNotNull(data.proxy).map { it.profile.id })
                     data.proxy = null
                 }

+ 66 - 7
app/src/main/java/io/nekohasekai/sagernet/bg/ProxyInstance.kt

@@ -1,5 +1,11 @@
 package io.nekohasekai.sagernet.bg
 
+import android.os.Build
+import android.os.SystemClock
+import cn.hutool.json.JSONObject
+import com.github.shadowsocks.plugin.PluginConfiguration
+import com.github.shadowsocks.plugin.PluginManager
+import io.nekohasekai.sagernet.SagerNet
 import io.nekohasekai.sagernet.database.DataStore
 import io.nekohasekai.sagernet.database.ProxyEntity
 import io.nekohasekai.sagernet.database.SagerDatabase
@@ -7,32 +13,81 @@ import io.nekohasekai.sagernet.fmt.gson.gson
 import io.nekohasekai.sagernet.fmt.v2ray.V2rayConfig
 import io.nekohasekai.sagernet.fmt.v2ray.buildV2rayConfig
 import io.nekohasekai.sagernet.ktx.Logs
+import kotlinx.coroutines.CoroutineScope
 import libv2ray.Libv2ray
 import libv2ray.V2RayPoint
 import libv2ray.V2RayVPNServiceSupportsSet
+import java.io.File
 import java.io.IOException
+import java.util.*
 
 class ProxyInstance(val profile: ProxyEntity) {
 
     lateinit var v2rayPoint: V2RayPoint
     lateinit var config: V2rayConfig
-    lateinit var service: VpnService
+    lateinit var base: BaseService.Interface
+
+    val bind get() = if (DataStore.allowAccess) "0.0.0.0" else "127.0.0.1"
 
     fun init(service: BaseService.Interface) {
+        base = service
         v2rayPoint = Libv2ray.newV2RayPoint(SagerSupportClass(if (service is VpnService)
             service else null), false)
         v2rayPoint.domainName =
             profile.requireBean().serverAddress + ":" + profile.requireBean().serverPort
-        config = buildV2rayConfig(profile.requireBean(),
-            if (DataStore.allowAccess) "0.0.0.0" else "127.0.0.1",
-            DataStore.socks5Port
-        )
+        config = buildV2rayConfig(profile, bind, DataStore.socks5Port)
         v2rayPoint.configureFileContent = gson.toJson(config).also {
             Logs.d(it)
         }
     }
 
+    var cacheFiles = LinkedList<File>()
+
     fun start() {
+        if (profile.useExternalShadowsocks()) {
+            val bean = profile.requireSS()
+            val port = DataStore.socks5Port + 10
+
+            val proxyConfig = JSONObject().also {
+                it["server"] = bean.serverAddress
+                it["server_port"] = bean.serverPort
+                it["method"] = bean.method
+                it["password"] = bean.password
+                it["local_address"] = "127.0.0.1"
+                it["local_port"] = port
+                it["local_udp_address"] = "127.0.0.1"
+                it["local_udp_port"] = port
+                it["mode"] = "tcp_and_udp"
+            }
+
+            if (bean.plugin.isNotBlank()) {
+                val pluginConfiguration = PluginConfiguration(bean.plugin ?: "")
+                PluginManager.init(pluginConfiguration)?.let { (path, opts, isV2) ->
+                    proxyConfig["plugin"] = path
+                    proxyConfig["plugin_args"] = opts.toString()
+                }
+            }
+
+            Logs.d(proxyConfig.toStringPretty())
+
+            val context =
+                if (Build.VERSION.SDK_INT < 24 || SagerNet.user.isUserUnlocked)
+                    SagerNet.application else SagerNet.deviceStorage
+            val configFile =
+                File(context.noBackupFilesDir,
+                    "shadowsocks_" + SystemClock.elapsedRealtime() + ".json")
+            configFile.writeText(proxyConfig.toString())
+            cacheFiles.add(configFile)
+
+            val commands = mutableListOf(
+                File(SagerNet.application.applicationInfo.nativeLibraryDir,
+                    Executable.SS_LOCAL).absolutePath,
+                "-c", configFile.absolutePath
+            )
+
+            base.data.processes!!.start(commands)
+        }
+
         v2rayPoint.runLoop(DataStore.preferIpv6)
     }
 
@@ -71,16 +126,20 @@ class ProxyInstance(val profile: ProxyEntity) {
 
     fun persistStats() {
         try {
+            uplink
+            downlink
             profile.tx += uplinkTotal
             profile.rx += downlinkTotal
-            uplinkTotal = 0L
-            downlinkTotal = 0L
             SagerDatabase.proxyDao.updateProxy(profile)
         } catch (e: IOException) {
             /*  if (!DataStore.directBootAware) throw e*/ // we should only reach here because we're in direct boot
         }
     }
 
+    fun shutdown(coroutineScope: CoroutineScope) {
+        cacheFiles.removeAll { it.delete(); true }
+    }
+
     private class SagerSupportClass(val service: VpnService?) : V2RayVPNServiceSupportsSet {
 
         override fun onEmitStatus(p0: Long, status: String): Long {

+ 1 - 2
app/src/main/java/io/nekohasekai/sagernet/bg/VpnService.kt

@@ -286,7 +286,7 @@ class VpnService : BaseVpnService(), BaseService.Interface {
          }
     */
         builder.addDisallowedApplication("com.github.shadowsocks")
-//        builder.addDisallowedApplication(packageName)
+        builder.addDisallowedApplication(packageName)
 
         metered = when (profile.meteredNetwork) {
             0 -> DataStore.meteredNetwork
@@ -350,5 +350,4 @@ class VpnService : BaseVpnService(), BaseService.Interface {
         }
     }
 
-
 }

+ 1 - 1
app/src/main/java/io/nekohasekai/sagernet/database/DataStore.kt

@@ -37,7 +37,7 @@ object DataStore {
     var meteredNetwork by configurationStore.boolean("metered_network")
     var proxyApps by configurationStore.int("proxyApps")
     var individual by configurationStore.string("individual")
-    var alwaysUseLibev by configurationStore.boolean("alwaysUseLibev")
+    var forceShadowsocksRust = true//by configurationStore.boolean("forceShadowsocksRust")
 
     // cache
     var dirty by profileCacheStore.boolean(Key.PROFILE_DIRTY)

+ 10 - 0
app/src/main/java/io/nekohasekai/sagernet/database/ProxyEntity.kt

@@ -5,6 +5,7 @@ import android.content.Intent
 import androidx.room.*
 import io.nekohasekai.sagernet.fmt.AbstractBean
 import io.nekohasekai.sagernet.fmt.shadowsocks.ShadowsocksBean
+import io.nekohasekai.sagernet.fmt.shadowsocks.methodsV2fly
 import io.nekohasekai.sagernet.fmt.socks.SOCKSBean
 import io.nekohasekai.sagernet.fmt.v2ray.VMessBean
 import io.nekohasekai.sagernet.ui.settings.ProfileSettingsActivity
@@ -53,6 +54,15 @@ class ProxyEntity(
         }
     }
 
+    fun useExternalShadowsocks(): Boolean {
+        if (type != "ss") return false
+        val bean = requireSS()
+        if (bean.plugin.isNotBlank()) return true
+        if (bean.method !in methodsV2fly) return true
+        if (DataStore.forceShadowsocksRust) return true
+        return false
+    }
+
     fun putBean(bean: AbstractBean) {
         when (bean) {
             is SOCKSBean -> {

+ 36 - 22
app/src/main/java/io/nekohasekai/sagernet/fmt/v2ray/VMessFmt.kt

@@ -2,14 +2,16 @@ package io.nekohasekai.sagernet.fmt.v2ray
 
 import cn.hutool.core.codec.Base64
 import cn.hutool.json.JSONObject
-import io.nekohasekai.sagernet.fmt.AbstractBean
+import io.nekohasekai.sagernet.database.ProxyEntity
 import io.nekohasekai.sagernet.fmt.shadowsocks.ShadowsocksBean
 import io.nekohasekai.sagernet.fmt.socks.SOCKSBean
 import io.nekohasekai.sagernet.fmt.v2ray.V2rayConfig.*
 import okhttp3.HttpUrl
 import okhttp3.HttpUrl.Companion.toHttpUrl
 
-fun buildV2rayConfig(bean: AbstractBean, listen: String, port: Int): V2rayConfig {
+fun buildV2rayConfig(proxy: ProxyEntity, listen: String, port: Int): V2rayConfig {
+
+    val bean = proxy.requireBean()
 
     return V2rayConfig().apply {
 
@@ -64,31 +66,43 @@ fun buildV2rayConfig(bean: AbstractBean, listen: String, port: Int): V2rayConfig
                                 SocksOutboundConfigurationObject.ServerObject().apply {
                                     address = bean.serverAddress
                                     this.port = bean.serverPort
-                                    users = if (bean.username.isNullOrBlank()) {
-                                        emptyList()
-                                    } else {
-                                        listOf(SocksOutboundConfigurationObject.ServerObject.UserObject()
-                                            .apply {
-                                                user = bean.username
-                                                pass = bean.password
-                                            })
+                                    if (!bean.username.isNullOrBlank()) {
+                                        users =
+                                            listOf(SocksOutboundConfigurationObject.ServerObject.UserObject()
+                                                .apply {
+                                                    user = bean.username
+                                                    pass = bean.password
+                                                })
                                     }
                                 }
                             )
                         })
                 } else if (bean is ShadowsocksBean) {
-                    protocol = "shadowsocks"
-                    settings = LazyOutboundConfigurationObject(
-                        ShadowsocksOutboundConfigurationObject().apply {
-                            servers = listOf(
-                                ShadowsocksOutboundConfigurationObject.ServerObject().apply {
-                                    address = bean.serverAddress
-                                    this.port = bean.serverPort
-                                    method = bean.method
-                                    password = bean.password
-                                }
-                            )
-                        })
+                    if (!proxy.useExternalShadowsocks()) {
+                        protocol = "shadowsocks"
+                        settings = LazyOutboundConfigurationObject(
+                            ShadowsocksOutboundConfigurationObject().apply {
+                                servers = listOf(
+                                    ShadowsocksOutboundConfigurationObject.ServerObject().apply {
+                                        address = bean.serverAddress
+                                        this.port = bean.serverPort
+                                        method = bean.method
+                                        password = bean.password
+                                    }
+                                )
+                            })
+                    } else {
+                        protocol = "socks"
+                        settings = LazyOutboundConfigurationObject(
+                            SocksOutboundConfigurationObject().apply {
+                                servers = listOf(
+                                    SocksOutboundConfigurationObject.ServerObject().apply {
+                                        address = "127.0.0.1"
+                                        this.port = port + 10
+                                    }
+                                )
+                            })
+                    }
                 }
             },
             OutboundObject().apply {

+ 9 - 0
bin/libs/shadowsocks.sh

@@ -0,0 +1,9 @@
+#!/bin/bash
+
+source "bin/init/env.sh"
+
+git submodule update --init shadowsocks/src/main/rust/shadowsocks-rust
+rm -rf shadowsocks/build/outputs/aar
+./gradlew shadowsocks:assembleRelease || exit 1
+mkdir -p app/libs
+cp shadowsocks/build/outputs/aar/* app/libs

+ 6 - 6
build.gradle

@@ -1,26 +1,26 @@
-// Top-level build file where you can add configuration options common to all sub-projects/modules.
 buildscript {
     ext {
         kotlin_version = "1.4.32"
     }
     repositories {
-        google()
         jcenter()
+        mavenCentral()
+        google()
+        maven { url "https://plugins.gradle.org/m2/" }
     }
     dependencies {
         classpath "com.android.tools.build:gradle:4.1.3"
         classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
-
-        // NOTE: Do not place your application dependencies here; they belong
-        // in the individual module build.gradle files
+        classpath "gradle.plugin.org.mozilla.rust-android-gradle:plugin:0.8.3"
     }
 }
 
 allprojects {
     repositories {
+        mavenCentral()
         google()
-        jcenter()
         maven { url 'https://jitpack.io' }
+        jcenter()
     }
 }
 

+ 3 - 1
settings.gradle

@@ -1,2 +1,4 @@
 include ':app'
-rootProject.name = "SagerNet"
+rootProject.name = "SagerNet"
+
+include ':shadowsocks'

+ 50 - 0
shadowsocks/build.gradle.kts

@@ -0,0 +1,50 @@
+import com.android.build.gradle.internal.tasks.factory.dependsOn
+
+plugins {
+    id("com.android.library")
+    id("org.mozilla.rust-android-gradle.rust-android")
+}
+
+android {
+    compileSdkVersion(30)
+    defaultConfig {
+        minSdkVersion(21)
+        targetSdkVersion(30)
+    }
+    buildToolsVersion = "30.0.3"
+}
+
+
+cargo {
+    module = "src/main/rust/shadowsocks-rust"
+    libname = "sslocal"
+    targets = listOf("arm", "arm64", "x86", "x86_64")
+    profile = findProperty("CARGO_PROFILE")?.toString() ?: "release"
+    extraCargoBuildArguments = listOf("--bin", "sslocal")
+    featureSpec.noDefaultBut(arrayOf(
+        "stream-cipher",
+        "logging",
+        "local-flow-stat",
+        "local-dns"))
+    exec = { spec, toolchain ->
+        spec.environment("RUST_ANDROID_GRADLE_LINKER_WRAPPER_PY",
+            "$projectDir/$module/../linker-wrapper.py")
+        spec.environment("RUST_ANDROID_GRADLE_TARGET",
+            "target/${toolchain.target}/$profile/lib$libname.so")
+    }
+}
+
+
+tasks.whenTaskAdded {
+    when (name) {
+        "mergeDebugJniLibFolders", "mergeReleaseJniLibFolders" -> dependsOn("cargoBuild")
+    }
+}
+
+tasks.register<Exec>("cargoClean") {
+    executable("cargo")     // cargo.cargoCommand
+    args("clean")
+    workingDir("$projectDir/${cargo.module}")
+}
+
+tasks.clean.dependsOn("cargoClean")

+ 4 - 0
shadowsocks/src/main/AndroidManifest.xml

@@ -0,0 +1,4 @@
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+    package="io.nekohasekai.ss_rust">
+
+</manifest>

+ 18 - 0
shadowsocks/src/main/rust/linker-wrapper.py

@@ -0,0 +1,18 @@
+from __future__ import absolute_import, print_function, unicode_literals
+
+import os
+import pipes
+import shutil
+import subprocess
+import sys
+
+args = [os.environ['RUST_ANDROID_GRADLE_CC'], os.environ['RUST_ANDROID_GRADLE_CC_LINK_ARG']] + sys.argv[1:]
+
+# This only appears when the subprocess call fails, but it's helpful then.
+printable_cmd = ' '.join(pipes.quote(arg) for arg in args)
+print(printable_cmd)
+
+code = subprocess.call(args)
+if code == 0:
+    shutil.copyfile(sys.argv[sys.argv.index('-o') + 1], os.environ['RUST_ANDROID_GRADLE_TARGET'])
+sys.exit(code)

+ 1 - 0
shadowsocks/src/main/rust/shadowsocks-rust

@@ -0,0 +1 @@
+Subproject commit 49b7004d7565de98a24b49657443b19c03365a8f