diff --git a/.github/issue_template.md b/.github/issue_template.md index d5edcba1..bc91020f 100644 --- a/.github/issue_template.md +++ b/.github/issue_template.md @@ -30,7 +30,7 @@ _Put an `x` inside the [ ] that applies._ * [ ] Per-App Proxy * [ ] Bypass mode * [ ] UDP Forwarding -* KCP Parameters: (leave blank if not enabled) +* Plugin configuration (if applicable): * [ ] Auto Connect * [ ] TCP Fast Open * [ ] NAT mode diff --git a/plugin/README.md b/plugin/README.md new file mode 100644 index 00000000..05cbe427 --- /dev/null +++ b/plugin/README.md @@ -0,0 +1,169 @@ +# shadowsocks-android plugin framework + +Support library for easier development on [shadowsocks + plugin](https://github.com/shadowsocks/shadowsocks-org/issues/28) for Android. Also includes some + useful resources to easily get consistent styling with the main app. + +## Official plugins + +These are some plugins ready to use on shadowsocks-android. + +* [simple-obfs](https://github.com/shadowsocks/simple-obfs-android/releases) +* [kcptun](https://github.com/shadowsocks/kcptun-android/releases) + +## Developer's guide + +WARNING: This library is still in beta (0.x) and its content is subject to massive changes. + +This library is designed with Java interoperability in mind so theoretically you can use this + library with other languages and/or build tools but there isn't documentation for that yet. This + guide is written for Scala + SBT. Contributions are welcome. + +The following is just a simple guide to get you started, for more technical details, see + [doc.md](https://github.com/shadowsocks/shadowsocks-android/blob/develop/plugin/doc.md). + +### Package name + +There are no arbitrary restrictions/requirements on package name, component name and content + provider authority, but you're suggested to follow the format in this documentations. For package + name, use `com.github.shadowsocks.plugin.$PLUGIN_ID` if it only contains a single plugin to + prevent duplicated plugins. In some places hyphens are not accepted, for example package name. In + that case, hyphens `-` should be changed into underscores `_`. For example, the package name for + `obfs-local` would probably be `com.github.shadowsocks.plugin.obfs_local`. + +### Add dependency + +First you need to add this library to your dependencies. This library is written mostly in Scala + and it's most convenient to use it with SBT: + +```scala +libraryDependencies += "com.github.shadowsocks" %% "plugin" % "0.0.1-SNAPSHOT" +``` + +### Native binary configuration + +First you need to get your native binary compiling on Android platform. + +* [Sample project for C](https://github.com/shadowsocks/simple-obfs-android/tree/4f82c4a4e415d666e70a7e2e60955cb0d85c1615); +* [Sample project for Go](https://github.com/shadowsocks/kcptun-android/tree/41f42077e177618553417c16559784a51e9d8c4c). + +In addition to functionalities of a normal plugin, it has to support these additional flags that + may get passed through arguments: + +* `-V`: VPN mode. In this case, the plugin should pass all file descriptors that needs protecting + from VPN connections (i.e. its traffic will not be forwarded through the VPN) through an + ancillary message to `./protect_path`; +* `--fast-open`: TCP fast open enabled. + +### Implement a binary provider + +It's super easy. You just need to implement two or three methods. For example for `obfs-local`: + +```scala +final class BinaryProvider extends NativePluginProvider { + override protected def populateFiles(provider: PathProvider) { + provider.addPath("obfs-local", "755") + // add additional files here + } + + // remove this method to disable fast mode, read more in the documentation + override def getExecutable: String = + getContext.getApplicationInfo.nativeLibraryDir + "/libobfs-local.so" + + override def openFile(uri: Uri): ParcelFileDescriptor = uri.getPath match { + case "/obfs-local" => + ParcelFileDescriptor.open(new File(getExecutable), ParcelFileDescriptor.MODE_READ_ONLY) + // handle additional files here + case _ => throw new FileNotFoundException() + } +} +``` + +Then add it to your manifest: + +```xml + + ... + + ... + + + + + + + + + + + + ... + + +``` + +### Add user interfaces + +You should add to your plugin app a configuration activity or a help activity or both if you're + going to use `ConfigurationActivity.fallbackToManualEditor`. + +#### Configuration activity + +This is used if found instead of a manual input dialog when user clicks "Configure..." in the main + app. This gives you maximum freedom of the user interface. To implement this, you need to extend + `ConfigurationActivity` and you will get current options via + `onInitializePluginOptions(PluginOptions)` and you can invoke `saveChanges(PluginOptions)` or + `discardChanges()` before `finish()` or `fallbackToManualEditor()`. Then add it to your manifest: + +```xml + + ... + + ... + + + + + + + + ... + + +``` + +#### Help activity/callback + +This is started when user taps "?" in manual editor. To implement this, you need to extend + `HelpCallback` if you want a simple dialog with help message as `CharSequence` or `HelpActivity` + if you want to provide custom user interface, implement the required methods, then add it to your + manifest: + +```xml + + ... + + ... + + + + + + + + ... + + +``` + +Great. Now your plugin is ready to use. diff --git a/plugin/doc.md b/plugin/doc.md new file mode 100644 index 00000000..4caebaac --- /dev/null +++ b/plugin/doc.md @@ -0,0 +1,230 @@ +# Overview + +Plugin should be bundled as an apk. `$PLUGIN_ID` in this documentation corresponds to the + executable name for the plugin in order to be cross-platform, e.g. `obfs-local`. An apk can have + more than one plugins bundled. We don't care as long as they have different `$PLUGIN_ID`. For + duplicated plugin ID, host should refuse to start. + +There are no arbitrary restrictions/requirements on package name, component name and content + provider authority, but you're suggested to follow the format in this documentations. For package + name, use `com.github.shadowsocks.plugin.$PLUGIN_ID` if it only contains a single plugin to prevent + duplicated plugins. In some places hyphens are not accepted, for example package name. In that + case, hyphens `-` should be changed into underscores `_`. For example, the package name for + `obfs-local` would probably be `com.github.shadowsocks.plugin.obfs_local`. + +It's advised to use this library for easier development, but you're free to start from scratch following this + documentation. + +# Plugin configuration + +Plugins get their args configured via one of the following two options: + +* A configuration activity; + ([example](https://github.com/shadowsocks/simple-obfs-android/tree/4f82c4a4e415d666e70a7e2e60955cb0d85c1615)) +* If no configuration activity is found or the activity requests the fallback mode, the fallback + mode will be used: user manual input and optional help message. + ([example](https://github.com/shadowsocks/kcptun-android/tree/41f42077e177618553417c16559784a51e9d8c4c)) + +Your user interface need not be consistent with shadowsocks-android styling - you don't need to use + preferences UI at all if you don't feel like it - however it's recommended to use Material Design + at minimum. + +## Configuration activity + +If the plugin provides a configuration activity, it will be started when user picks your plugin and + taps configure. It: + +* MUST have action: `com.github.shadowsocks.plugin.ACTION_CONFIGURE`; +* MUST have category: `android.intent.category.DEFAULT`; +* MUST be able to receive data URI `plugin://com.github.shadowsocks/$PLUGIN_ID`; +* SHOULD parse string extra `com.github.shadowsocks.plugin.EXTRA_OPTIONS` (all options as a single + string) and display the current options; +* SHOULD distinguish between server settings and feature settings in some way, e.g. for + `obfs-local`, `obfs` is a server setting and `obfs_host` is a feature setting; +* On finish, it SHOULD return one of the following results: + - `RESULT_OK = 0`: In this case it MUST return the data Intent with the new + `com.github.shadowsocks.plugin.EXTRA_OPTIONS`; + - `RESULT_CANCELED = -1`: Nothing will be changed; + - `RESULT_FALLBACK = 1`: Fallback mode is requested and the host should display the fallback + editor. + +This corresponds to `com.github.shadowsocks.plugin.ConfigurationActivity` in the plugin library. + Here's what a proper configuration activity usually should look like in `AndroidManifest.xml`: + +```xml + + ... + + ... + + + + + + + + ... + + +``` + +## Help activity/callback + +If the plugin doesn't provide a configuration activity, it's highly recommended to provide a help + message in the form of an Activity. It: + +* MUST have action: `com.github.shadowsocks.plugin.ACTION_HELP`; +* MUST have category: `android.intent.category.DEFAULT`; +* MUST be able to receive data URI `plugin://com.github.shadowsocks/$PLUGIN_ID`; +* CAN parse string extra `com.github.shadowsocks.plugin.EXTRA_OPTIONS` and display some more + relevant information; +* SHOULD either: + - Be invisible and return help message with CharSequence extra + `com.github.shadowsocks.plugin.EXTRA_HELP_MESSAGE` in the data intent with `RESULT_OK`; (in this + case, a simple dialog will be shown containing the message) + - Be visible and return `RESULT_CANCELED`. +* SHOULD distinguish between server settings and feature settings in some way, e.g. for + `simple_obfs`, `obfs` is a server setting and `obfs_host` is a feature setting. + +This corresponds to `com.github.shadowsocks.plugin.HelpActivity` or + `com.github.shadowsocks.plugin.HelpCallback` in the plugin library. Here's what a proper help + activity/callback usually should look like in `AndroidManifest.xml`: + +```xml + + ... + + ... + + + + + + + + ... + + +``` + +# Plugin implementation + +Every plugin can be either in native mode or JVM mode. + +## Native mode + +In native mode, plugins are provided as native executables and `shadowsocks-libev`'s plugin mode + will be used. + +Every native mode plugin MUST have a content provider to provide the native executables (since they + can exceed 1M which is the limit of Intent size) that: + +* MUST have `android:label` and `android:icon`; (may be configured by its parent `application`) +* MUST have an intent filter with action `com.github.shadowsocks.plugin.ACTION_NATIVE_PLUGIN`; + (used for discovering plugins) +* MUST have meta-data `com.github.shadowsocks.plugin.id` with string value `$PLUGIN_ID`; +* MUST have an intent filter with action `com.github.shadowsocks.plugin.ACTION_NATIVE_PLUGIN` and + data `plugin://com.github.shadowsocks/$PLUGIN_ID`; (used for configuring plugin) +* CAN have meta-data `com.github.shadowsocks.plugin.default_config` with string value, default is + empty; +* MUST implement `query` that returns the file list which MUST include `$PLUGIN_ID` when having + these as arguments: + - `uri = "content://$authority_of_your_provider`; + - `projection = ["path", "mode"]`; (relative path, for example `obfs-local`; file mode, for + example `755`) + - `selection = null`; + - `selectionArgs = null`; + - `sortOrder = null`; +* MUST implement `openFile` that for files returned in `query`, `openFile` with `mode = "r"` returns + a valid `ParcelFileDescriptor` for reading. For example, `uri` can be + `content://com.github.shadowsocks.plugin.kcptun/kcptun`. + +This corresponds to `com.github.shadowsocks.plugin.NativePluginProvider` in the plugin library. + Here's what a proper native plugin provider usually should look like in `AndroidManifest.xml`: + +```xml + + ... + + ... + + + + + + + + + + + + ... + + +``` + +## Native mode without binary copying + +If your plugin binary executable can run in place, you can support native mode without binary + copying. To support this mode, your `ContentProvider` must first support native mode with binary + copying (this will be used if the fast routine fails) and: + +* MUST implement `call` that returns absolute path to the entry executable as + `com.github.shadowsocks.plugin.EXTRA_ENTRY` when having `method = "shadowsocks:getExecutable"`; + (`com.github.shadowsocks.plugin.EXTRA_OPTIONS` is provided in extras as well just in case you + need them) + +If you don't plan to support this mode, you can just throw `UnsupportedOperationException` when + being invoked. It will fallback to the slow routine automatically. + +## JVM mode + +This feature hasn't been implemented yet. + +# Plugin security + +Plugins are certified using package signatures and shadowsocks-android will consider these + signatures as trusted: + +* Signatures by [trusted sources](https://github.com/shadowsocks/shadowsocks-android/blob/develop/mobile/src/main/scala/com/github/shadowsocks/plugin/PluginManager.scala#L22) + which includes: + - @madeye, i.e. the signer of the main repo; + - The main repo doesn't contain any other trusted signatures. Third-party forks should add their + signatures to this trusted sources if they have plugins signed by them before publishing their + source code. +* Current package signature, which means: + - If you get apk from shadowsocks-android releases or Google Play, this means only apk signed by + @madeye will be recognized as trusted. + - If you get apk from a third-party fork, all plugins from that developer will get recognized as + trusted automatically even if its source code isn't available anywhere online. + +A warning will be shown for untrusted plugins. No arbitrary restrictions will be applied. + +# Plugin platform versioning + +In order to be able to identify compatible and incompatible plugins, [Semantic + Versioning](http://semver.org/) will be used. + +>Given a version number MAJOR.MINOR.PATCH, increment the: +> +>1. MAJOR version when you make incompatible API changes, +>2. MINOR version when you add functionality in a backwards-compatible manner, and +>3. PATCH version when you make backwards-compatible bug fixes. + +Plugin app must include this in their application tag: (which should be automatically included if + you are using our library) + +``` + +``` diff --git a/plugin/src/main/java/com/github/shadowsocks/plugin/PluginContract.java b/plugin/src/main/java/com/github/shadowsocks/plugin/PluginContract.java index ec73be4b..e716d2cb 100644 --- a/plugin/src/main/java/com/github/shadowsocks/plugin/PluginContract.java +++ b/plugin/src/main/java/com/github/shadowsocks/plugin/PluginContract.java @@ -26,7 +26,7 @@ public final class PluginContract { /** * Activity Action: Used for HelpActivity or HelpCallback. * - * Constant Value: "com.github.shadowsocks.plugin.ACTION_CONFIGURE" + * Constant Value: "com.github.shadowsocks.plugin.ACTION_HELP" */ public static final String ACTION_HELP = "com.github.shadowsocks.plugin.ACTION_HELP"; @@ -39,7 +39,7 @@ public final class PluginContract { */ public static final String EXTRA_ENTRY = "com.github.shadowsocks.plugin.EXTRA_ENTRY"; /** - * The lookup key for a string that provides the whole command as a string. + * The lookup key for a string that provides the options as a string. * * Example: "obfs=http;obfs-host=www.baidu.com" * @@ -69,7 +69,7 @@ public final class PluginContract { */ public static final String METADATA_KEY_DEFAULT_CONFIG = "com.github.shadowsocks.plugin.default_config"; - static final String METHOD_GET_EXECUTABLE = "shadowsocks:getExecutable"; + public static final String METHOD_GET_EXECUTABLE = "shadowsocks:getExecutable"; /** ConfigurationActivity result: fallback to manual edit mode. */ public static final int RESULT_FALLBACK = 1; diff --git a/plugin/src/main/scala/com/github/shadowsocks/plugin/ConfigurationActivity.scala b/plugin/src/main/scala/com/github/shadowsocks/plugin/ConfigurationActivity.scala index ff618ade..3759c868 100644 --- a/plugin/src/main/scala/com/github/shadowsocks/plugin/ConfigurationActivity.scala +++ b/plugin/src/main/scala/com/github/shadowsocks/plugin/ConfigurationActivity.scala @@ -12,9 +12,13 @@ import android.content.Intent * ... * <application> * ... - * <activity android:name="com.github.shadowsocks.$PLUGIN_ID.ConfigureActivity"> + * <activity android:name=".ConfigureActivity"> * <intent-filter> - * <action android:name="com.github.shadowsocks.plugin.$PLUGIN_ID.ACTION_CONFIGURE" /> + * <action android:name="com.github.shadowsocks.plugin.ACTION_CONFIGURE"/> + * <category android:name="android.intent.category.DEFAULT"/> + * <data android:scheme="plugin" + * android:host="com.github.shadowsocks" + * android:path="/$PLUGIN_ID"/> * </intent-filter> * </activity> * ... diff --git a/plugin/src/main/scala/com/github/shadowsocks/plugin/HelpActivity.scala b/plugin/src/main/scala/com/github/shadowsocks/plugin/HelpActivity.scala index 40288b8c..591833d9 100644 --- a/plugin/src/main/scala/com/github/shadowsocks/plugin/HelpActivity.scala +++ b/plugin/src/main/scala/com/github/shadowsocks/plugin/HelpActivity.scala @@ -8,9 +8,13 @@ package com.github.shadowsocks.plugin * ... * <application> * ... - * <activity android:name="com.github.shadowsocks.$PLUGIN_ID.HelpActivity"> + * <activity android:name=".HelpActivity"> * <intent-filter> - * <action android:name="com.github.shadowsocks.plugin.$PLUGIN_ID.ACTION_HELP" /> + * <action android:name="com.github.shadowsocks.plugin.ACTION_HELP"/> + * <category android:name="android.intent.category.DEFAULT"/> + * <data android:scheme="plugin" + * android:host="com.github.shadowsocks" + * android:path="/$PLUGIN_ID"/> * </intent-filter> * </activity> * ...