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'