diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..401f9d6
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,16 @@
+*.iml
+.gradle
+/local.properties
+/.idea/caches
+/.idea/libraries
+/.idea/modules.xml
+/.idea/workspace.xml
+/.idea/navEditor.xml
+/.idea/assetWizardSettings.xml
+.DS_Store
+build/
+/captures
+.externalNativeBuild
+/.idea
+release/
+jniLibs/
\ No newline at end of file
diff --git a/.gitmodules b/.gitmodules
new file mode 100644
index 0000000..3b23f8a
--- /dev/null
+++ b/.gitmodules
@@ -0,0 +1,3 @@
+[submodule "go/src/github.com/ginuerzh/gost"]
+ path = go/src/github.com/ginuerzh/gost
+ url = https://github.com/xausky/gost.git
diff --git a/app/build.gradle b/app/build.gradle
new file mode 100644
index 0000000..03332aa
--- /dev/null
+++ b/app/build.gradle
@@ -0,0 +1,49 @@
+apply plugin: 'com.android.application'
+
+android {
+ compileSdkVersion 28
+ defaultConfig {
+ applicationId "com.github.shadowsocks.plugin.gost"
+ minSdkVersion 21
+ targetSdkVersion 28
+ versionCode 1
+ versionName "1.0"
+ }
+ splits {
+ abi {
+ enable true
+ reset()
+ include 'x86', 'x86_64', 'armeabi-v7a', "arm64-v8a"
+ universalApk true
+ }
+ }
+ signingConfigs {
+ releaseConfig {
+ storeFile file("../../xausky.jks")
+ storePassword project.hasProperty("KEYSTORE_PASS") ? KEYSTORE_PASS : System.getenv("KEYSTORE_PASS")
+ keyAlias project.hasProperty("ALIAS_NAME") ? ALIAS_NAME : System.getenv("ALIAS_NAME")
+ keyPassword project.hasProperty("ALIAS_PASS") ? ALIAS_PASS : System.getenv("ALIAS_PASS")
+ }
+ }
+ buildTypes {
+ release {
+ minifyEnabled false
+ signingConfig signingConfigs.releaseConfig
+ proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
+ }
+ }
+}
+
+dependencies {
+ implementation 'com.github.shadowsocks:plugin:1.2.0'
+}
+
+task buildGoLibrary(type: Exec) {
+ commandLine 'sh', '../build-go.sh'
+}
+
+tasks.whenTaskAdded { theTask ->
+ if (theTask.name.equals("preReleaseBuild")) {
+ theTask.dependsOn "buildGoLibrary"
+ }
+}
diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro
new file mode 100644
index 0000000..f1b4245
--- /dev/null
+++ b/app/proguard-rules.pro
@@ -0,0 +1,21 @@
+# Add project specific ProGuard rules here.
+# You can control the set of applied configuration files using the
+# proguardFiles setting in build.gradle.
+#
+# For more details, see
+# http://developer.android.com/guide/developing/tools/proguard.html
+
+# If your project uses WebView with JS, uncomment the following
+# and specify the fully qualified class name to the JavaScript interface
+# class:
+#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
+# public *;
+#}
+
+# Uncomment this to preserve the line number information for
+# debugging stack traces.
+#-keepattributes SourceFile,LineNumberTable
+
+# If you keep the line number information, uncomment this to
+# hide the original source file name.
+#-renamesourcefileattribute SourceFile
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..85ccda8
--- /dev/null
+++ b/app/src/main/AndroidManifest.xml
@@ -0,0 +1,28 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/java/com/github/shadowsocks/plugin/gost/BinaryProvider.java b/app/src/main/java/com/github/shadowsocks/plugin/gost/BinaryProvider.java
new file mode 100644
index 0000000..02550be
--- /dev/null
+++ b/app/src/main/java/com/github/shadowsocks/plugin/gost/BinaryProvider.java
@@ -0,0 +1,34 @@
+package com.github.shadowsocks.plugin.gost;
+
+import android.net.Uri;
+import android.os.ParcelFileDescriptor;
+import com.github.shadowsocks.plugin.NativePluginProvider;
+import com.github.shadowsocks.plugin.PathProvider;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import java.io.File;
+import java.io.FileNotFoundException;
+
+public class BinaryProvider extends NativePluginProvider {
+ @NotNull
+ @Override
+ public ParcelFileDescriptor openFile(@Nullable Uri uri) {
+ try {
+ return ParcelFileDescriptor.open(new File(getExecutable()), ParcelFileDescriptor.MODE_READ_ONLY);
+ } catch (FileNotFoundException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ @NotNull
+ @Override
+ public String getExecutable() {
+ return getContext().getApplicationInfo().nativeLibraryDir + "/libgost-plugin.so";
+ }
+
+ @Override
+ protected void populateFiles(@NotNull PathProvider provider) {
+ provider.addPath("gost-plugin", 0755);
+ }
+}
diff --git a/app/src/main/res/drawable-v24/ic_launcher_foreground.xml b/app/src/main/res/drawable-v24/ic_launcher_foreground.xml
new file mode 100644
index 0000000..1f6bb29
--- /dev/null
+++ b/app/src/main/res/drawable-v24/ic_launcher_foreground.xml
@@ -0,0 +1,34 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/drawable/ic_launcher_background.xml b/app/src/main/res/drawable/ic_launcher_background.xml
new file mode 100644
index 0000000..0d025f9
--- /dev/null
+++ b/app/src/main/res/drawable/ic_launcher_background.xml
@@ -0,0 +1,170 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
new file mode 100644
index 0000000..eca70cf
--- /dev/null
+++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
new file mode 100644
index 0000000..eca70cf
--- /dev/null
+++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher.png b/app/src/main/res/mipmap-hdpi/ic_launcher.png
new file mode 100644
index 0000000..898f3ed
Binary files /dev/null and b/app/src/main/res/mipmap-hdpi/ic_launcher.png differ
diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher_round.png b/app/src/main/res/mipmap-hdpi/ic_launcher_round.png
new file mode 100644
index 0000000..dffca36
Binary files /dev/null and b/app/src/main/res/mipmap-hdpi/ic_launcher_round.png differ
diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher.png b/app/src/main/res/mipmap-mdpi/ic_launcher.png
new file mode 100644
index 0000000..64ba76f
Binary files /dev/null and b/app/src/main/res/mipmap-mdpi/ic_launcher.png differ
diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher_round.png b/app/src/main/res/mipmap-mdpi/ic_launcher_round.png
new file mode 100644
index 0000000..dae5e08
Binary files /dev/null and b/app/src/main/res/mipmap-mdpi/ic_launcher_round.png differ
diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/app/src/main/res/mipmap-xhdpi/ic_launcher.png
new file mode 100644
index 0000000..e5ed465
Binary files /dev/null and b/app/src/main/res/mipmap-xhdpi/ic_launcher.png differ
diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png b/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png
new file mode 100644
index 0000000..14ed0af
Binary files /dev/null and b/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png differ
diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
new file mode 100644
index 0000000..b0907ca
Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ
diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png b/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png
new file mode 100644
index 0000000..d8ae031
Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png differ
diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
new file mode 100644
index 0000000..2c18de9
Binary files /dev/null and b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ
diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png
new file mode 100644
index 0000000..beed3cd
Binary files /dev/null and b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png differ
diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml
new file mode 100644
index 0000000..69b2233
--- /dev/null
+++ b/app/src/main/res/values/colors.xml
@@ -0,0 +1,6 @@
+
+
+ #008577
+ #00574B
+ #D81B60
+
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
new file mode 100644
index 0000000..7d13578
--- /dev/null
+++ b/app/src/main/res/values/strings.xml
@@ -0,0 +1,3 @@
+
+ ShadowsocksGostPlugin
+
diff --git a/build-go.sh b/build-go.sh
new file mode 100644
index 0000000..037cb18
--- /dev/null
+++ b/build-go.sh
@@ -0,0 +1,17 @@
+DIR="$( cd "$( dirname "$0" )" && pwd )"
+export GOPATH=$DIR/go
+CC=$ANDROID_NDK_ROOT/toolchains/llvm/prebuilt/linux-x86_64/bin/armv7a-linux-androideabi21-clang \
+GOOS="android" GOARCH="arm" CGO_ENABLED="1" \
+go build -a -o $DIR/app/src/main/jniLibs/armeabi-v7a/libgost-plugin.so github.com/ginuerzh/gost/cmd/gost
+
+CC=$ANDROID_NDK_ROOT/toolchains/llvm/prebuilt/linux-x86_64/bin/aarch64-linux-android21-clang \
+GOOS="android" GOARCH="arm64" CGO_ENABLED="1" \
+go build -a -o $DIR/app/src/main/jniLibs/arm64-v8a/libgost-plugin.so github.com/ginuerzh/gost/cmd/gost
+
+CC=$ANDROID_NDK_ROOT/toolchains/llvm/prebuilt/linux-x86_64/bin/i686-linux-android21-clang \
+GOOS="android" GOARCH="386" CGO_ENABLED="1" \
+go build -a -o $DIR/app/src/main/jniLibs/x86/libgost-plugin.so github.com/ginuerzh/gost/cmd/gost
+
+CC=$ANDROID_NDK_ROOT/toolchains/llvm/prebuilt/linux-x86_64/bin/x86_64-linux-android21-clang \
+GOOS="android" GOARCH="amd64" CGO_ENABLED="1" \
+go build -a -o $DIR/app/src/main/jniLibs/x86_64/libgost-plugin.so github.com/ginuerzh/gost/cmd/gost
\ No newline at end of file
diff --git a/build.gradle b/build.gradle
new file mode 100644
index 0000000..fafc1b9
--- /dev/null
+++ b/build.gradle
@@ -0,0 +1,27 @@
+// Top-level build file where you can add configuration options common to all sub-projects/modules.
+
+buildscript {
+ repositories {
+ google()
+ jcenter()
+
+ }
+ dependencies {
+ classpath 'com.android.tools.build:gradle:3.4.0'
+
+ // NOTE: Do not place your application dependencies here; they belong
+ // in the individual module build.gradle files
+ }
+}
+
+allprojects {
+ repositories {
+ google()
+ jcenter()
+
+ }
+}
+
+task clean(type: Delete) {
+ delete rootProject.buildDir
+}
diff --git a/gitignore.txt b/gitignore.txt
new file mode 100644
index 0000000..cb36f0d
--- /dev/null
+++ b/gitignore.txt
@@ -0,0 +1,11 @@
+*.iml
+.gradle
+/local.properties
+/gradle.properties
+/.idea/
+.DS_Store
+build/
+/captures
+.externalNativeBuild
+/app/fabric.properties
+.vscode
diff --git a/go/src/github.com/ginuerzh/gost b/go/src/github.com/ginuerzh/gost
new file mode 160000
index 0000000..a8e7e89
--- /dev/null
+++ b/go/src/github.com/ginuerzh/gost
@@ -0,0 +1 @@
+Subproject commit a8e7e89d2cbb4334fcc1b61410ff504309cbb130
diff --git a/gradle.properties b/gradle.properties
new file mode 100644
index 0000000..82618ce
--- /dev/null
+++ b/gradle.properties
@@ -0,0 +1,15 @@
+# Project-wide Gradle settings.
+# IDE (e.g. Android Studio) users:
+# Gradle settings configured through the IDE *will override*
+# any settings specified in this file.
+# For more details on how to configure your build environment visit
+# http://www.gradle.org/docs/current/userguide/build_environment.html
+# Specifies the JVM arguments used for the daemon process.
+# The setting is particularly useful for tweaking memory settings.
+org.gradle.jvmargs=-Xmx1536m
+# When configured, Gradle will run in incubating parallel mode.
+# This option should only be used with decoupled projects. More details, visit
+# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
+# org.gradle.parallel=true
+
+
diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar
new file mode 100644
index 0000000..f6b961f
Binary files /dev/null and b/gradle/wrapper/gradle-wrapper.jar differ
diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties
new file mode 100644
index 0000000..bd95d92
--- /dev/null
+++ b/gradle/wrapper/gradle-wrapper.properties
@@ -0,0 +1,6 @@
+#Mon May 06 23:00:16 CST 2019
+distributionBase=GRADLE_USER_HOME
+distributionPath=wrapper/dists
+zipStoreBase=GRADLE_USER_HOME
+zipStorePath=wrapper/dists
+distributionUrl=https\://services.gradle.org/distributions/gradle-5.1.1-all.zip
diff --git a/gradlew b/gradlew
new file mode 100755
index 0000000..cccdd3d
--- /dev/null
+++ b/gradlew
@@ -0,0 +1,172 @@
+#!/usr/bin/env sh
+
+##############################################################################
+##
+## Gradle start up script for UN*X
+##
+##############################################################################
+
+# Attempt to set APP_HOME
+# Resolve links: $0 may be a link
+PRG="$0"
+# Need this for relative symlinks.
+while [ -h "$PRG" ] ; do
+ ls=`ls -ld "$PRG"`
+ link=`expr "$ls" : '.*-> \(.*\)$'`
+ if expr "$link" : '/.*' > /dev/null; then
+ PRG="$link"
+ else
+ PRG=`dirname "$PRG"`"/$link"
+ fi
+done
+SAVED="`pwd`"
+cd "`dirname \"$PRG\"`/" >/dev/null
+APP_HOME="`pwd -P`"
+cd "$SAVED" >/dev/null
+
+APP_NAME="Gradle"
+APP_BASE_NAME=`basename "$0"`
+
+# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+DEFAULT_JVM_OPTS=""
+
+# Use the maximum available, or set MAX_FD != -1 to use that value.
+MAX_FD="maximum"
+
+warn () {
+ echo "$*"
+}
+
+die () {
+ echo
+ echo "$*"
+ echo
+ exit 1
+}
+
+# OS specific support (must be 'true' or 'false').
+cygwin=false
+msys=false
+darwin=false
+nonstop=false
+case "`uname`" in
+ CYGWIN* )
+ cygwin=true
+ ;;
+ Darwin* )
+ darwin=true
+ ;;
+ MINGW* )
+ msys=true
+ ;;
+ NONSTOP* )
+ nonstop=true
+ ;;
+esac
+
+CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
+
+# Determine the Java command to use to start the JVM.
+if [ -n "$JAVA_HOME" ] ; then
+ if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
+ # IBM's JDK on AIX uses strange locations for the executables
+ JAVACMD="$JAVA_HOME/jre/sh/java"
+ else
+ JAVACMD="$JAVA_HOME/bin/java"
+ fi
+ if [ ! -x "$JAVACMD" ] ; then
+ die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+ fi
+else
+ JAVACMD="java"
+ which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+fi
+
+# Increase the maximum file descriptors if we can.
+if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
+ MAX_FD_LIMIT=`ulimit -H -n`
+ if [ $? -eq 0 ] ; then
+ if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
+ MAX_FD="$MAX_FD_LIMIT"
+ fi
+ ulimit -n $MAX_FD
+ if [ $? -ne 0 ] ; then
+ warn "Could not set maximum file descriptor limit: $MAX_FD"
+ fi
+ else
+ warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
+ fi
+fi
+
+# For Darwin, add options to specify how the application appears in the dock
+if $darwin; then
+ GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
+fi
+
+# For Cygwin, switch paths to Windows format before running java
+if $cygwin ; then
+ APP_HOME=`cygpath --path --mixed "$APP_HOME"`
+ CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
+ JAVACMD=`cygpath --unix "$JAVACMD"`
+
+ # We build the pattern for arguments to be converted via cygpath
+ ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
+ SEP=""
+ for dir in $ROOTDIRSRAW ; do
+ ROOTDIRS="$ROOTDIRS$SEP$dir"
+ SEP="|"
+ done
+ OURCYGPATTERN="(^($ROOTDIRS))"
+ # Add a user-defined pattern to the cygpath arguments
+ if [ "$GRADLE_CYGPATTERN" != "" ] ; then
+ OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
+ fi
+ # Now convert the arguments - kludge to limit ourselves to /bin/sh
+ i=0
+ for arg in "$@" ; do
+ CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
+ CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
+
+ if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
+ eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
+ else
+ eval `echo args$i`="\"$arg\""
+ fi
+ i=$((i+1))
+ done
+ case $i in
+ (0) set -- ;;
+ (1) set -- "$args0" ;;
+ (2) set -- "$args0" "$args1" ;;
+ (3) set -- "$args0" "$args1" "$args2" ;;
+ (4) set -- "$args0" "$args1" "$args2" "$args3" ;;
+ (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
+ (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
+ (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
+ (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
+ (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
+ esac
+fi
+
+# Escape application args
+save () {
+ for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
+ echo " "
+}
+APP_ARGS=$(save "$@")
+
+# Collect all arguments for the java command, following the shell quoting and substitution rules
+eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
+
+# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong
+if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then
+ cd "$(dirname "$0")"
+fi
+
+exec "$JAVACMD" "$@"
diff --git a/gradlew.bat b/gradlew.bat
new file mode 100644
index 0000000..f955316
--- /dev/null
+++ b/gradlew.bat
@@ -0,0 +1,84 @@
+@if "%DEBUG%" == "" @echo off
+@rem ##########################################################################
+@rem
+@rem Gradle startup script for Windows
+@rem
+@rem ##########################################################################
+
+@rem Set local scope for the variables with windows NT shell
+if "%OS%"=="Windows_NT" setlocal
+
+set DIRNAME=%~dp0
+if "%DIRNAME%" == "" set DIRNAME=.
+set APP_BASE_NAME=%~n0
+set APP_HOME=%DIRNAME%
+
+@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+set DEFAULT_JVM_OPTS=
+
+@rem Find java.exe
+if defined JAVA_HOME goto findJavaFromJavaHome
+
+set JAVA_EXE=java.exe
+%JAVA_EXE% -version >NUL 2>&1
+if "%ERRORLEVEL%" == "0" goto init
+
+echo.
+echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:findJavaFromJavaHome
+set JAVA_HOME=%JAVA_HOME:"=%
+set JAVA_EXE=%JAVA_HOME%/bin/java.exe
+
+if exist "%JAVA_EXE%" goto init
+
+echo.
+echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:init
+@rem Get command-line arguments, handling Windows variants
+
+if not "%OS%" == "Windows_NT" goto win9xME_args
+
+:win9xME_args
+@rem Slurp the command line arguments.
+set CMD_LINE_ARGS=
+set _SKIP=2
+
+:win9xME_args_slurp
+if "x%~1" == "x" goto execute
+
+set CMD_LINE_ARGS=%*
+
+:execute
+@rem Setup the command line
+
+set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
+
+@rem Execute Gradle
+"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
+
+:end
+@rem End local scope for the variables with windows NT shell
+if "%ERRORLEVEL%"=="0" goto mainEnd
+
+:fail
+rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
+rem the _cmd.exe /c_ return code!
+if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
+exit /b 1
+
+:mainEnd
+if "%OS%"=="Windows_NT" endlocal
+
+:omega
diff --git a/main.go b/main.go
new file mode 100644
index 0000000..8a7a334
--- /dev/null
+++ b/main.go
@@ -0,0 +1,499 @@
+package main
+
+import (
+ "crypto/sha256"
+ "crypto/tls"
+ "flag"
+ "fmt"
+ "net"
+ "strings"
+
+ // _ "net/http/pprof"
+ "os"
+ "runtime"
+ "time"
+
+ "github.com/ginuerzh/gost"
+ "github.com/go-log/log"
+)
+
+var (
+ options route
+ routes []route
+)
+
+func init() {
+ gost.SetLogger(&gost.LogLogger{})
+
+ var (
+ configureFile string
+ printVersion bool
+ vpnMode bool
+ fastOpen bool
+ )
+ localHost := os.Getenv("SS_LOCAL_HOST")
+ localPort := os.Getenv("SS_LOCAL_PORT")
+ pluginOptions := os.Getenv("SS_PLUGIN_OPTIONS")
+ if localHost == "" || localPort == "" {
+ fmt.Fprintln(os.Stderr, "Can only be used in the shadowsocks plugin.")
+ os.Exit(1)
+ }
+ if pluginOptions == "" {
+ pluginOptions = "-F ss+mws://chacha20:ss123456@#SS_HOST:#SS_PORT"
+ }
+ pluginOptions = strings.ReplaceAll(pluginOptions, "#SS_HOST", os.Getenv("SS_REMOTE_HOST"))
+ pluginOptions = strings.ReplaceAll(pluginOptions, "#SS_PORT", os.Getenv("SS_REMOTE_PORT"))
+
+ os.Args = append(os.Args, "-L")
+ os.Args = append(os.Args, fmt.Sprintf("ss+tcp://chacha20:ss123456@[%s]:%s", localHost, localPort))
+ os.Args = append(os.Args, strings.Split(pluginOptions, " ")...)
+ flag.Var(&options.ChainNodes, "F", "forward address, can make a forward chain")
+ flag.Var(&options.ServeNodes, "L", "listen address, can listen on multiple ports")
+ flag.StringVar(&configureFile, "C", "", "configure file")
+ flag.BoolVar(&options.Debug, "D", false, "enable debug log")
+ flag.BoolVar(&vpnMode, "V", false, "VPN Mode")
+ flag.BoolVar(&fastOpen, "fast-open", false, "fast Open TCP")
+ flag.BoolVar(&printVersion, "PV", false, "print version")
+ flag.Parse()
+
+ if printVersion {
+ fmt.Fprintf(os.Stderr, "gost %s (%s)\n", gost.Version, runtime.Version())
+ os.Exit(0)
+ }
+ if len(options.ServeNodes) > 0 {
+ routes = append(routes, options)
+ }
+ gost.Debug = options.Debug
+
+ if err := loadConfigureFile(configureFile); err != nil {
+ log.Log(err)
+ os.Exit(1)
+ }
+
+ if flag.NFlag() == 0 || len(routes) == 0 {
+ flag.PrintDefaults()
+ os.Exit(0)
+ }
+
+}
+
+func main() {
+ // go func() {
+ // log.Log(http.ListenAndServe("localhost:6060", nil))
+ // }()
+ // NOTE: as of 2.6, you can use custom cert/key files to initialize the default certificate.
+ config, err := tlsConfig(defaultCertFile, defaultKeyFile)
+ if err != nil {
+ // generate random self-signed certificate.
+ cert, err := gost.GenCertificate()
+ if err != nil {
+ log.Log(err)
+ os.Exit(1)
+ }
+ config = &tls.Config{
+ Certificates: []tls.Certificate{cert},
+ }
+ }
+ gost.DefaultTLSConfig = config
+
+ for _, route := range routes {
+ if err := route.serve(); err != nil {
+ log.Log(err)
+ os.Exit(1)
+ }
+ }
+
+ select {}
+}
+
+type route struct {
+ ChainNodes, ServeNodes stringList
+ Retries int
+ Debug bool
+}
+
+func (r *route) initChain() (*gost.Chain, error) {
+ chain := gost.NewChain()
+ chain.Retries = r.Retries
+ gid := 1 // group ID
+
+ for _, ns := range r.ChainNodes {
+ ngroup := gost.NewNodeGroup()
+ ngroup.ID = gid
+ gid++
+
+ // parse the base nodes
+ nodes, err := parseChainNode(ns)
+ if err != nil {
+ return nil, err
+ }
+
+ nid := 1 // node ID
+ for i := range nodes {
+ nodes[i].ID = nid
+ nid++
+ }
+ ngroup.AddNode(nodes...)
+
+ go gost.PeriodReload(&peerConfig{
+ group: ngroup,
+ baseNodes: nodes,
+ }, nodes[0].Get("peer"))
+
+ chain.AddNodeGroup(ngroup)
+ }
+
+ return chain, nil
+}
+
+func parseChainNode(ns string) (nodes []gost.Node, err error) {
+ node, err := gost.ParseNode(ns)
+ if err != nil {
+ return
+ }
+
+ users, err := parseUsers(node.Get("secrets"))
+ if err != nil {
+ return
+ }
+ if node.User == nil && len(users) > 0 {
+ node.User = users[0]
+ }
+ serverName, sport, _ := net.SplitHostPort(node.Addr)
+ if serverName == "" {
+ serverName = "localhost" // default server name
+ }
+
+ rootCAs, err := loadCA(node.Get("ca"))
+ if err != nil {
+ return
+ }
+ tlsCfg := &tls.Config{
+ ServerName: serverName,
+ InsecureSkipVerify: !node.GetBool("secure"),
+ RootCAs: rootCAs,
+ }
+ wsOpts := &gost.WSOptions{}
+ wsOpts.EnableCompression = node.GetBool("compression")
+ wsOpts.ReadBufferSize = node.GetInt("rbuf")
+ wsOpts.WriteBufferSize = node.GetInt("wbuf")
+ wsOpts.UserAgent = node.Get("agent")
+
+ var tr gost.Transporter
+ switch node.Transport {
+ case "tls":
+ tr = gost.TLSTransporter()
+ case "mtls":
+ tr = gost.MTLSTransporter()
+ case "ws":
+ tr = gost.WSTransporter(wsOpts)
+ case "mws":
+ tr = gost.MWSTransporter(wsOpts)
+ case "wss":
+ tr = gost.WSSTransporter(wsOpts)
+ case "mwss":
+ tr = gost.MWSSTransporter(wsOpts)
+ case "kcp":
+ config, err := parseKCPConfig(node.Get("c"))
+ if err != nil {
+ return nil, err
+ }
+ tr = gost.KCPTransporter(config)
+ case "ssh":
+ if node.Protocol == "direct" || node.Protocol == "remote" {
+ tr = gost.SSHForwardTransporter()
+ } else {
+ tr = gost.SSHTunnelTransporter()
+ }
+ case "quic":
+ config := &gost.QUICConfig{
+ TLSConfig: tlsCfg,
+ KeepAlive: node.GetBool("keepalive"),
+ Timeout: time.Duration(node.GetInt("timeout")) * time.Second,
+ IdleTimeout: time.Duration(node.GetInt("idle")) * time.Second,
+ }
+
+ if cipher := node.Get("cipher"); cipher != "" {
+ sum := sha256.Sum256([]byte(cipher))
+ config.Key = sum[:]
+ }
+
+ tr = gost.QUICTransporter(config)
+ case "http2":
+ tr = gost.HTTP2Transporter(tlsCfg)
+ case "h2":
+ tr = gost.H2Transporter(tlsCfg)
+ case "h2c":
+ tr = gost.H2CTransporter()
+
+ case "obfs4":
+ tr = gost.Obfs4Transporter()
+ case "ohttp":
+ tr = gost.ObfsHTTPTransporter()
+ default:
+ tr = gost.TCPTransporter()
+ }
+
+ var connector gost.Connector
+ switch node.Protocol {
+ case "http2":
+ connector = gost.HTTP2Connector(node.User)
+ case "socks", "socks5":
+ connector = gost.SOCKS5Connector(node.User)
+ case "socks4":
+ connector = gost.SOCKS4Connector()
+ case "socks4a":
+ connector = gost.SOCKS4AConnector()
+ case "ss":
+ connector = gost.ShadowConnector(node.User)
+ case "direct":
+ connector = gost.SSHDirectForwardConnector()
+ case "remote":
+ connector = gost.SSHRemoteForwardConnector()
+ case "forward":
+ connector = gost.ForwardConnector()
+ case "sni":
+ connector = gost.SNIConnector(node.Get("host"))
+ case "http":
+ fallthrough
+ default:
+ node.Protocol = "http" // default protocol is HTTP
+ connector = gost.HTTPConnector(node.User)
+ }
+
+ timeout := node.GetInt("timeout")
+ node.DialOptions = append(node.DialOptions,
+ gost.TimeoutDialOption(time.Duration(timeout)*time.Second),
+ )
+
+ handshakeOptions := []gost.HandshakeOption{
+ gost.AddrHandshakeOption(node.Addr),
+ gost.HostHandshakeOption(node.Host),
+ gost.UserHandshakeOption(node.User),
+ gost.TLSConfigHandshakeOption(tlsCfg),
+ gost.IntervalHandshakeOption(time.Duration(node.GetInt("ping")) * time.Second),
+ gost.TimeoutHandshakeOption(time.Duration(timeout) * time.Second),
+ gost.RetryHandshakeOption(node.GetInt("retry")),
+ }
+ node.Client = &gost.Client{
+ Connector: connector,
+ Transporter: tr,
+ }
+
+ node.Bypass = parseBypass(node.Get("bypass"))
+
+ ips := parseIP(node.Get("ip"), sport)
+ for _, ip := range ips {
+ node.Addr = ip
+ // override the default node address
+ node.HandshakeOptions = append(handshakeOptions, gost.AddrHandshakeOption(ip))
+ // One node per IP
+ nodes = append(nodes, node)
+ }
+ if len(ips) == 0 {
+ node.HandshakeOptions = handshakeOptions
+ nodes = []gost.Node{node}
+ }
+
+ if node.Transport == "obfs4" {
+ for i := range nodes {
+ if err := gost.Obfs4Init(nodes[i], false); err != nil {
+ return nil, err
+ }
+ }
+ }
+
+ return
+}
+
+func (r *route) serve() error {
+ chain, err := r.initChain()
+ if err != nil {
+ return err
+ }
+
+ for _, ns := range r.ServeNodes {
+ node, err := gost.ParseNode(ns)
+ if err != nil {
+ return err
+ }
+ users, err := parseUsers(node.Get("secrets"))
+ if err != nil {
+ return err
+ }
+ if node.User != nil {
+ users = append(users, node.User)
+ }
+ certFile, keyFile := node.Get("cert"), node.Get("key")
+ tlsCfg, err := tlsConfig(certFile, keyFile)
+ if err != nil && certFile != "" && keyFile != "" {
+ return err
+ }
+
+ wsOpts := &gost.WSOptions{}
+ wsOpts.EnableCompression = node.GetBool("compression")
+ wsOpts.ReadBufferSize = node.GetInt("rbuf")
+ wsOpts.WriteBufferSize = node.GetInt("wbuf")
+
+ var ln gost.Listener
+ switch node.Transport {
+ case "tls":
+ ln, err = gost.TLSListener(node.Addr, tlsCfg)
+ case "mtls":
+ ln, err = gost.MTLSListener(node.Addr, tlsCfg)
+ case "ws":
+ wsOpts.WriteBufferSize = node.GetInt("wbuf")
+ ln, err = gost.WSListener(node.Addr, wsOpts)
+ case "mws":
+ ln, err = gost.MWSListener(node.Addr, wsOpts)
+ case "wss":
+ ln, err = gost.WSSListener(node.Addr, tlsCfg, wsOpts)
+ case "mwss":
+ ln, err = gost.MWSSListener(node.Addr, tlsCfg, wsOpts)
+ case "kcp":
+ config, er := parseKCPConfig(node.Get("c"))
+ if er != nil {
+ return er
+ }
+ ln, err = gost.KCPListener(node.Addr, config)
+ case "ssh":
+ config := &gost.SSHConfig{
+ Users: users,
+ TLSConfig: tlsCfg,
+ }
+ if node.Protocol == "forward" {
+ ln, err = gost.TCPListener(node.Addr)
+ } else {
+ ln, err = gost.SSHTunnelListener(node.Addr, config)
+ }
+ case "quic":
+ config := &gost.QUICConfig{
+ TLSConfig: tlsCfg,
+ KeepAlive: node.GetBool("keepalive"),
+ Timeout: time.Duration(node.GetInt("timeout")) * time.Second,
+ IdleTimeout: time.Duration(node.GetInt("idle")) * time.Second,
+ }
+ if cipher := node.Get("cipher"); cipher != "" {
+ sum := sha256.Sum256([]byte(cipher))
+ config.Key = sum[:]
+ }
+
+ ln, err = gost.QUICListener(node.Addr, config)
+ case "http2":
+ ln, err = gost.HTTP2Listener(node.Addr, tlsCfg)
+ case "h2":
+ ln, err = gost.H2Listener(node.Addr, tlsCfg)
+ case "h2c":
+ ln, err = gost.H2CListener(node.Addr)
+ case "tcp":
+ // Directly use SSH port forwarding if the last chain node is forward+ssh
+ if chain.LastNode().Protocol == "forward" && chain.LastNode().Transport == "ssh" {
+ chain.Nodes()[len(chain.Nodes())-1].Client.Connector = gost.SSHDirectForwardConnector()
+ chain.Nodes()[len(chain.Nodes())-1].Client.Transporter = gost.SSHForwardTransporter()
+ }
+ ln, err = gost.TCPListener(node.Addr)
+ case "rtcp":
+ // Directly use SSH port forwarding if the last chain node is forward+ssh
+ if chain.LastNode().Protocol == "forward" && chain.LastNode().Transport == "ssh" {
+ chain.Nodes()[len(chain.Nodes())-1].Client.Connector = gost.SSHRemoteForwardConnector()
+ chain.Nodes()[len(chain.Nodes())-1].Client.Transporter = gost.SSHForwardTransporter()
+ }
+ ln, err = gost.TCPRemoteForwardListener(node.Addr, chain)
+ case "udp":
+ ln, err = gost.UDPDirectForwardListener(node.Addr, time.Duration(node.GetInt("ttl"))*time.Second)
+ case "rudp":
+ ln, err = gost.UDPRemoteForwardListener(node.Addr, chain, time.Duration(node.GetInt("ttl"))*time.Second)
+ case "ssu":
+ ln, err = gost.ShadowUDPListener(node.Addr, node.User, time.Duration(node.GetInt("ttl"))*time.Second)
+ case "obfs4":
+ if err = gost.Obfs4Init(node, true); err != nil {
+ return err
+ }
+ ln, err = gost.Obfs4Listener(node.Addr)
+ case "ohttp":
+ ln, err = gost.ObfsHTTPListener(node.Addr)
+ default:
+ ln, err = gost.TCPListener(node.Addr)
+ }
+ if err != nil {
+ return err
+ }
+
+ var handler gost.Handler
+ switch node.Protocol {
+ case "http2":
+ handler = gost.HTTP2Handler()
+ case "socks", "socks5":
+ handler = gost.SOCKS5Handler()
+ case "socks4", "socks4a":
+ handler = gost.SOCKS4Handler()
+ case "ss":
+ handler = gost.ShadowHandler()
+ case "http":
+ handler = gost.HTTPHandler()
+ case "tcp":
+ handler = gost.TCPDirectForwardHandler(node.Remote)
+ case "rtcp":
+ handler = gost.TCPRemoteForwardHandler(node.Remote)
+ case "udp":
+ handler = gost.UDPDirectForwardHandler(node.Remote)
+ case "rudp":
+ handler = gost.UDPRemoteForwardHandler(node.Remote)
+ case "forward":
+ handler = gost.SSHForwardHandler()
+ case "redirect":
+ handler = gost.TCPRedirectHandler()
+ case "ssu":
+ handler = gost.ShadowUDPdHandler()
+ case "sni":
+ handler = gost.SNIHandler()
+ default:
+ // start from 2.5, if remote is not empty, then we assume that it is a forward tunnel.
+ if node.Remote != "" {
+ handler = gost.TCPDirectForwardHandler(node.Remote)
+ } else {
+ handler = gost.AutoHandler()
+ }
+ }
+
+ var whitelist, blacklist *gost.Permissions
+ if node.Values.Get("whitelist") != "" {
+ if whitelist, err = gost.ParsePermissions(node.Get("whitelist")); err != nil {
+ return err
+ }
+ }
+ if node.Values.Get("blacklist") != "" {
+ if blacklist, err = gost.ParsePermissions(node.Get("blacklist")); err != nil {
+ return err
+ }
+ }
+
+ var hosts *gost.Hosts
+ if f, _ := os.Open(node.Get("hosts")); f != nil {
+ f.Close()
+ hosts = gost.NewHosts()
+ go gost.PeriodReload(hosts, node.Get("hosts"))
+ }
+
+ handler.Init(
+ gost.AddrHandlerOption(node.Addr),
+ gost.ChainHandlerOption(chain),
+ gost.UsersHandlerOption(users...),
+ gost.TLSConfigHandlerOption(tlsCfg),
+ gost.WhitelistHandlerOption(whitelist),
+ gost.BlacklistHandlerOption(blacklist),
+ gost.BypassHandlerOption(parseBypass(node.Get("bypass"))),
+ gost.StrategyHandlerOption(parseStrategy(node.Get("strategy"))),
+ gost.ResolverHandlerOption(parseResolver(node.Get("dns"))),
+ gost.HostsHandlerOption(hosts),
+ gost.RetryHandlerOption(node.GetInt("retry")),
+ gost.TimeoutHandlerOption(time.Duration(node.GetInt("timeout"))*time.Second),
+ gost.ProbeResistHandlerOption(node.Get("probe_resist")),
+ )
+
+ srv := &gost.Server{Listener: ln}
+ go srv.Serve(handler)
+ }
+
+ return nil
+}
diff --git a/settings.gradle b/settings.gradle
new file mode 100644
index 0000000..e7b4def
--- /dev/null
+++ b/settings.gradle
@@ -0,0 +1 @@
+include ':app'