├── .gitignore ├── LICENSE ├── README.md ├── app ├── .gitignore ├── build.gradle ├── proguard-rules.pro └── src │ └── main │ ├── AndroidManifest.xml │ ├── java │ └── com │ │ └── vm │ │ └── shadowsocks │ │ ├── core │ │ ├── AppInfo.java │ │ ├── AppProxyManager.java │ │ ├── ChinaIpMaskManager.java │ │ ├── DnsProxy.java │ │ ├── HttpHostHeaderParser.java │ │ ├── LocalVpnService.java │ │ ├── NatSession.java │ │ ├── NatSessionManager.java │ │ ├── ProxyConfig.java │ │ ├── TcpProxyServer.java │ │ └── TunnelFactory.java │ │ ├── dns │ │ ├── DnsFlags.java │ │ ├── DnsHeader.java │ │ ├── DnsPacket.java │ │ ├── Question.java │ │ ├── Resource.java │ │ └── ResourcePointer.java │ │ ├── tcpip │ │ ├── CommonMethods.java │ │ ├── IPHeader.java │ │ ├── TCPHeader.java │ │ └── UDPHeader.java │ │ ├── tunnel │ │ ├── Config.java │ │ ├── RawTunnel.java │ │ ├── Tunnel.java │ │ ├── httpconnect │ │ │ ├── HttpConnectConfig.java │ │ │ └── HttpConnectTunnel.java │ │ └── shadowsocks │ │ │ ├── AesCrypt.java │ │ │ ├── BlowFishCrypt.java │ │ │ ├── CamelliaCrypt.java │ │ │ ├── Chacha20Crypt.java │ │ │ ├── CryptBase.java │ │ │ ├── CryptFactory.java │ │ │ ├── ICrypt.java │ │ │ ├── Rc4Md5Crypt.java │ │ │ ├── SeedCrypt.java │ │ │ ├── ShadowSocksKey.java │ │ │ ├── ShadowsocksConfig.java │ │ │ └── ShadowsocksTunnel.java │ │ └── ui │ │ ├── AppManager.java │ │ └── MainActivity.java │ └── res │ ├── drawable-xxhdpi │ └── shadowsocks.png │ ├── layout │ ├── activity_main.xml │ ├── layout_apps.xml │ └── layout_apps_item.xml │ ├── menu │ └── main_activity_actions.xml │ ├── raw │ ├── config │ └── ipmask │ ├── values-sw600dp │ └── dimens.xml │ ├── values-sw720dp-land │ └── dimens.xml │ ├── values-v11 │ └── styles.xml │ ├── values-v14 │ └── styles.xml │ ├── values-v21 │ └── styles.xml │ ├── values-zh-rCN │ └── strings.xml │ └── values │ ├── dimens.xml │ ├── strings.xml │ └── styles.xml ├── build.gradle ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat └── settings.gradle /.gitignore: -------------------------------------------------------------------------------- 1 | # Built application files 2 | *.apk 3 | *.ap_ 4 | 5 | 6 | # Files for the ART/Dalvik VM 7 | *.dex 8 | 9 | # Java class files 10 | *.class 11 | 12 | # Generated files 13 | bin/ 14 | gen/ 15 | out/ 16 | 17 | # Gradle files 18 | .gradle/ 19 | build/ 20 | 21 | # Local configuration file (sdk path, etc) 22 | local.properties 23 | 24 | # Proguard folder generated by Eclipse 25 | proguard/ 26 | 27 | # Log Files 28 | *.log 29 | 30 | # Android Studio Navigation editor temp files 31 | .navigation/ 32 | 33 | # Android Studio captures folder 34 | captures/ 35 | 36 | # Intellij 37 | *.iml 38 | .idea/workspace.xml 39 | 40 | # Keystore files 41 | *.jks 42 | 43 | 44 | 45 | 46 | ######### MAC 47 | *.DS_Store 48 | .AppleDouble 49 | .LSOverride 50 | 51 | # Icon must end with two \r 52 | Icon 53 | 54 | 55 | # Thumbnails 56 | ._* 57 | 58 | # Files that might appear in the root of a volume 59 | .DocumentRevisions-V100 60 | .fseventsd 61 | .Spotlight-V100 62 | .TemporaryItems 63 | .Trashes 64 | .VolumeIcon.icns 65 | .com.apple.timemachine.donotpresent 66 | 67 | # Directories potentially created on remote AFP share 68 | .AppleDB 69 | .AppleDesktop 70 | Network Trash Folder 71 | Temporary Items 72 | .apdisk 73 | 74 | 75 | ######### Windows 76 | # Windows image file caches 77 | Thumbs.db 78 | ehthumbs.db 79 | 80 | # Folder config file 81 | Desktop.ini 82 | 83 | # Recycle Bin used on file shares 84 | $RECYCLE.BIN/ 85 | 86 | # Windows Installer files 87 | *.cab 88 | *.msi 89 | *.msm 90 | *.msp 91 | 92 | # Windows shortcuts 93 | *.lnk 94 | 95 | 96 | ######### linux 97 | *~ 98 | 99 | # temporary files which can be created if a process still has a handle open of a deleted file 100 | .fuse_hidden* 101 | 102 | # KDE directory preferences 103 | .directory 104 | 105 | # Linux trash folder which might appear on any partition or disk 106 | .Trash-* 107 | 108 | .idea/ 109 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright {yyyy} {name of copyright owner} 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # shadowsocks-android-java 2 | 3 | Last release: [Download](https://github.com/dawei101/shadowsocks-android-java/releases) 4 | 5 | 6 | This version of shadowsocks for android is pure java version. 7 | Beacuse of the way of implementation, some feature would not work (icmp protocol, and correct dns result under command line) 8 | 9 | Most code is merged from [smartproxy](https://github.com/hedaode/SmartProxy) and [shadowsocks-java](https://github.com/blakey22/shadowsocks-java) 10 | This app inherit Smartproxy's feature: tiny/low power cost/simple operation 11 | 12 | 13 | shadowsocks settings format 14 | 15 | ``` 16 | ss://method:password@host:port 17 | ss://base64encode(method:password@host:port) 18 | ``` 19 | 20 | And also it inherited the support of http proxy from Smartproxy , Set the url as stardand http(s) proxy format when use it. 21 | 22 | http proxy foramt: 23 | 24 | ``` 25 | http://(username:passsword)@host:port 26 | ``` 27 | Support methods of encryption: 28 | 29 | ``` 30 | bf-cfb 31 | seed-cfb 32 | aes-128-cfb 33 | aes-192-cfb 34 | aes-256-cfb 35 | aes-128-ofb 36 | aes-192-ofb 37 | aes-256-ofb 38 | camellia-128-cfb 39 | camellia-192-cfb 40 | camellia-256-cfb 41 | chacha20 42 | chacha20-ietf 43 | rc4-md5 44 | ``` 45 | 46 | #### Brother version 47 | 48 | [Shadowsocks android(Scala)](https://github.com/shadowsocks/shadowsocks-android) 49 | 50 | Scala version is high threshold to lots of developer, so it's a better choice to choose this version. 51 | 52 | ----------- 53 | 54 | ### 关于 About 55 | 56 | 本版本为shadowsocks android版的纯java版本 57 | 因为实现原理的缘故,会牺牲掉一些功能(主要是用到icmp协议,以及在命令行下的dns解析的正确地址). 58 | 59 | 代码多整理自 [smartproxy](https://github.com/hedaode/SmartProxy) 和 [shadowsocks-java](https://github.com/blakey22/shadowsocks-java) 60 | 本shadowsocks-android的特点继承了SmartProxy的优点: 体积小,耗电低,设置保持最简单的方式 61 | shadowsocks设置格式: 62 | ``` 63 | ss://method:password@host:port 64 | ss://base64encode(method:password@host:port) 65 | ``` 66 | 67 | 其中代码保留了SmartProxy对http代理的支持, 使用时将配置链接填写标准http代理格式即可. 68 | http代理格式 69 | 70 | ``` 71 | http://(username:passsword)@host:port 72 | ``` 73 | 支持的加密类型: 74 | 75 | ``` 76 | bf-cfb 77 | seed-cfb 78 | aes-128-cfb 79 | aes-192-cfb 80 | aes-256-cfb 81 | aes-128-ofb 82 | aes-192-ofb 83 | aes-256-ofb 84 | camellia-128-cfb 85 | camellia-192-cfb 86 | camellia-256-cfb 87 | chacha20 88 | chacha20-ietf 89 | rc4-md5 90 | ``` 91 | 92 | ### 兄弟版本 93 | Brother version 94 | 95 | ##### [Shadowsocks android(Scala)](https://github.com/shadowsocks/shadowsocks-android) 96 | 97 | ### 作者同系列版本 98 | [shadowsocks 桌面版,一份代码完美支持windows,mac osx,linux](https://github.com/dawei101/tongsheClient.shadowsocks-go) 99 | 100 | 101 | #### LICENSE 102 | 103 | [Apache License](./LICENSE) 104 | 105 | 106 | 107 | -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /app/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.application' 2 | 3 | 4 | buildscript { 5 | repositories { 6 | mavenCentral() 7 | } 8 | dependencies { 9 | classpath 'com.android.tools.build:gradle:2.3.3' 10 | } 11 | } 12 | 13 | android { 14 | compileSdkVersion 26 15 | buildToolsVersion '26.0.1' 16 | useLibrary 'org.apache.http.legacy' 17 | 18 | defaultConfig { 19 | applicationId "com.vm.shadowsocks" 20 | minSdkVersion 15 21 | targetSdkVersion 26 22 | versionCode 1 23 | versionName "1.2" 24 | } 25 | buildTypes { 26 | release { 27 | minifyEnabled true 28 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 29 | } 30 | } 31 | } 32 | 33 | dependencies { 34 | compile fileTree(include: ['*.jar'], dir: 'libs') 35 | 36 | testCompile 'junit:junit:4.12' 37 | 38 | compile 'com.journeyapps:zxing-android-embedded:3.5.0' 39 | compile 'com.google.zxing:core:3.0.1' 40 | 41 | compile 'org.bouncycastle:bcprov-jdk15on:1.57' 42 | compile 'com.futuremind.recyclerfastscroll:fastscroll:0.2.5' 43 | compile 'io.reactivex.rxjava2:rxandroid:2.0.1' 44 | } 45 | -------------------------------------------------------------------------------- /app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # By default, the flags in this file are appended to flags specified 3 | # in /Development/adt-bundle/sdk/tools/proguard/proguard-android.txt 4 | # You can edit the include path and order by changing the proguardFiles 5 | # directive in build.gradle. 6 | # 7 | # For more details, see 8 | # http://developer.android.com/guide/developing/tools/proguard.html 9 | 10 | # Add any project specific keep options here: 11 | 12 | # If your project uses WebView with JS, uncomment the following 13 | # and specify the fully qualified class name to the JavaScript interface 14 | # class: 15 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 16 | # public *; 17 | #} 18 | -keep class com.journeyapps.barcodescanner.** { *; } 19 | -dontwarn org.bouncycastle.** -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | 9 | 15 | 16 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 31 | 32 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /app/src/main/java/com/vm/shadowsocks/core/AppInfo.java: -------------------------------------------------------------------------------- 1 | package com.vm.shadowsocks.core; 2 | 3 | import android.graphics.drawable.Drawable; 4 | 5 | public class AppInfo { 6 | private Drawable appIcon; 7 | private String appLabel; 8 | private String pkgName; 9 | 10 | public AppInfo() { 11 | } 12 | 13 | public Drawable getAppIcon() { 14 | return this.appIcon; 15 | } 16 | 17 | public String getAppLabel() { 18 | return this.appLabel; 19 | } 20 | 21 | public String getPkgName() { 22 | return this.pkgName; 23 | } 24 | 25 | public void setAppIcon(Drawable var1) { 26 | this.appIcon = var1; 27 | } 28 | 29 | public void setAppLabel(String var1) { 30 | this.appLabel = var1; 31 | } 32 | 33 | public void setPkgName(String var1) { 34 | this.pkgName = var1; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /app/src/main/java/com/vm/shadowsocks/core/AppProxyManager.java: -------------------------------------------------------------------------------- 1 | package com.vm.shadowsocks.core; 2 | 3 | import android.content.Context; 4 | import android.content.SharedPreferences; 5 | import android.graphics.drawable.Drawable; 6 | import android.os.Build; 7 | 8 | import org.json.JSONArray; 9 | import org.json.JSONObject; 10 | 11 | import java.util.ArrayList; 12 | import java.util.List; 13 | 14 | import static android.content.Context.MODE_PRIVATE; 15 | 16 | public class AppProxyManager { 17 | public static boolean isLollipopOrAbove = Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP; 18 | 19 | public static AppProxyManager Instance; 20 | private static final String PROXY_APPS = "PROXY_APPS"; 21 | private Context mContext; 22 | 23 | public List mlistAppInfo = new ArrayList(); 24 | public List proxyAppInfo = new ArrayList(); 25 | 26 | public AppProxyManager(Context context){ 27 | this.mContext = context; 28 | Instance = this; 29 | readProxyAppsList(); 30 | } 31 | 32 | public void removeProxyApp(String pkg){ 33 | for (AppInfo app : this.proxyAppInfo) { 34 | if (app.getPkgName().equals(pkg)){ 35 | proxyAppInfo.remove(app); 36 | break; 37 | } 38 | } 39 | writeProxyAppsList(); 40 | } 41 | 42 | public void addProxyApp(String pkg){ 43 | for (AppInfo app : this.mlistAppInfo) { 44 | if (app.getPkgName().equals(pkg)){ 45 | proxyAppInfo.add(app); 46 | break; 47 | } 48 | } 49 | writeProxyAppsList(); 50 | } 51 | 52 | public boolean isAppProxy(String pkg){ 53 | for (AppInfo app : this.proxyAppInfo) { 54 | if (app.getPkgName().equals(pkg)){ 55 | return true; 56 | } 57 | } 58 | return false; 59 | } 60 | 61 | private void readProxyAppsList() { 62 | SharedPreferences preferences = mContext.getSharedPreferences("shadowsocksProxyUrl", MODE_PRIVATE); 63 | String tmpString = preferences.getString(PROXY_APPS, ""); 64 | try { 65 | if (proxyAppInfo != null){ 66 | proxyAppInfo.clear(); 67 | } 68 | if (tmpString.isEmpty()){ 69 | return; 70 | } 71 | JSONArray jsonArray = new JSONArray(tmpString); 72 | for (int i = 0; i < jsonArray.length() ; i++){ 73 | JSONObject object = jsonArray.getJSONObject(i); 74 | AppInfo appInfo = new AppInfo(); 75 | appInfo.setAppLabel(object.getString("label")); 76 | appInfo.setPkgName(object.getString("pkg")); 77 | proxyAppInfo.add(appInfo); 78 | } 79 | } catch (Exception e){ 80 | e.printStackTrace(); 81 | } 82 | } 83 | 84 | private void writeProxyAppsList() { 85 | SharedPreferences preferences = mContext.getSharedPreferences("shadowsocksProxyUrl", MODE_PRIVATE); 86 | try { 87 | JSONArray jsonArray = new JSONArray(); 88 | for (int i = 0; i < proxyAppInfo.size() ; i++){ 89 | JSONObject object = new JSONObject(); 90 | AppInfo appInfo = proxyAppInfo.get(i); 91 | object.put("label", appInfo.getAppLabel()); 92 | object.put("pkg", appInfo.getPkgName()); 93 | jsonArray.put(object); 94 | } 95 | SharedPreferences.Editor editor = preferences.edit(); 96 | editor.putString(PROXY_APPS, jsonArray.toString()); 97 | editor.apply(); 98 | } catch (Exception e){ 99 | e.printStackTrace(); 100 | } 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /app/src/main/java/com/vm/shadowsocks/core/ChinaIpMaskManager.java: -------------------------------------------------------------------------------- 1 | package com.vm.shadowsocks.core; 2 | 3 | import android.util.SparseIntArray; 4 | 5 | import com.vm.shadowsocks.tcpip.CommonMethods; 6 | 7 | import java.io.IOException; 8 | import java.io.InputStream; 9 | 10 | 11 | public class ChinaIpMaskManager { 12 | 13 | static SparseIntArray ChinaIpMaskDict = new SparseIntArray(3000); 14 | static SparseIntArray MaskDict = new SparseIntArray(); 15 | 16 | public static boolean isIPInChina(int ip) { 17 | boolean found = false; 18 | for (int i = 0; i < MaskDict.size(); i++) { 19 | int mask = MaskDict.keyAt(i); 20 | int networkIP = ip & mask; 21 | int mask2 = ChinaIpMaskDict.get(networkIP); 22 | if (mask2 == mask) { 23 | found = true; 24 | break; 25 | } 26 | } 27 | return found; 28 | } 29 | 30 | public static void loadFromFile(InputStream inputStream) { 31 | int count = 0; 32 | try { 33 | byte[] buffer = new byte[4096]; 34 | while ((count = inputStream.read(buffer)) > 0) { 35 | for (int i = 0; i < count; i += 8) { 36 | int ip = CommonMethods.readInt(buffer, i); 37 | int mask = CommonMethods.readInt(buffer, i + 4); 38 | ChinaIpMaskDict.put(ip, mask); 39 | MaskDict.put(mask, mask); 40 | //System.out.printf("%s/%s\n", CommonMethods.IP2String(ip),CommonMethods.IP2String(mask)); 41 | } 42 | } 43 | inputStream.close(); 44 | System.out.printf("ChinaIpMask records count: %d\n", ChinaIpMaskDict.size()); 45 | } catch (IOException e) { 46 | // TODO Auto-generated catch block 47 | e.printStackTrace(); 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /app/src/main/java/com/vm/shadowsocks/core/DnsProxy.java: -------------------------------------------------------------------------------- 1 | package com.vm.shadowsocks.core; 2 | 3 | import android.util.SparseArray; 4 | 5 | import com.vm.shadowsocks.dns.DnsPacket; 6 | import com.vm.shadowsocks.dns.Question; 7 | import com.vm.shadowsocks.dns.Resource; 8 | import com.vm.shadowsocks.dns.ResourcePointer; 9 | import com.vm.shadowsocks.tcpip.CommonMethods; 10 | import com.vm.shadowsocks.tcpip.IPHeader; 11 | import com.vm.shadowsocks.tcpip.UDPHeader; 12 | 13 | import java.io.IOException; 14 | import java.net.DatagramPacket; 15 | import java.net.DatagramSocket; 16 | import java.net.InetSocketAddress; 17 | import java.nio.ByteBuffer; 18 | import java.util.concurrent.ConcurrentHashMap; 19 | 20 | 21 | public class DnsProxy implements Runnable { 22 | 23 | private class QueryState { 24 | public short ClientQueryID; 25 | public long QueryNanoTime; 26 | public int ClientIP; 27 | public short ClientPort; 28 | public int RemoteIP; 29 | public short RemotePort; 30 | } 31 | 32 | public boolean Stopped; 33 | private static final ConcurrentHashMap IPDomainMaps = new ConcurrentHashMap(); 34 | private static final ConcurrentHashMap DomainIPMaps = new ConcurrentHashMap(); 35 | private final long QUERY_TIMEOUT_NS = 10 * 1000000000L; 36 | private DatagramSocket m_Client; 37 | private Thread m_ReceivedThread; 38 | private short m_QueryID; 39 | private SparseArray m_QueryArray; 40 | 41 | public DnsProxy() throws IOException { 42 | m_QueryArray = new SparseArray(); 43 | m_Client = new DatagramSocket(0); 44 | } 45 | 46 | public static String reverseLookup(int ip) { 47 | return IPDomainMaps.get(ip); 48 | } 49 | 50 | public void start() { 51 | m_ReceivedThread = new Thread(this); 52 | m_ReceivedThread.setName("DnsProxyThread"); 53 | m_ReceivedThread.start(); 54 | } 55 | 56 | public void stop() { 57 | Stopped = true; 58 | if (m_Client != null) { 59 | m_Client.close(); 60 | m_Client = null; 61 | } 62 | } 63 | 64 | @Override 65 | public void run() { 66 | try { 67 | byte[] RECEIVE_BUFFER = new byte[2000]; 68 | IPHeader ipHeader = new IPHeader(RECEIVE_BUFFER, 0); 69 | ipHeader.Default(); 70 | UDPHeader udpHeader = new UDPHeader(RECEIVE_BUFFER, 20); 71 | 72 | ByteBuffer dnsBuffer = ByteBuffer.wrap(RECEIVE_BUFFER); 73 | dnsBuffer.position(28); 74 | dnsBuffer = dnsBuffer.slice(); 75 | 76 | DatagramPacket packet = new DatagramPacket(RECEIVE_BUFFER, 28, RECEIVE_BUFFER.length - 28); 77 | 78 | while (m_Client != null && !m_Client.isClosed()) { 79 | 80 | packet.setLength(RECEIVE_BUFFER.length - 28); 81 | m_Client.receive(packet); 82 | 83 | dnsBuffer.clear(); 84 | dnsBuffer.limit(packet.getLength()); 85 | try { 86 | DnsPacket dnsPacket = DnsPacket.FromBytes(dnsBuffer); 87 | if (dnsPacket != null) { 88 | OnDnsResponseReceived(ipHeader, udpHeader, dnsPacket); 89 | } 90 | } catch (Exception e) { 91 | e.printStackTrace(); 92 | LocalVpnService.Instance.writeLog("Parse dns error: %s", e); 93 | } 94 | } 95 | } catch (Exception e) { 96 | e.printStackTrace(); 97 | } finally { 98 | System.out.println("DnsResolver Thread Exited."); 99 | this.stop(); 100 | } 101 | } 102 | 103 | private int getFirstIP(DnsPacket dnsPacket) { 104 | for (int i = 0; i < dnsPacket.Header.ResourceCount; i++) { 105 | Resource resource = dnsPacket.Resources[i]; 106 | if (resource.Type == 1) { 107 | int ip = CommonMethods.readInt(resource.Data, 0); 108 | return ip; 109 | } 110 | } 111 | return 0; 112 | } 113 | 114 | private void tamperDnsResponse(byte[] rawPacket, DnsPacket dnsPacket, int newIP) { 115 | Question question = dnsPacket.Questions[0]; 116 | 117 | dnsPacket.Header.setResourceCount((short) 1); 118 | dnsPacket.Header.setAResourceCount((short) 0); 119 | dnsPacket.Header.setEResourceCount((short) 0); 120 | 121 | ResourcePointer rPointer = new ResourcePointer(rawPacket, question.Offset() + question.Length()); 122 | rPointer.setDomain((short) 0xC00C); 123 | rPointer.setType(question.Type); 124 | rPointer.setClass(question.Class); 125 | rPointer.setTTL(ProxyConfig.Instance.getDnsTTL()); 126 | rPointer.setDataLength((short) 4); 127 | rPointer.setIP(newIP); 128 | 129 | dnsPacket.Size = 12 + question.Length() + 16; 130 | } 131 | 132 | private int getOrCreateFakeIP(String domainString) { 133 | Integer fakeIP = DomainIPMaps.get(domainString); 134 | if (fakeIP == null) { 135 | int hashIP = domainString.hashCode(); 136 | do { 137 | fakeIP = ProxyConfig.FAKE_NETWORK_IP | (hashIP & 0x0000FFFF); 138 | hashIP++; 139 | } while (IPDomainMaps.containsKey(fakeIP)); 140 | 141 | DomainIPMaps.put(domainString, fakeIP); 142 | IPDomainMaps.put(fakeIP, domainString); 143 | } 144 | return fakeIP; 145 | } 146 | 147 | private boolean dnsPollution(byte[] rawPacket, DnsPacket dnsPacket) { 148 | if (dnsPacket.Header.QuestionCount > 0) { 149 | Question question = dnsPacket.Questions[0]; 150 | if (question.Type == 1) { 151 | int realIP = getFirstIP(dnsPacket); 152 | if (ProxyConfig.Instance.needProxy(question.Domain, realIP)) { 153 | int fakeIP = getOrCreateFakeIP(question.Domain); 154 | tamperDnsResponse(rawPacket, dnsPacket, fakeIP); 155 | if (ProxyConfig.IS_DEBUG) 156 | System.out.printf("FakeDns: %s=>%s(%s)\n", question.Domain, CommonMethods.ipIntToString(realIP), CommonMethods.ipIntToString(fakeIP)); 157 | return true; 158 | } 159 | } 160 | } 161 | return false; 162 | } 163 | 164 | private void OnDnsResponseReceived(IPHeader ipHeader, UDPHeader udpHeader, DnsPacket dnsPacket) { 165 | QueryState state = null; 166 | synchronized (m_QueryArray) { 167 | state = m_QueryArray.get(dnsPacket.Header.ID); 168 | if (state != null) { 169 | m_QueryArray.remove(dnsPacket.Header.ID); 170 | } 171 | } 172 | 173 | if (state != null) { 174 | //DNS污染,默认污染海外网站 175 | dnsPollution(udpHeader.m_Data, dnsPacket); 176 | 177 | dnsPacket.Header.setID(state.ClientQueryID); 178 | ipHeader.setSourceIP(state.RemoteIP); 179 | ipHeader.setDestinationIP(state.ClientIP); 180 | ipHeader.setProtocol(IPHeader.UDP); 181 | ipHeader.setTotalLength(20 + 8 + dnsPacket.Size); 182 | udpHeader.setSourcePort(state.RemotePort); 183 | udpHeader.setDestinationPort(state.ClientPort); 184 | udpHeader.setTotalLength(8 + dnsPacket.Size); 185 | 186 | LocalVpnService.Instance.sendUDPPacket(ipHeader, udpHeader); 187 | } 188 | } 189 | 190 | private int getIPFromCache(String domain) { 191 | Integer ip = DomainIPMaps.get(domain); 192 | if (ip == null) { 193 | return 0; 194 | } else { 195 | return ip; 196 | } 197 | } 198 | 199 | private boolean interceptDns(IPHeader ipHeader, UDPHeader udpHeader, DnsPacket dnsPacket) { 200 | Question question = dnsPacket.Questions[0]; 201 | System.out.println("DNS Qeury " + question.Domain); 202 | if (question.Type == 1) { 203 | if (ProxyConfig.Instance.needProxy(question.Domain, getIPFromCache(question.Domain))) { 204 | int fakeIP = getOrCreateFakeIP(question.Domain); 205 | tamperDnsResponse(ipHeader.m_Data, dnsPacket, fakeIP); 206 | 207 | if (ProxyConfig.IS_DEBUG) 208 | System.out.printf("interceptDns FakeDns: %s=>%s\n", question.Domain, CommonMethods.ipIntToString(fakeIP)); 209 | 210 | int sourceIP = ipHeader.getSourceIP(); 211 | short sourcePort = udpHeader.getSourcePort(); 212 | ipHeader.setSourceIP(ipHeader.getDestinationIP()); 213 | ipHeader.setDestinationIP(sourceIP); 214 | ipHeader.setTotalLength(20 + 8 + dnsPacket.Size); 215 | udpHeader.setSourcePort(udpHeader.getDestinationPort()); 216 | udpHeader.setDestinationPort(sourcePort); 217 | udpHeader.setTotalLength(8 + dnsPacket.Size); 218 | LocalVpnService.Instance.sendUDPPacket(ipHeader, udpHeader); 219 | return true; 220 | } 221 | } 222 | return false; 223 | } 224 | 225 | private void clearExpiredQueries() { 226 | long now = System.nanoTime(); 227 | for (int i = m_QueryArray.size() - 1; i >= 0; i--) { 228 | QueryState state = m_QueryArray.valueAt(i); 229 | if ((now - state.QueryNanoTime) > QUERY_TIMEOUT_NS) { 230 | m_QueryArray.removeAt(i); 231 | } 232 | } 233 | } 234 | 235 | public void onDnsRequestReceived(IPHeader ipHeader, UDPHeader udpHeader, DnsPacket dnsPacket) { 236 | if (!interceptDns(ipHeader, udpHeader, dnsPacket)) { 237 | //转发DNS 238 | QueryState state = new QueryState(); 239 | state.ClientQueryID = dnsPacket.Header.ID; 240 | state.QueryNanoTime = System.nanoTime(); 241 | state.ClientIP = ipHeader.getSourceIP(); 242 | state.ClientPort = udpHeader.getSourcePort(); 243 | state.RemoteIP = ipHeader.getDestinationIP(); 244 | state.RemotePort = udpHeader.getDestinationPort(); 245 | 246 | // 转换QueryID 247 | m_QueryID++;// 增加ID 248 | dnsPacket.Header.setID(m_QueryID); 249 | 250 | synchronized (m_QueryArray) { 251 | clearExpiredQueries();//清空过期的查询,减少内存开销。 252 | m_QueryArray.put(m_QueryID, state);// 关联数据 253 | } 254 | 255 | InetSocketAddress remoteAddress = new InetSocketAddress(CommonMethods.ipIntToInet4Address(state.RemoteIP), state.RemotePort); 256 | DatagramPacket packet = new DatagramPacket(udpHeader.m_Data, udpHeader.m_Offset + 8, dnsPacket.Size); 257 | packet.setSocketAddress(remoteAddress); 258 | 259 | try { 260 | if (LocalVpnService.Instance.protect(m_Client)) { 261 | m_Client.send(packet); 262 | } else { 263 | System.err.println("VPN protect udp socket failed."); 264 | } 265 | } catch (IOException e) { 266 | // TODO Auto-generated catch block 267 | e.printStackTrace(); 268 | } 269 | } 270 | } 271 | } 272 | -------------------------------------------------------------------------------- /app/src/main/java/com/vm/shadowsocks/core/HttpHostHeaderParser.java: -------------------------------------------------------------------------------- 1 | package com.vm.shadowsocks.core; 2 | 3 | import com.vm.shadowsocks.tcpip.CommonMethods; 4 | 5 | import java.util.Locale; 6 | 7 | 8 | public class HttpHostHeaderParser { 9 | 10 | public static String parseHost(byte[] buffer, int offset, int count) { 11 | try { 12 | switch (buffer[offset]) { 13 | case 'G'://GET 14 | case 'H'://HEAD 15 | case 'P'://POST,PUT 16 | case 'D'://DELETE 17 | case 'O'://OPTIONS 18 | case 'T'://TRACE 19 | case 'C'://CONNECT 20 | return getHttpHost(buffer, offset, count); 21 | case 0x16://SSL 22 | return getSNI(buffer, offset, count); 23 | } 24 | } catch (Exception e) { 25 | e.printStackTrace(); 26 | LocalVpnService.Instance.writeLog("Error: parseHost:%s", e); 27 | } 28 | return null; 29 | } 30 | 31 | static String getHttpHost(byte[] buffer, int offset, int count) { 32 | String headerString = new String(buffer, offset, count); 33 | String[] headerLines = headerString.split("\\r\\n"); 34 | String requestLine = headerLines[0]; 35 | if (requestLine.startsWith("GET") || requestLine.startsWith("POST") || requestLine.startsWith("HEAD") || requestLine.startsWith("OPTIONS")) { 36 | for (int i = 1; i < headerLines.length; i++) { 37 | String[] nameValueStrings = headerLines[i].split(":"); 38 | if (nameValueStrings.length == 2) { 39 | String name = nameValueStrings[0].toLowerCase(Locale.ENGLISH).trim(); 40 | String value = nameValueStrings[1].trim(); 41 | if ("host".equals(name)) { 42 | return value; 43 | } 44 | } 45 | } 46 | } 47 | return null; 48 | } 49 | 50 | static String getSNI(byte[] buffer, int offset, int count) { 51 | int limit = offset + count; 52 | if (count > 43 && buffer[offset] == 0x16) {//TLS Client Hello 53 | offset += 43;//skip 43 bytes header 54 | 55 | //read sessionID: 56 | if (offset + 1 > limit) return null; 57 | int sessionIDLength = buffer[offset++] & 0xFF; 58 | offset += sessionIDLength; 59 | 60 | //read cipher suites: 61 | if (offset + 2 > limit) return null; 62 | int cipherSuitesLength = CommonMethods.readShort(buffer, offset) & 0xFFFF; 63 | offset += 2; 64 | offset += cipherSuitesLength; 65 | 66 | //read Compression method: 67 | if (offset + 1 > limit) return null; 68 | int compressionMethodLength = buffer[offset++] & 0xFF; 69 | offset += compressionMethodLength; 70 | 71 | if (offset == limit) { 72 | System.err.println("TLS Client Hello packet doesn't contains SNI info.(offset == limit)"); 73 | return null; 74 | } 75 | 76 | //read Extensions: 77 | if (offset + 2 > limit) return null; 78 | int extensionsLength = CommonMethods.readShort(buffer, offset) & 0xFFFF; 79 | offset += 2; 80 | 81 | if (offset + extensionsLength > limit) { 82 | System.err.println("TLS Client Hello packet is incomplete."); 83 | return null; 84 | } 85 | 86 | while (offset + 4 <= limit) { 87 | int type0 = buffer[offset++] & 0xFF; 88 | int type1 = buffer[offset++] & 0xFF; 89 | int length = CommonMethods.readShort(buffer, offset) & 0xFFFF; 90 | offset += 2; 91 | 92 | if (type0 == 0x00 && type1 == 0x00 && length > 5) { //have SNI 93 | offset += 5;//skip SNI header. 94 | length -= 5;//SNI size; 95 | if (offset + length > limit) return null; 96 | String serverName = new String(buffer, offset, length); 97 | if (ProxyConfig.IS_DEBUG) 98 | System.out.printf("SNI: %s\n", serverName); 99 | return serverName; 100 | } else { 101 | offset += length; 102 | } 103 | } 104 | 105 | System.err.println("TLS Client Hello packet doesn't contains Host field info."); 106 | return null; 107 | } else { 108 | System.err.println("Bad TLS Client Hello packet."); 109 | return null; 110 | } 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /app/src/main/java/com/vm/shadowsocks/core/NatSession.java: -------------------------------------------------------------------------------- 1 | package com.vm.shadowsocks.core; 2 | 3 | public class NatSession { 4 | public int RemoteIP; 5 | public short RemotePort; 6 | public String RemoteHost; 7 | public int BytesSent; 8 | public int PacketSent; 9 | public long LastNanoTime; 10 | } 11 | -------------------------------------------------------------------------------- /app/src/main/java/com/vm/shadowsocks/core/NatSessionManager.java: -------------------------------------------------------------------------------- 1 | package com.vm.shadowsocks.core; 2 | 3 | import android.util.SparseArray; 4 | 5 | import com.vm.shadowsocks.tcpip.CommonMethods; 6 | 7 | public class NatSessionManager { 8 | 9 | static final int MAX_SESSION_COUNT = 60; 10 | static final long SESSION_TIMEOUT_NS = 60 * 1000000000L; 11 | static final SparseArray Sessions = new SparseArray(); 12 | 13 | public static NatSession getSession(int portKey) { 14 | NatSession session = Sessions.get(portKey); 15 | if (session!=null) { 16 | session.LastNanoTime = System.nanoTime(); 17 | } 18 | return Sessions.get(portKey); 19 | } 20 | 21 | public static int getSessionCount() { 22 | return Sessions.size(); 23 | } 24 | 25 | static void clearExpiredSessions() { 26 | long now = System.nanoTime(); 27 | for (int i = Sessions.size() - 1; i >= 0; i--) { 28 | NatSession session = Sessions.valueAt(i); 29 | if (now - session.LastNanoTime > SESSION_TIMEOUT_NS) { 30 | Sessions.removeAt(i); 31 | } 32 | } 33 | } 34 | 35 | public static NatSession createSession(int portKey, int remoteIP, short remotePort) { 36 | if (Sessions.size() > MAX_SESSION_COUNT) { 37 | clearExpiredSessions();//清理过期的会话。 38 | } 39 | 40 | NatSession session = new NatSession(); 41 | session.LastNanoTime = System.nanoTime(); 42 | session.RemoteIP = remoteIP; 43 | session.RemotePort = remotePort; 44 | 45 | if (ProxyConfig.isFakeIP(remoteIP)) { 46 | session.RemoteHost = DnsProxy.reverseLookup(remoteIP); 47 | } 48 | 49 | if (session.RemoteHost == null) { 50 | session.RemoteHost = CommonMethods.ipIntToString(remoteIP); 51 | } 52 | Sessions.put(portKey, session); 53 | return session; 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /app/src/main/java/com/vm/shadowsocks/core/ProxyConfig.java: -------------------------------------------------------------------------------- 1 | package com.vm.shadowsocks.core; 2 | 3 | import android.annotation.SuppressLint; 4 | import android.os.Build; 5 | 6 | import com.vm.shadowsocks.tcpip.CommonMethods; 7 | import com.vm.shadowsocks.tunnel.Config; 8 | import com.vm.shadowsocks.tunnel.httpconnect.HttpConnectConfig; 9 | import com.vm.shadowsocks.tunnel.shadowsocks.ShadowsocksConfig; 10 | 11 | import org.apache.http.HttpResponse; 12 | import org.apache.http.client.HttpClient; 13 | import org.apache.http.client.methods.HttpGet; 14 | import org.apache.http.impl.client.DefaultHttpClient; 15 | import org.apache.http.util.EntityUtils; 16 | 17 | import java.io.FileInputStream; 18 | import java.io.InputStream; 19 | import java.net.InetAddress; 20 | import java.net.InetSocketAddress; 21 | import java.util.ArrayList; 22 | import java.util.HashMap; 23 | import java.util.Locale; 24 | import java.util.Timer; 25 | import java.util.TimerTask; 26 | import java.util.regex.Matcher; 27 | import java.util.regex.Pattern; 28 | 29 | 30 | public class ProxyConfig { 31 | public static final ProxyConfig Instance = new ProxyConfig(); 32 | public final static boolean IS_DEBUG = false; 33 | public static String AppInstallID; 34 | public static String AppVersion; 35 | public final static int FAKE_NETWORK_MASK = CommonMethods.ipStringToInt("255.255.0.0"); 36 | public final static int FAKE_NETWORK_IP = CommonMethods.ipStringToInt("172.25.0.0"); 37 | 38 | ArrayList m_IpList; 39 | ArrayList m_DnsList; 40 | ArrayList m_RouteList; 41 | public ArrayList m_ProxyList; 42 | HashMap m_DomainMap; 43 | 44 | public boolean globalMode = false; 45 | 46 | int m_dns_ttl; 47 | String m_welcome_info; 48 | String m_session_name; 49 | String m_user_agent; 50 | boolean m_outside_china_use_proxy = true; 51 | boolean m_isolate_http_host_header = true; 52 | int m_mtu; 53 | 54 | Timer m_Timer; 55 | 56 | public class IPAddress { 57 | public final String Address; 58 | public final int PrefixLength; 59 | 60 | public IPAddress(String address, int prefixLength) { 61 | this.Address = address; 62 | this.PrefixLength = prefixLength; 63 | } 64 | 65 | public IPAddress(String ipAddresString) { 66 | String[] arrStrings = ipAddresString.split("/"); 67 | String address = arrStrings[0]; 68 | int prefixLength = 32; 69 | if (arrStrings.length > 1) { 70 | prefixLength = Integer.parseInt(arrStrings[1]); 71 | } 72 | this.Address = address; 73 | this.PrefixLength = prefixLength; 74 | } 75 | 76 | @SuppressLint("DefaultLocale") 77 | @Override 78 | public String toString() { 79 | return String.format("%s/%d", Address, PrefixLength); 80 | } 81 | 82 | @Override 83 | public boolean equals(Object o) { 84 | if (o == null) { 85 | return false; 86 | } else { 87 | return this.toString().equals(o.toString()); 88 | } 89 | } 90 | } 91 | 92 | public ProxyConfig() { 93 | m_IpList = new ArrayList(); 94 | m_DnsList = new ArrayList(); 95 | m_RouteList = new ArrayList(); 96 | m_ProxyList = new ArrayList(); 97 | m_DomainMap = new HashMap(); 98 | 99 | m_Timer = new Timer(); 100 | m_Timer.schedule(m_Task, 120000, 120000);//每两分钟刷新一次。 101 | } 102 | 103 | TimerTask m_Task = new TimerTask() { 104 | @Override 105 | public void run() { 106 | refreshProxyServer();//定时更新dns缓存 107 | } 108 | 109 | //定时更新dns缓存 110 | void refreshProxyServer() { 111 | try { 112 | for (int i = 0; i < m_ProxyList.size(); i++) { 113 | try { 114 | Config config = m_ProxyList.get(0); 115 | InetAddress address = InetAddress.getByName(config.ServerAddress.getHostName()); 116 | if (address != null && !address.equals(config.ServerAddress.getAddress())) { 117 | config.ServerAddress = new InetSocketAddress(address, config.ServerAddress.getPort()); 118 | } 119 | } catch (Exception e) { 120 | } 121 | } 122 | } catch (Exception e) { 123 | 124 | } 125 | } 126 | }; 127 | 128 | 129 | public static boolean isFakeIP(int ip) { 130 | return (ip & ProxyConfig.FAKE_NETWORK_MASK) == ProxyConfig.FAKE_NETWORK_IP; 131 | } 132 | 133 | public Config getDefaultProxy() { 134 | if (m_ProxyList.size() > 0) { 135 | return m_ProxyList.get(0); 136 | } else { 137 | return null; 138 | } 139 | } 140 | 141 | public Config getDefaultTunnelConfig(InetSocketAddress destAddress) { 142 | return getDefaultProxy(); 143 | } 144 | 145 | public IPAddress getDefaultLocalIP() { 146 | if (m_IpList.size() > 0) { 147 | return m_IpList.get(0); 148 | } else { 149 | return new IPAddress("10.8.0.2", 32); 150 | } 151 | } 152 | 153 | public ArrayList getDnsList() { 154 | return m_DnsList; 155 | } 156 | 157 | public ArrayList getRouteList() { 158 | return m_RouteList; 159 | } 160 | 161 | public int getDnsTTL() { 162 | if (m_dns_ttl < 30) { 163 | m_dns_ttl = 30; 164 | } 165 | return m_dns_ttl; 166 | } 167 | 168 | public String getWelcomeInfo() { 169 | return m_welcome_info; 170 | } 171 | 172 | public String getSessionName() { 173 | if (m_session_name == null) { 174 | m_session_name = getDefaultProxy().ServerAddress.getHostName(); 175 | } 176 | return m_session_name; 177 | } 178 | 179 | public String getUserAgent() { 180 | if (m_user_agent == null || m_user_agent.isEmpty()) { 181 | m_user_agent = System.getProperty("http.agent"); 182 | } 183 | return m_user_agent; 184 | } 185 | 186 | public int getMTU() { 187 | if (m_mtu > 1400 && m_mtu <= 20000) { 188 | return m_mtu; 189 | } else { 190 | return 20000; 191 | } 192 | } 193 | 194 | private Boolean getDomainState(String domain) { 195 | domain = domain.toLowerCase(); 196 | while (domain.length() > 0) { 197 | Boolean stateBoolean = m_DomainMap.get(domain); 198 | if (stateBoolean != null) { 199 | return stateBoolean; 200 | } else { 201 | int start = domain.indexOf('.') + 1; 202 | if (start > 0 && start < domain.length()) { 203 | domain = domain.substring(start); 204 | } else { 205 | return null; 206 | } 207 | } 208 | } 209 | return null; 210 | } 211 | 212 | public boolean needProxy(String host, int ip) { 213 | if (globalMode) { 214 | return true; 215 | } 216 | if (host != null) { 217 | Boolean stateBoolean = getDomainState(host); 218 | if (stateBoolean != null) { 219 | return stateBoolean.booleanValue(); 220 | } 221 | } 222 | 223 | if (isFakeIP(ip)) 224 | return true; 225 | 226 | if (m_outside_china_use_proxy && ip != 0) { 227 | return !ChinaIpMaskManager.isIPInChina(ip); 228 | } 229 | return false; 230 | } 231 | 232 | public boolean isIsolateHttpHostHeader() { 233 | return m_isolate_http_host_header; 234 | } 235 | 236 | private String[] downloadConfig(String url) throws Exception { 237 | try { 238 | HttpClient client = new DefaultHttpClient(); 239 | HttpGet requestGet = new HttpGet(url); 240 | 241 | requestGet.addHeader("X-Android-MODEL", Build.MODEL); 242 | requestGet.addHeader("X-Android-SDK_INT", Integer.toString(Build.VERSION.SDK_INT)); 243 | requestGet.addHeader("X-Android-RELEASE", Build.VERSION.RELEASE); 244 | requestGet.addHeader("X-App-Version", AppVersion); 245 | requestGet.addHeader("X-App-Install-ID", AppInstallID); 246 | requestGet.setHeader("User-Agent", System.getProperty("http.agent")); 247 | HttpResponse response = client.execute(requestGet); 248 | 249 | String configString = EntityUtils.toString(response.getEntity(), "UTF-8"); 250 | String[] lines = configString.split("\\n"); 251 | return lines; 252 | } catch (Exception e) { 253 | throw new Exception(String.format("Download config file from %s failed.", url)); 254 | } 255 | } 256 | 257 | private String[] readConfigFromFile(String path) throws Exception { 258 | StringBuilder sBuilder = new StringBuilder(); 259 | FileInputStream inputStream = null; 260 | try { 261 | byte[] buffer = new byte[8192]; 262 | int count = 0; 263 | inputStream = new FileInputStream(path); 264 | while ((count = inputStream.read(buffer)) > 0) { 265 | sBuilder.append(new String(buffer, 0, count, "UTF-8")); 266 | } 267 | return sBuilder.toString().split("\\n"); 268 | } catch (Exception e) { 269 | throw new Exception(String.format("Can't read config file: %s", path)); 270 | } finally { 271 | if (inputStream != null) { 272 | try { 273 | inputStream.close(); 274 | } catch (Exception e2) { 275 | } 276 | } 277 | } 278 | } 279 | 280 | public void loadFromFile(InputStream inputStream) throws Exception { 281 | byte[] bytes = new byte[inputStream.available()]; 282 | inputStream.read(bytes); 283 | loadFromLines(new String(bytes).split("\\r?\\n")); 284 | } 285 | 286 | public void loadFromUrl(String url) throws Exception { 287 | String[] lines = null; 288 | if (url.charAt(0) == '/') { 289 | lines = readConfigFromFile(url); 290 | } else { 291 | lines = downloadConfig(url); 292 | } 293 | loadFromLines(lines); 294 | } 295 | 296 | protected void loadFromLines(String[] lines) throws Exception { 297 | 298 | m_IpList.clear(); 299 | m_DnsList.clear(); 300 | m_RouteList.clear(); 301 | m_ProxyList.clear(); 302 | m_DomainMap.clear(); 303 | 304 | int lineNumber = 0; 305 | for (String line : lines) { 306 | lineNumber++; 307 | String[] items = line.split("\\s+"); 308 | if (items.length < 2) { 309 | continue; 310 | } 311 | 312 | String tagString = items[0].toLowerCase(Locale.ENGLISH).trim(); 313 | try { 314 | if (!tagString.startsWith("#")) { 315 | if (ProxyConfig.IS_DEBUG) 316 | System.out.println(line); 317 | 318 | if (tagString.equals("ip")) { 319 | addIPAddressToList(items, 1, m_IpList); 320 | } else if (tagString.equals("dns")) { 321 | addIPAddressToList(items, 1, m_DnsList); 322 | } else if (tagString.equals("route")) { 323 | addIPAddressToList(items, 1, m_RouteList); 324 | } else if (tagString.equals("proxy")) { 325 | addProxyToList(items, 1); 326 | } else if (tagString.equals("direct_domain")) { 327 | addDomainToHashMap(items, 1, false); 328 | } else if (tagString.equals("proxy_domain")) { 329 | addDomainToHashMap(items, 1, true); 330 | } else if (tagString.equals("dns_ttl")) { 331 | m_dns_ttl = Integer.parseInt(items[1]); 332 | } else if (tagString.equals("welcome_info")) { 333 | m_welcome_info = line.substring(line.indexOf(" ")).trim(); 334 | } else if (tagString.equals("session_name")) { 335 | m_session_name = items[1]; 336 | } else if (tagString.equals("user_agent")) { 337 | m_user_agent = line.substring(line.indexOf(" ")).trim(); 338 | } else if (tagString.equals("outside_china_use_proxy")) { 339 | m_outside_china_use_proxy = convertToBool(items[1]); 340 | } else if (tagString.equals("isolate_http_host_header")) { 341 | m_isolate_http_host_header = convertToBool(items[1]); 342 | } else if (tagString.equals("mtu")) { 343 | m_mtu = Integer.parseInt(items[1]); 344 | } 345 | } 346 | } catch (Exception e) { 347 | throw new Exception(String.format("config file parse error: line:%d, tag:%s, error:%s", lineNumber, tagString, e)); 348 | } 349 | 350 | } 351 | 352 | //查找默认代理。 353 | if (m_ProxyList.size() == 0) { 354 | tryAddProxy(lines); 355 | } 356 | } 357 | 358 | private void tryAddProxy(String[] lines) { 359 | for (String line : lines) { 360 | Pattern p = Pattern.compile("proxy\\s+([^:]+):(\\d+)", Pattern.CASE_INSENSITIVE); 361 | Matcher m = p.matcher(line); 362 | while (m.find()) { 363 | HttpConnectConfig config = new HttpConnectConfig(); 364 | config.ServerAddress = new InetSocketAddress(m.group(1), Integer.parseInt(m.group(2))); 365 | if (!m_ProxyList.contains(config)) { 366 | m_ProxyList.add(config); 367 | m_DomainMap.put(config.ServerAddress.getHostName(), false); 368 | } 369 | } 370 | } 371 | } 372 | 373 | public void addProxyToList(String proxyString) throws Exception { 374 | Config config = null; 375 | if (proxyString.startsWith("ss://")) { 376 | config = ShadowsocksConfig.parse(proxyString); 377 | } else { 378 | if (!proxyString.toLowerCase().startsWith("http://")) { 379 | proxyString = "http://" + proxyString; 380 | } 381 | config = HttpConnectConfig.parse(proxyString); 382 | } 383 | if (!m_ProxyList.contains(config)) { 384 | m_ProxyList.add(config); 385 | m_DomainMap.put(config.ServerAddress.getHostName(), false); 386 | } 387 | } 388 | 389 | private void addProxyToList(String[] items, int offset) throws Exception { 390 | for (int i = offset; i < items.length; i++) { 391 | addProxyToList(items[i].trim()); 392 | } 393 | } 394 | 395 | private void addDomainToHashMap(String[] items, int offset, Boolean state) { 396 | for (int i = offset; i < items.length; i++) { 397 | String domainString = items[i].toLowerCase().trim(); 398 | if (domainString.charAt(0) == '.') { 399 | domainString = domainString.substring(1); 400 | } 401 | m_DomainMap.put(domainString, state); 402 | } 403 | } 404 | 405 | private boolean convertToBool(String valueString) { 406 | if (valueString == null || valueString.isEmpty()) 407 | return false; 408 | valueString = valueString.toLowerCase(Locale.ENGLISH).trim(); 409 | if (valueString.equals("on") || valueString.equals("1") || valueString.equals("true") || valueString.equals("yes")) { 410 | return true; 411 | } else { 412 | return false; 413 | } 414 | } 415 | 416 | 417 | private void addIPAddressToList(String[] items, int offset, ArrayList list) { 418 | for (int i = offset; i < items.length; i++) { 419 | String item = items[i].trim().toLowerCase(); 420 | if (item.startsWith("#")) { 421 | break; 422 | } else { 423 | IPAddress ip = new IPAddress(item); 424 | if (!list.contains(ip)) { 425 | list.add(ip); 426 | } 427 | } 428 | } 429 | } 430 | 431 | } 432 | -------------------------------------------------------------------------------- /app/src/main/java/com/vm/shadowsocks/core/TcpProxyServer.java: -------------------------------------------------------------------------------- 1 | package com.vm.shadowsocks.core; 2 | 3 | import com.vm.shadowsocks.tcpip.CommonMethods; 4 | import com.vm.shadowsocks.tunnel.Tunnel; 5 | 6 | import java.io.IOException; 7 | import java.net.InetSocketAddress; 8 | import java.nio.channels.SelectionKey; 9 | import java.nio.channels.Selector; 10 | import java.nio.channels.ServerSocketChannel; 11 | import java.nio.channels.SocketChannel; 12 | import java.util.Iterator; 13 | 14 | public class TcpProxyServer implements Runnable { 15 | 16 | public boolean Stopped; 17 | public short Port; 18 | 19 | Selector m_Selector; 20 | ServerSocketChannel m_ServerSocketChannel; 21 | Thread m_ServerThread; 22 | 23 | public TcpProxyServer(int port) throws IOException { 24 | m_Selector = Selector.open(); 25 | m_ServerSocketChannel = ServerSocketChannel.open(); 26 | m_ServerSocketChannel.configureBlocking(false); 27 | m_ServerSocketChannel.socket().bind(new InetSocketAddress(port)); 28 | m_ServerSocketChannel.register(m_Selector, SelectionKey.OP_ACCEPT); 29 | this.Port = (short) m_ServerSocketChannel.socket().getLocalPort(); 30 | System.out.printf("AsyncTcpServer listen on %d success.\n", this.Port & 0xFFFF); 31 | } 32 | 33 | public void start() { 34 | m_ServerThread = new Thread(this); 35 | m_ServerThread.setName("TcpProxyServerThread"); 36 | m_ServerThread.start(); 37 | } 38 | 39 | public void stop() { 40 | this.Stopped = true; 41 | if (m_Selector != null) { 42 | try { 43 | m_Selector.close(); 44 | m_Selector = null; 45 | } catch (Exception e) { 46 | e.printStackTrace(); 47 | } 48 | } 49 | 50 | if (m_ServerSocketChannel != null) { 51 | try { 52 | m_ServerSocketChannel.close(); 53 | m_ServerSocketChannel = null; 54 | } catch (Exception e) { 55 | e.printStackTrace(); 56 | } 57 | } 58 | } 59 | 60 | @Override 61 | public void run() { 62 | try { 63 | while (true) { 64 | m_Selector.select(); 65 | Iterator keyIterator = m_Selector.selectedKeys().iterator(); 66 | while (keyIterator.hasNext()) { 67 | SelectionKey key = keyIterator.next(); 68 | if (key.isValid()) { 69 | try { 70 | if (key.isReadable()) { 71 | ((Tunnel) key.attachment()).onReadable(key); 72 | } else if (key.isWritable()) { 73 | ((Tunnel) key.attachment()).onWritable(key); 74 | } else if (key.isConnectable()) { 75 | ((Tunnel) key.attachment()).onConnectable(); 76 | } else if (key.isAcceptable()) { 77 | onAccepted(key); 78 | } 79 | } catch (Exception e) { 80 | System.out.println(e.toString()); 81 | } 82 | } 83 | keyIterator.remove(); 84 | } 85 | } 86 | } catch (Exception e) { 87 | e.printStackTrace(); 88 | } finally { 89 | this.stop(); 90 | System.out.println("TcpServer thread exited."); 91 | } 92 | } 93 | 94 | InetSocketAddress getDestAddress(SocketChannel localChannel) { 95 | short portKey = (short) localChannel.socket().getPort(); 96 | NatSession session = NatSessionManager.getSession(portKey); 97 | if (session != null) { 98 | if (ProxyConfig.Instance.needProxy(session.RemoteHost, session.RemoteIP)) { 99 | if (ProxyConfig.IS_DEBUG) 100 | System.out.printf("%d/%d:[PROXY] %s=>%s:%d\n", NatSessionManager.getSessionCount(), Tunnel.SessionCount, session.RemoteHost, CommonMethods.ipIntToString(session.RemoteIP), session.RemotePort & 0xFFFF); 101 | return InetSocketAddress.createUnresolved(session.RemoteHost, session.RemotePort & 0xFFFF); 102 | } else { 103 | return new InetSocketAddress(localChannel.socket().getInetAddress(), session.RemotePort & 0xFFFF); 104 | } 105 | } 106 | return null; 107 | } 108 | 109 | void onAccepted(SelectionKey key) { 110 | Tunnel localTunnel = null; 111 | try { 112 | SocketChannel localChannel = m_ServerSocketChannel.accept(); 113 | localTunnel = TunnelFactory.wrap(localChannel, m_Selector); 114 | 115 | InetSocketAddress destAddress = getDestAddress(localChannel); 116 | if (destAddress != null) { 117 | Tunnel remoteTunnel = TunnelFactory.createTunnelByConfig(destAddress, m_Selector); 118 | remoteTunnel.setBrotherTunnel(localTunnel);//关联兄弟 119 | localTunnel.setBrotherTunnel(remoteTunnel);//关联兄弟 120 | remoteTunnel.connect(destAddress);//开始连接 121 | } else { 122 | LocalVpnService.Instance.writeLog("Error: socket(%s:%d) target host is null.", localChannel.socket().getInetAddress().toString(), localChannel.socket().getPort()); 123 | localTunnel.dispose(); 124 | } 125 | } catch (Exception e) { 126 | e.printStackTrace(); 127 | LocalVpnService.Instance.writeLog("Error: remote socket create failed: %s", e.toString()); 128 | if (localTunnel != null) { 129 | localTunnel.dispose(); 130 | } 131 | } 132 | } 133 | 134 | } 135 | -------------------------------------------------------------------------------- /app/src/main/java/com/vm/shadowsocks/core/TunnelFactory.java: -------------------------------------------------------------------------------- 1 | package com.vm.shadowsocks.core; 2 | 3 | import com.vm.shadowsocks.tunnel.Config; 4 | import com.vm.shadowsocks.tunnel.RawTunnel; 5 | import com.vm.shadowsocks.tunnel.Tunnel; 6 | import com.vm.shadowsocks.tunnel.httpconnect.HttpConnectConfig; 7 | import com.vm.shadowsocks.tunnel.httpconnect.HttpConnectTunnel; 8 | import com.vm.shadowsocks.tunnel.shadowsocks.ShadowsocksConfig; 9 | import com.vm.shadowsocks.tunnel.shadowsocks.ShadowsocksTunnel; 10 | 11 | import java.net.InetSocketAddress; 12 | import java.nio.channels.Selector; 13 | import java.nio.channels.SocketChannel; 14 | 15 | public class TunnelFactory { 16 | 17 | public static Tunnel wrap(SocketChannel channel, Selector selector) { 18 | return new RawTunnel(channel, selector); 19 | } 20 | 21 | public static Tunnel createTunnelByConfig(InetSocketAddress destAddress, Selector selector) throws Exception { 22 | if (destAddress.isUnresolved()) { 23 | Config config = ProxyConfig.Instance.getDefaultTunnelConfig(destAddress); 24 | if (config instanceof HttpConnectConfig) { 25 | return new HttpConnectTunnel((HttpConnectConfig) config, selector); 26 | } else if (config instanceof ShadowsocksConfig) { 27 | return new ShadowsocksTunnel((ShadowsocksConfig) config, selector); 28 | } 29 | throw new Exception("The config is unknow."); 30 | } else { 31 | return new RawTunnel(destAddress, selector); 32 | } 33 | } 34 | 35 | } 36 | -------------------------------------------------------------------------------- /app/src/main/java/com/vm/shadowsocks/dns/DnsFlags.java: -------------------------------------------------------------------------------- 1 | package com.vm.shadowsocks.dns; 2 | 3 | public class DnsFlags { 4 | public boolean QR;//1 bits 5 | public int OpCode;//4 bits 6 | public boolean AA;//1 bits 7 | public boolean TC;//1 bits 8 | public boolean RD;//1 bits 9 | public boolean RA;//1 bits 10 | public int Zero;//3 bits 11 | public int Rcode;//4 bits 12 | 13 | public static DnsFlags Parse(short value) { 14 | int m_Flags = value & 0xFFFF; 15 | DnsFlags flags = new DnsFlags(); 16 | flags.QR = ((m_Flags >> 7) & 0x01) == 1; 17 | flags.OpCode = (m_Flags >> 3) & 0x0F; 18 | flags.AA = ((m_Flags >> 2) & 0x01) == 1; 19 | flags.TC = ((m_Flags >> 1) & 0x01) == 1; 20 | flags.RD = (m_Flags & 0x01) == 1; 21 | flags.RA = (m_Flags >> 15) == 1; 22 | flags.Zero = (m_Flags >> 12) & 0x07; 23 | flags.Rcode = ((m_Flags >> 8) & 0xF); 24 | return flags; 25 | } 26 | 27 | public short ToShort() { 28 | int m_Flags = 0; 29 | m_Flags |= (this.QR ? 1 : 0) << 7; 30 | m_Flags |= (this.OpCode & 0x0F) << 3; 31 | m_Flags |= (this.AA ? 1 : 0) << 2; 32 | m_Flags |= (this.TC ? 1 : 0) << 1; 33 | m_Flags |= this.RD ? 1 : 0; 34 | m_Flags |= (this.RA ? 1 : 0) << 15; 35 | m_Flags |= (this.Zero & 0x07) << 12; 36 | m_Flags |= (this.Rcode & 0x0F) << 8; 37 | return (short) m_Flags; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /app/src/main/java/com/vm/shadowsocks/dns/DnsHeader.java: -------------------------------------------------------------------------------- 1 | package com.vm.shadowsocks.dns; 2 | 3 | import com.vm.shadowsocks.tcpip.CommonMethods; 4 | 5 | import java.nio.ByteBuffer; 6 | 7 | 8 | public class DnsHeader { 9 | public short ID; 10 | public DnsFlags Flags; 11 | public short QuestionCount; 12 | public short ResourceCount; 13 | public short AResourceCount; 14 | public short EResourceCount; 15 | 16 | public static DnsHeader FromBytes(ByteBuffer buffer) { 17 | DnsHeader header = new DnsHeader(buffer.array(), buffer.arrayOffset() + buffer.position()); 18 | header.ID = buffer.getShort(); 19 | header.Flags = DnsFlags.Parse(buffer.getShort()); 20 | header.QuestionCount = buffer.getShort(); 21 | header.ResourceCount = buffer.getShort(); 22 | header.AResourceCount = buffer.getShort(); 23 | header.EResourceCount = buffer.getShort(); 24 | return header; 25 | } 26 | 27 | public void ToBytes(ByteBuffer buffer) { 28 | buffer.putShort(this.ID); 29 | buffer.putShort(this.Flags.ToShort()); 30 | buffer.putShort(this.QuestionCount); 31 | buffer.putShort(this.ResourceCount); 32 | buffer.putShort(this.AResourceCount); 33 | buffer.putShort(this.EResourceCount); 34 | } 35 | 36 | static final short offset_ID = 0; 37 | static final short offset_Flags = 2; 38 | static final short offset_QuestionCount = 4; 39 | static final short offset_ResourceCount = 6; 40 | static final short offset_AResourceCount = 8; 41 | static final short offset_EResourceCount = 10; 42 | 43 | public byte[] Data; 44 | public int Offset; 45 | 46 | public DnsHeader(byte[] data, int offset) { 47 | this.Offset = offset; 48 | this.Data = data; 49 | } 50 | 51 | public short getID() { 52 | return CommonMethods.readShort(Data, Offset + offset_ID); 53 | } 54 | 55 | public short getFlags() { 56 | return CommonMethods.readShort(Data, Offset + offset_Flags); 57 | } 58 | 59 | public short getQuestionCount() { 60 | return CommonMethods.readShort(Data, Offset + offset_QuestionCount); 61 | } 62 | 63 | public short getResourceCount() { 64 | return CommonMethods.readShort(Data, Offset + offset_ResourceCount); 65 | } 66 | 67 | public short getAResourceCount() { 68 | return CommonMethods.readShort(Data, Offset + offset_AResourceCount); 69 | } 70 | 71 | public short getEResourceCount() { 72 | return CommonMethods.readShort(Data, Offset + offset_EResourceCount); 73 | } 74 | 75 | public void setID(short value) { 76 | CommonMethods.writeShort(Data, Offset + offset_ID, value); 77 | } 78 | 79 | public void setFlags(short value) { 80 | CommonMethods.writeShort(Data, Offset + offset_Flags, value); 81 | } 82 | 83 | public void setQuestionCount(short value) { 84 | CommonMethods.writeShort(Data, Offset + offset_QuestionCount, value); 85 | } 86 | 87 | public void setResourceCount(short value) { 88 | CommonMethods.writeShort(Data, Offset + offset_ResourceCount, value); 89 | } 90 | 91 | public void setAResourceCount(short value) { 92 | CommonMethods.writeShort(Data, Offset + offset_AResourceCount, value); 93 | } 94 | 95 | public void setEResourceCount(short value) { 96 | CommonMethods.writeShort(Data, Offset + offset_EResourceCount, value); 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /app/src/main/java/com/vm/shadowsocks/dns/DnsPacket.java: -------------------------------------------------------------------------------- 1 | package com.vm.shadowsocks.dns; 2 | 3 | import java.nio.ByteBuffer; 4 | 5 | public class DnsPacket { 6 | public DnsHeader Header; 7 | public Question[] Questions; 8 | public Resource[] Resources; 9 | public Resource[] AResources; 10 | public Resource[] EResources; 11 | 12 | public int Size; 13 | 14 | public static DnsPacket FromBytes(ByteBuffer buffer) { 15 | if (buffer.limit() < 12) 16 | return null; 17 | if (buffer.limit() > 512) 18 | return null; 19 | 20 | DnsPacket packet = new DnsPacket(); 21 | packet.Size = buffer.limit(); 22 | packet.Header = DnsHeader.FromBytes(buffer); 23 | 24 | if (packet.Header.QuestionCount > 2 || packet.Header.ResourceCount > 50 || packet.Header.AResourceCount > 50 || packet.Header.EResourceCount > 50) { 25 | return null; 26 | } 27 | 28 | packet.Questions = new Question[packet.Header.QuestionCount]; 29 | packet.Resources = new Resource[packet.Header.ResourceCount]; 30 | packet.AResources = new Resource[packet.Header.AResourceCount]; 31 | packet.EResources = new Resource[packet.Header.EResourceCount]; 32 | 33 | for (int i = 0; i < packet.Questions.length; i++) { 34 | packet.Questions[i] = Question.FromBytes(buffer); 35 | } 36 | 37 | for (int i = 0; i < packet.Resources.length; i++) { 38 | packet.Resources[i] = Resource.FromBytes(buffer); 39 | } 40 | 41 | for (int i = 0; i < packet.AResources.length; i++) { 42 | packet.AResources[i] = Resource.FromBytes(buffer); 43 | } 44 | 45 | for (int i = 0; i < packet.EResources.length; i++) { 46 | packet.EResources[i] = Resource.FromBytes(buffer); 47 | } 48 | 49 | return packet; 50 | } 51 | 52 | public void ToBytes(ByteBuffer buffer) { 53 | Header.QuestionCount = 0; 54 | Header.ResourceCount = 0; 55 | Header.AResourceCount = 0; 56 | Header.EResourceCount = 0; 57 | 58 | if (Questions != null) 59 | Header.QuestionCount = (short) Questions.length; 60 | if (Resources != null) 61 | Header.ResourceCount = (short) Resources.length; 62 | if (AResources != null) 63 | Header.AResourceCount = (short) AResources.length; 64 | if (EResources != null) 65 | Header.EResourceCount = (short) EResources.length; 66 | 67 | this.Header.ToBytes(buffer); 68 | 69 | for (int i = 0; i < Header.QuestionCount; i++) { 70 | this.Questions[i].ToBytes(buffer); 71 | } 72 | 73 | for (int i = 0; i < Header.ResourceCount; i++) { 74 | this.Resources[i].ToBytes(buffer); 75 | } 76 | 77 | for (int i = 0; i < Header.AResourceCount; i++) { 78 | this.AResources[i].ToBytes(buffer); 79 | } 80 | 81 | for (int i = 0; i < Header.EResourceCount; i++) { 82 | this.EResources[i].ToBytes(buffer); 83 | } 84 | } 85 | 86 | public static String ReadDomain(ByteBuffer buffer, int dnsHeaderOffset) { 87 | StringBuilder sb = new StringBuilder(); 88 | int len = 0; 89 | while (buffer.hasRemaining() && (len = (buffer.get() & 0xFF)) > 0) { 90 | if ((len & 0xc0) == 0xc0)// pointer 高2位为11表示是指针。如:1100 0000 91 | { 92 | // 指针的取值是前一字节的后6位加后一字节的8位共14位的值。 93 | int pointer = buffer.get() & 0xFF;// 低8位 94 | pointer |= (len & 0x3F) << 8;// 高6位 95 | 96 | ByteBuffer newBuffer = ByteBuffer.wrap(buffer.array(), dnsHeaderOffset + pointer, dnsHeaderOffset + buffer.limit()); 97 | sb.append(ReadDomain(newBuffer, dnsHeaderOffset)); 98 | return sb.toString(); 99 | } else { 100 | while (len > 0 && buffer.hasRemaining()) { 101 | sb.append((char) (buffer.get() & 0xFF)); 102 | len--; 103 | } 104 | sb.append('.'); 105 | } 106 | } 107 | 108 | if (len == 0 && sb.length() > 0) { 109 | sb.deleteCharAt(sb.length() - 1);//去掉末尾的点(.) 110 | } 111 | return sb.toString(); 112 | } 113 | 114 | public static void WriteDomain(String domain, ByteBuffer buffer) { 115 | if (domain == null || domain == "") { 116 | buffer.put((byte) 0); 117 | return; 118 | } 119 | 120 | String[] arr = domain.split("\\."); 121 | for (String item : arr) { 122 | if (arr.length > 1) { 123 | buffer.put((byte) item.length()); 124 | } 125 | 126 | for (int i = 0; i < item.length(); i++) { 127 | buffer.put((byte) item.codePointAt(i)); 128 | } 129 | } 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /app/src/main/java/com/vm/shadowsocks/dns/Question.java: -------------------------------------------------------------------------------- 1 | package com.vm.shadowsocks.dns; 2 | 3 | import java.nio.ByteBuffer; 4 | 5 | public class Question { 6 | public String Domain; 7 | public short Type; 8 | public short Class; 9 | 10 | private int offset; 11 | 12 | public int Offset() { 13 | return offset; 14 | } 15 | 16 | private int length; 17 | 18 | public int Length() { 19 | return length; 20 | } 21 | 22 | public static Question FromBytes(ByteBuffer buffer) { 23 | Question q = new Question(); 24 | q.offset = buffer.arrayOffset() + buffer.position(); 25 | q.Domain = DnsPacket.ReadDomain(buffer, buffer.arrayOffset()); 26 | q.Type = buffer.getShort(); 27 | q.Class = buffer.getShort(); 28 | q.length = buffer.arrayOffset() + buffer.position() - q.offset; 29 | return q; 30 | } 31 | 32 | public void ToBytes(ByteBuffer buffer) { 33 | this.offset = buffer.position(); 34 | DnsPacket.WriteDomain(this.Domain, buffer); 35 | buffer.putShort(this.Type); 36 | buffer.putShort(this.Class); 37 | this.length = buffer.position() - this.offset; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /app/src/main/java/com/vm/shadowsocks/dns/Resource.java: -------------------------------------------------------------------------------- 1 | package com.vm.shadowsocks.dns; 2 | 3 | import java.nio.ByteBuffer; 4 | 5 | public class Resource { 6 | public String Domain; 7 | public short Type; 8 | public short Class; 9 | public int TTL; 10 | public short DataLength; 11 | public byte[] Data; 12 | 13 | private int offset; 14 | 15 | public int Offset() { 16 | return offset; 17 | } 18 | 19 | private int length; 20 | 21 | public int Length() { 22 | return length; 23 | } 24 | 25 | public static Resource FromBytes(ByteBuffer buffer) { 26 | 27 | Resource r = new Resource(); 28 | r.offset = buffer.arrayOffset() + buffer.position(); 29 | r.Domain = DnsPacket.ReadDomain(buffer, buffer.arrayOffset()); 30 | r.Type = buffer.getShort(); 31 | r.Class = buffer.getShort(); 32 | r.TTL = buffer.getInt(); 33 | r.DataLength = buffer.getShort(); 34 | r.Data = new byte[r.DataLength & 0xFFFF]; 35 | buffer.get(r.Data); 36 | r.length = buffer.arrayOffset() + buffer.position() - r.offset; 37 | return r; 38 | } 39 | 40 | public void ToBytes(ByteBuffer buffer) { 41 | if (this.Data == null) { 42 | this.Data = new byte[0]; 43 | } 44 | this.DataLength = (short) this.Data.length; 45 | 46 | this.offset = buffer.position(); 47 | DnsPacket.WriteDomain(this.Domain, buffer); 48 | buffer.putShort(this.Type); 49 | buffer.putShort(this.Class); 50 | buffer.putInt(this.TTL); 51 | 52 | buffer.putShort(this.DataLength); 53 | buffer.put(this.Data); 54 | this.length = buffer.position() - this.offset; 55 | } 56 | 57 | 58 | } 59 | -------------------------------------------------------------------------------- /app/src/main/java/com/vm/shadowsocks/dns/ResourcePointer.java: -------------------------------------------------------------------------------- 1 | package com.vm.shadowsocks.dns; 2 | 3 | import com.vm.shadowsocks.tcpip.CommonMethods; 4 | 5 | public class ResourcePointer { 6 | static final short offset_Domain = 0; 7 | static final short offset_Type = 2; 8 | static final short offset_Class = 4; 9 | static final int offset_TTL = 6; 10 | static final short offset_DataLength = 10; 11 | static final int offset_IP = 12; 12 | 13 | byte[] Data; 14 | int Offset; 15 | 16 | public ResourcePointer(byte[] data, int offset) { 17 | this.Data = data; 18 | this.Offset = offset; 19 | } 20 | 21 | public void setDomain(short value) { 22 | CommonMethods.writeShort(Data, Offset + offset_Domain, value); 23 | } 24 | 25 | public short getType() { 26 | return CommonMethods.readShort(Data, Offset + offset_Type); 27 | } 28 | 29 | public void setType(short value) { 30 | CommonMethods.writeShort(Data, Offset + offset_Type, value); 31 | } 32 | 33 | public short getClass(short value) { 34 | return CommonMethods.readShort(Data, Offset + offset_Class); 35 | } 36 | 37 | public void setClass(short value) { 38 | CommonMethods.writeShort(Data, Offset + offset_Class, value); 39 | } 40 | 41 | public int getTTL() { 42 | return CommonMethods.readInt(Data, Offset + offset_TTL); 43 | } 44 | 45 | public void setTTL(int value) { 46 | CommonMethods.writeInt(Data, Offset + offset_TTL, value); 47 | } 48 | 49 | public short getDataLength() { 50 | return CommonMethods.readShort(Data, Offset + offset_DataLength); 51 | } 52 | 53 | public void setDataLength(short value) { 54 | CommonMethods.writeShort(Data, Offset + offset_DataLength, value); 55 | } 56 | 57 | public int getIP() { 58 | return CommonMethods.readInt(Data, Offset + offset_IP); 59 | } 60 | 61 | public void setIP(int value) { 62 | CommonMethods.writeInt(Data, Offset + offset_IP, value); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /app/src/main/java/com/vm/shadowsocks/tcpip/CommonMethods.java: -------------------------------------------------------------------------------- 1 | package com.vm.shadowsocks.tcpip; 2 | 3 | import java.net.Inet4Address; 4 | import java.net.InetAddress; 5 | import java.net.UnknownHostException; 6 | 7 | public class CommonMethods { 8 | 9 | public static InetAddress ipIntToInet4Address(int ip) { 10 | byte[] ipAddress = new byte[4]; 11 | writeInt(ipAddress, 0, ip); 12 | try { 13 | return Inet4Address.getByAddress(ipAddress); 14 | } catch (UnknownHostException e) { 15 | // TODO Auto-generated catch block 16 | e.printStackTrace(); 17 | return null; 18 | } 19 | } 20 | 21 | public static String ipIntToString(int ip) { 22 | return String.format("%s.%s.%s.%s", (ip >> 24) & 0x00FF, 23 | (ip >> 16) & 0x00FF, (ip >> 8) & 0x00FF, ip & 0x00FF); 24 | } 25 | 26 | public static String ipBytesToString(byte[] ip) { 27 | return String.format("%s.%s.%s.%s", ip[0] & 0x00FF, ip[1] & 0x00FF, ip[2] & 0x00FF, ip[3] & 0x00FF); 28 | } 29 | 30 | public static int ipStringToInt(String ip) { 31 | String[] arrStrings = ip.split("\\."); 32 | int r = (Integer.parseInt(arrStrings[0]) << 24) 33 | | (Integer.parseInt(arrStrings[1]) << 16) 34 | | (Integer.parseInt(arrStrings[2]) << 8) 35 | | Integer.parseInt(arrStrings[3]); 36 | return r; 37 | } 38 | 39 | public static int readInt(byte[] data, int offset) { 40 | int r = ((data[offset] & 0xFF) << 24) 41 | | ((data[offset + 1] & 0xFF) << 16) 42 | | ((data[offset + 2] & 0xFF) << 8) | (data[offset + 3] & 0xFF); 43 | return r; 44 | } 45 | 46 | public static short readShort(byte[] data, int offset) { 47 | int r = ((data[offset] & 0xFF) << 8) | (data[offset + 1] & 0xFF); 48 | return (short) r; 49 | } 50 | 51 | public static void writeInt(byte[] data, int offset, int value) { 52 | data[offset] = (byte) (value >> 24); 53 | data[offset + 1] = (byte) (value >> 16); 54 | data[offset + 2] = (byte) (value >> 8); 55 | data[offset + 3] = (byte) (value); 56 | } 57 | 58 | public static void writeShort(byte[] data, int offset, short value) { 59 | data[offset] = (byte) (value >> 8); 60 | data[offset + 1] = (byte) (value); 61 | } 62 | 63 | // 网络字节顺序与主机字节顺序的转换 64 | 65 | public static short htons(short u) { 66 | int r = ((u & 0xFFFF) << 8) | ((u & 0xFFFF) >> 8); 67 | return (short) r; 68 | } 69 | 70 | public static short ntohs(short u) { 71 | int r = ((u & 0xFFFF) << 8) | ((u & 0xFFFF) >> 8); 72 | return (short) r; 73 | } 74 | 75 | public static int hton(int u) { 76 | int r = (u >> 24) & 0x000000FF; 77 | r |= (u >> 8) & 0x0000FF00; 78 | r |= (u << 8) & 0x00FF0000; 79 | r |= (u << 24) & 0xFF000000; 80 | return r; 81 | } 82 | 83 | public static int ntoh(int u) { 84 | int r = (u >> 24) & 0x000000FF; 85 | r |= (u >> 8) & 0x0000FF00; 86 | r |= (u << 8) & 0x00FF0000; 87 | r |= (u << 24) & 0xFF000000; 88 | return r; 89 | } 90 | 91 | // 计算校验和 92 | public static short checksum(long sum, byte[] buf, int offset, int len) { 93 | sum += getsum(buf, offset, len); 94 | while ((sum >> 16) > 0) 95 | sum = (sum & 0xFFFF) + (sum >> 16); 96 | return (short) ~sum; 97 | } 98 | 99 | public static long getsum(byte[] buf, int offset, int len) { 100 | long sum = 0; /* assume 32 bit long, 16 bit short */ 101 | while (len > 1) { 102 | sum += readShort(buf, offset) & 0xFFFF; 103 | offset += 2; 104 | len -= 2; 105 | } 106 | 107 | if (len > 0) /* take care of left over byte */ { 108 | sum += (buf[offset] & 0xFF) << 8; 109 | } 110 | return sum; 111 | } 112 | 113 | // 计算IP包的校验和 114 | public static boolean ComputeIPChecksum(IPHeader ipHeader) { 115 | short oldCrc = ipHeader.getCrc(); 116 | ipHeader.setCrc((short) 0);// 计算前置零 117 | short newCrc = CommonMethods.checksum(0, ipHeader.m_Data, 118 | ipHeader.m_Offset, ipHeader.getHeaderLength()); 119 | ipHeader.setCrc(newCrc); 120 | return oldCrc == newCrc; 121 | } 122 | 123 | // 计算TCP或UDP的校验和 124 | public static boolean ComputeTCPChecksum(IPHeader ipHeader, TCPHeader tcpHeader) { 125 | ComputeIPChecksum(ipHeader);//计算IP校验和 126 | int ipData_len = ipHeader.getTotalLength() - ipHeader.getHeaderLength();// IP数据长度 127 | if (ipData_len < 0) 128 | return false; 129 | // 计算为伪首部和 130 | long sum = getsum(ipHeader.m_Data, ipHeader.m_Offset 131 | + IPHeader.offset_src_ip, 8); 132 | sum += ipHeader.getProtocol() & 0xFF; 133 | sum += ipData_len; 134 | 135 | short oldCrc = tcpHeader.getCrc(); 136 | tcpHeader.setCrc((short) 0);// 计算前置0 137 | 138 | short newCrc = checksum(sum, tcpHeader.m_Data, tcpHeader.m_Offset, ipData_len);// 计算校验和 139 | 140 | tcpHeader.setCrc(newCrc); 141 | return oldCrc == newCrc; 142 | } 143 | 144 | // 计算TCP或UDP的校验和 145 | public static boolean ComputeUDPChecksum(IPHeader ipHeader, UDPHeader udpHeader) { 146 | ComputeIPChecksum(ipHeader);//计算IP校验和 147 | int ipData_len = ipHeader.getTotalLength() - ipHeader.getHeaderLength();// IP数据长度 148 | if (ipData_len < 0) 149 | return false; 150 | // 计算为伪首部和 151 | long sum = getsum(ipHeader.m_Data, ipHeader.m_Offset 152 | + IPHeader.offset_src_ip, 8); 153 | sum += ipHeader.getProtocol() & 0xFF; 154 | sum += ipData_len; 155 | 156 | short oldCrc = udpHeader.getCrc(); 157 | udpHeader.setCrc((short) 0);// 计算前置0 158 | 159 | short newCrc = checksum(sum, udpHeader.m_Data, udpHeader.m_Offset, ipData_len);// 计算校验和 160 | 161 | udpHeader.setCrc(newCrc); 162 | return oldCrc == newCrc; 163 | } 164 | } 165 | -------------------------------------------------------------------------------- /app/src/main/java/com/vm/shadowsocks/tcpip/IPHeader.java: -------------------------------------------------------------------------------- 1 | package com.vm.shadowsocks.tcpip; 2 | 3 | public class IPHeader { 4 | 5 | public static final short IP = 0x0800; 6 | public static final byte ICMP = 1; 7 | public static final byte TCP = 6; 8 | public static final byte UDP = 17; 9 | 10 | static final byte offset_ver_ihl = 0; // 0: Version (4 bits) + Internet header length (4// bits) 11 | static final byte offset_tos = 1; // 1: Type of service 12 | static final short offset_tlen = 2; // 2: Total length 13 | static final short offset_identification = 4; // :4 Identification 14 | static final short offset_flags_fo = 6; // 6: Flags (3 bits) + Fragment offset (13 bits) 15 | static final byte offset_ttl = 8; // 8: Time to live 16 | public static final byte offset_proto = 9; // 9: Protocol 17 | static final short offset_crc = 10; // 10: Header checksum 18 | public static final int offset_src_ip = 12; // 12: Source address 19 | public static final int offset_dest_ip = 16; // 16: Destination address 20 | static final int offset_op_pad = 20; // 20: Option + Padding 21 | 22 | public byte[] m_Data; 23 | public int m_Offset; 24 | 25 | public IPHeader(byte[] data, int offset) { 26 | this.m_Data = data; 27 | this.m_Offset = offset; 28 | } 29 | 30 | public void Default() { 31 | setHeaderLength(20); 32 | setTos((byte) 0); 33 | setTotalLength(0); 34 | setIdentification(0); 35 | setFlagsAndOffset((short) 0); 36 | setTTL((byte) 64); 37 | } 38 | 39 | public int getDataLength() { 40 | return this.getTotalLength() - this.getHeaderLength(); 41 | } 42 | 43 | public int getHeaderLength() { 44 | return (m_Data[m_Offset + offset_ver_ihl] & 0x0F) * 4; 45 | } 46 | 47 | public void setHeaderLength(int value) { 48 | m_Data[m_Offset + offset_ver_ihl] = (byte) ((4 << 4) | (value / 4)); 49 | } 50 | 51 | public byte getTos() { 52 | return m_Data[m_Offset + offset_tos]; 53 | } 54 | 55 | public void setTos(byte value) { 56 | m_Data[m_Offset + offset_tos] = value; 57 | } 58 | 59 | public int getTotalLength() { 60 | return CommonMethods.readShort(m_Data, m_Offset + offset_tlen) & 0xFFFF; 61 | } 62 | 63 | public void setTotalLength(int value) { 64 | CommonMethods.writeShort(m_Data, m_Offset + offset_tlen, (short) value); 65 | } 66 | 67 | public int getIdentification() { 68 | return CommonMethods.readShort(m_Data, m_Offset + offset_identification) & 0xFFFF; 69 | } 70 | 71 | public void setIdentification(int value) { 72 | CommonMethods.writeShort(m_Data, m_Offset + offset_identification, (short) value); 73 | } 74 | 75 | public short getFlagsAndOffset() { 76 | return CommonMethods.readShort(m_Data, m_Offset + offset_flags_fo); 77 | } 78 | 79 | public void setFlagsAndOffset(short value) { 80 | CommonMethods.writeShort(m_Data, m_Offset + offset_flags_fo, value); 81 | } 82 | 83 | public byte getTTL() { 84 | return m_Data[m_Offset + offset_ttl]; 85 | } 86 | 87 | public void setTTL(byte value) { 88 | m_Data[m_Offset + offset_ttl] = value; 89 | } 90 | 91 | public byte getProtocol() { 92 | return m_Data[m_Offset + offset_proto]; 93 | } 94 | 95 | public void setProtocol(byte value) { 96 | m_Data[m_Offset + offset_proto] = value; 97 | } 98 | 99 | public short getCrc() { 100 | return CommonMethods.readShort(m_Data, m_Offset + offset_crc); 101 | } 102 | 103 | public void setCrc(short value) { 104 | CommonMethods.writeShort(m_Data, m_Offset + offset_crc, value); 105 | } 106 | 107 | public int getSourceIP() { 108 | return CommonMethods.readInt(m_Data, m_Offset + offset_src_ip); 109 | } 110 | 111 | public void setSourceIP(int value) { 112 | CommonMethods.writeInt(m_Data, m_Offset + offset_src_ip, value); 113 | } 114 | 115 | public int getDestinationIP() { 116 | return CommonMethods.readInt(m_Data, m_Offset + offset_dest_ip); 117 | } 118 | 119 | public void setDestinationIP(int value) { 120 | CommonMethods.writeInt(m_Data, m_Offset + offset_dest_ip, value); 121 | } 122 | 123 | @Override 124 | public String toString() { 125 | return String.format("%s->%s Pro=%s,HLen=%d", CommonMethods.ipIntToString(getSourceIP()), CommonMethods.ipIntToString(getDestinationIP()), getProtocol(), getHeaderLength()); 126 | } 127 | 128 | } 129 | -------------------------------------------------------------------------------- /app/src/main/java/com/vm/shadowsocks/tcpip/TCPHeader.java: -------------------------------------------------------------------------------- 1 | package com.vm.shadowsocks.tcpip; 2 | 3 | 4 | public class TCPHeader { 5 | 6 | public static final int FIN = 1; 7 | public static final int SYN = 2; 8 | public static final int RST = 4; 9 | public static final int PSH = 8; 10 | public static final int ACK = 16; 11 | public static final int URG = 32; 12 | 13 | static final short offset_src_port = 0; // 16位源端口 14 | static final short offset_dest_port = 2; // 16位目的端口 15 | static final int offset_seq = 4; // 32位序列号 16 | static final int offset_ack = 8; // 32位确认号 17 | static final byte offset_lenres = 12; // 4位首部长度/4位保留字 18 | static final byte offset_flag = 13; // 6位标志位 19 | static final short offset_win = 14; // 16位窗口大小 20 | static final short offset_crc = 16; // 16位校验和 21 | static final short offset_urp = 18; // 16位紧急数据偏移量 22 | 23 | public byte[] m_Data; 24 | public int m_Offset; 25 | 26 | public TCPHeader(byte[] data, int offset) { 27 | this.m_Data = data; 28 | this.m_Offset = offset; 29 | } 30 | 31 | public int getHeaderLength() { 32 | int lenres = m_Data[m_Offset + offset_lenres] & 0xFF; 33 | return (lenres >> 4) * 4; 34 | } 35 | 36 | public short getSourcePort() { 37 | return CommonMethods.readShort(m_Data, m_Offset + offset_src_port); 38 | } 39 | 40 | public void setSourcePort(short value) { 41 | CommonMethods.writeShort(m_Data, m_Offset + offset_src_port, value); 42 | } 43 | 44 | public short getDestinationPort() { 45 | return CommonMethods.readShort(m_Data, m_Offset + offset_dest_port); 46 | } 47 | 48 | public void setDestinationPort(short value) { 49 | CommonMethods.writeShort(m_Data, m_Offset + offset_dest_port, value); 50 | } 51 | 52 | public byte getFlags() { 53 | return m_Data[m_Offset + offset_flag]; 54 | } 55 | 56 | public short getCrc() { 57 | return CommonMethods.readShort(m_Data, m_Offset + offset_crc); 58 | } 59 | 60 | public void setCrc(short value) { 61 | CommonMethods.writeShort(m_Data, m_Offset + offset_crc, value); 62 | } 63 | 64 | public int getSeqID() { 65 | return CommonMethods.readInt(m_Data, m_Offset + offset_seq); 66 | } 67 | 68 | public int getAckID() { 69 | return CommonMethods.readInt(m_Data, m_Offset + offset_ack); 70 | } 71 | 72 | @Override 73 | public String toString() { 74 | // TODO Auto-generated method stub 75 | return String.format("%s%s%s%s%s%s%d->%d %s:%s", 76 | (getFlags() & SYN) == SYN ? "SYN " : "", 77 | (getFlags() & ACK) == ACK ? "ACK " : "", 78 | (getFlags() & PSH) == PSH ? "PSH " : "", 79 | (getFlags() & RST) == RST ? "RST " : "", 80 | (getFlags() & FIN) == FIN ? "FIN " : "", 81 | (getFlags() & URG) == URG ? "URG " : "", 82 | getSourcePort() & 0xFFFF, 83 | getDestinationPort() & 0xFFFF, 84 | getSeqID(), 85 | getAckID()); 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /app/src/main/java/com/vm/shadowsocks/tcpip/UDPHeader.java: -------------------------------------------------------------------------------- 1 | package com.vm.shadowsocks.tcpip; 2 | 3 | public class UDPHeader { 4 | static final short offset_src_port = 0; // Source port 5 | static final short offset_dest_port = 2; // Destination port 6 | static final short offset_tlen = 4; // Datagram length 7 | static final short offset_crc = 6; // Checksum 8 | 9 | public byte[] m_Data; 10 | public int m_Offset; 11 | 12 | public UDPHeader(byte[] data, int offset) { 13 | this.m_Data = data; 14 | this.m_Offset = offset; 15 | } 16 | 17 | public short getSourcePort() { 18 | return CommonMethods.readShort(m_Data, m_Offset + offset_src_port); 19 | } 20 | 21 | public void setSourcePort(short value) { 22 | CommonMethods.writeShort(m_Data, m_Offset + offset_src_port, value); 23 | } 24 | 25 | public short getDestinationPort() { 26 | return CommonMethods.readShort(m_Data, m_Offset + offset_dest_port); 27 | } 28 | 29 | public void setDestinationPort(short value) { 30 | CommonMethods.writeShort(m_Data, m_Offset + offset_dest_port, value); 31 | } 32 | 33 | public int getTotalLength() { 34 | return CommonMethods.readShort(m_Data, m_Offset + offset_tlen) & 0xFFFF; 35 | } 36 | 37 | public void setTotalLength(int value) { 38 | CommonMethods.writeShort(m_Data, m_Offset + offset_tlen, (short) value); 39 | } 40 | 41 | public short getCrc() { 42 | return CommonMethods.readShort(m_Data, m_Offset + offset_crc); 43 | } 44 | 45 | public void setCrc(short value) { 46 | CommonMethods.writeShort(m_Data, m_Offset + offset_crc, value); 47 | } 48 | 49 | @Override 50 | public String toString() { 51 | // TODO Auto-generated method stub 52 | return String.format("%d->%d", getSourcePort() & 0xFFFF, 53 | getDestinationPort() & 0xFFFF); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /app/src/main/java/com/vm/shadowsocks/tunnel/Config.java: -------------------------------------------------------------------------------- 1 | package com.vm.shadowsocks.tunnel; 2 | 3 | import java.net.InetSocketAddress; 4 | 5 | public abstract class Config { 6 | public InetSocketAddress ServerAddress; 7 | } 8 | -------------------------------------------------------------------------------- /app/src/main/java/com/vm/shadowsocks/tunnel/RawTunnel.java: -------------------------------------------------------------------------------- 1 | package com.vm.shadowsocks.tunnel; 2 | 3 | import java.net.InetSocketAddress; 4 | import java.nio.ByteBuffer; 5 | import java.nio.channels.Selector; 6 | import java.nio.channels.SocketChannel; 7 | 8 | public class RawTunnel extends Tunnel { 9 | 10 | public RawTunnel(InetSocketAddress serverAddress, Selector selector) throws Exception { 11 | super(serverAddress, selector); 12 | } 13 | 14 | public RawTunnel(SocketChannel innerChannel, Selector selector) { 15 | super(innerChannel, selector); 16 | // TODO Auto-generated constructor stub 17 | } 18 | 19 | @Override 20 | protected void onConnected(ByteBuffer buffer) throws Exception { 21 | onTunnelEstablished(); 22 | } 23 | 24 | @Override 25 | protected void beforeSend(ByteBuffer buffer) throws Exception { 26 | // TODO Auto-generated method stub 27 | 28 | } 29 | 30 | @Override 31 | protected void afterReceived(ByteBuffer buffer) throws Exception { 32 | // TODO Auto-generated method stub 33 | 34 | } 35 | 36 | @Override 37 | protected boolean isTunnelEstablished() { 38 | return true; 39 | } 40 | 41 | @Override 42 | protected void onDispose() { 43 | // TODO Auto-generated method stub 44 | 45 | } 46 | 47 | } 48 | -------------------------------------------------------------------------------- /app/src/main/java/com/vm/shadowsocks/tunnel/Tunnel.java: -------------------------------------------------------------------------------- 1 | package com.vm.shadowsocks.tunnel; 2 | 3 | import android.annotation.SuppressLint; 4 | 5 | import com.vm.shadowsocks.core.LocalVpnService; 6 | import com.vm.shadowsocks.core.ProxyConfig; 7 | 8 | import java.io.IOException; 9 | import java.net.InetSocketAddress; 10 | import java.nio.ByteBuffer; 11 | import java.nio.channels.SelectionKey; 12 | import java.nio.channels.Selector; 13 | import java.nio.channels.SocketChannel; 14 | 15 | public abstract class Tunnel { 16 | 17 | final static ByteBuffer GL_BUFFER = ByteBuffer.allocate(20000); 18 | public static long SessionCount; 19 | 20 | protected abstract void onConnected(ByteBuffer buffer) throws Exception; 21 | 22 | protected abstract boolean isTunnelEstablished(); 23 | 24 | protected abstract void beforeSend(ByteBuffer buffer) throws Exception; 25 | 26 | protected abstract void afterReceived(ByteBuffer buffer) throws Exception; 27 | 28 | protected abstract void onDispose(); 29 | 30 | private SocketChannel m_InnerChannel; 31 | private ByteBuffer m_SendRemainBuffer; 32 | private Selector m_Selector; 33 | private Tunnel m_BrotherTunnel; 34 | private boolean m_Disposed; 35 | private InetSocketAddress m_ServerEP; 36 | protected InetSocketAddress m_DestAddress; 37 | 38 | public Tunnel(SocketChannel innerChannel, Selector selector) { 39 | this.m_InnerChannel = innerChannel; 40 | this.m_Selector = selector; 41 | SessionCount++; 42 | } 43 | 44 | public Tunnel(InetSocketAddress serverAddress, Selector selector) throws IOException { 45 | SocketChannel innerChannel = SocketChannel.open(); 46 | innerChannel.configureBlocking(false); 47 | this.m_InnerChannel = innerChannel; 48 | this.m_Selector = selector; 49 | this.m_ServerEP = serverAddress; 50 | SessionCount++; 51 | } 52 | 53 | public void setBrotherTunnel(Tunnel brotherTunnel) { 54 | m_BrotherTunnel = brotherTunnel; 55 | } 56 | 57 | public void connect(InetSocketAddress destAddress) throws Exception { 58 | if (LocalVpnService.Instance.protect(m_InnerChannel.socket())) {//保护socket不走vpn 59 | m_DestAddress = destAddress; 60 | m_InnerChannel.register(m_Selector, SelectionKey.OP_CONNECT, this);//注册连接事件 61 | m_InnerChannel.connect(m_ServerEP);//连接目标 62 | } else { 63 | throw new Exception("VPN protect socket failed."); 64 | } 65 | } 66 | 67 | protected void beginReceive() throws Exception { 68 | if (m_InnerChannel.isBlocking()) { 69 | m_InnerChannel.configureBlocking(false); 70 | } 71 | m_InnerChannel.register(m_Selector, SelectionKey.OP_READ, this);//注册读事件 72 | } 73 | 74 | 75 | protected boolean write(ByteBuffer buffer, boolean copyRemainData) throws Exception { 76 | int bytesSent; 77 | while (buffer.hasRemaining()) { 78 | bytesSent = m_InnerChannel.write(buffer); 79 | if (bytesSent == 0) { 80 | break;//不能再发送了,终止循环 81 | } 82 | } 83 | 84 | if (buffer.hasRemaining()) {//数据没有发送完毕 85 | if (copyRemainData) {//拷贝剩余数据,然后侦听写入事件,待可写入时写入。 86 | //拷贝剩余数据 87 | if (m_SendRemainBuffer == null) { 88 | m_SendRemainBuffer = ByteBuffer.allocate(buffer.capacity()); 89 | } 90 | m_SendRemainBuffer.clear(); 91 | m_SendRemainBuffer.put(buffer); 92 | m_SendRemainBuffer.flip(); 93 | m_InnerChannel.register(m_Selector, SelectionKey.OP_WRITE, this);//注册写事件 94 | } 95 | return false; 96 | } else {//发送完毕了 97 | return true; 98 | } 99 | } 100 | 101 | protected void onTunnelEstablished() throws Exception { 102 | this.beginReceive();//开始接收数据 103 | m_BrotherTunnel.beginReceive();//兄弟也开始收数据吧 104 | } 105 | 106 | @SuppressLint("DefaultLocale") 107 | public void onConnectable() { 108 | try { 109 | if (m_InnerChannel.finishConnect()) {//连接成功 110 | onConnected(GL_BUFFER);//通知子类TCP已连接,子类可以根据协议实现握手等。 111 | } else {//连接失败 112 | LocalVpnService.Instance.writeLog("Error: connect to %s failed.", m_ServerEP); 113 | this.dispose(); 114 | } 115 | } catch (Exception e) { 116 | LocalVpnService.Instance.writeLog("Error: connect to %s failed: %s", m_ServerEP, e); 117 | this.dispose(); 118 | } 119 | } 120 | 121 | public void onReadable(SelectionKey key) { 122 | try { 123 | ByteBuffer buffer = GL_BUFFER; 124 | buffer.clear(); 125 | int bytesRead = m_InnerChannel.read(buffer); 126 | if (bytesRead > 0) { 127 | buffer.flip(); 128 | afterReceived(buffer);//先让子类处理,例如解密数据。 129 | if (isTunnelEstablished() && buffer.hasRemaining()) {//将读到的数据,转发给兄弟。 130 | m_BrotherTunnel.beforeSend(buffer);//发送之前,先让子类处理,例如做加密等。 131 | if (!m_BrotherTunnel.write(buffer, true)) { 132 | key.cancel();//兄弟吃不消,就取消读取事件。 133 | if (ProxyConfig.IS_DEBUG) 134 | System.out.printf("%s can not read more.\n", m_ServerEP); 135 | } 136 | } 137 | } else if (bytesRead < 0) { 138 | this.dispose();//连接已关闭,释放资源。 139 | } 140 | } catch (Exception e) { 141 | e.printStackTrace(); 142 | this.dispose(); 143 | } 144 | } 145 | 146 | public void onWritable(SelectionKey key) { 147 | try { 148 | this.beforeSend(m_SendRemainBuffer);//发送之前,先让子类处理,例如做加密等。 149 | if (this.write(m_SendRemainBuffer, false)) {//如果剩余数据已经发送完毕 150 | key.cancel();//取消写事件。 151 | if (isTunnelEstablished()) { 152 | m_BrotherTunnel.beginReceive();//这边数据发送完毕,通知兄弟可以收数据了。 153 | } else { 154 | this.beginReceive();//开始接收代理服务器响应数据 155 | } 156 | } 157 | } catch (Exception e) { 158 | this.dispose(); 159 | } 160 | } 161 | 162 | public void dispose() { 163 | disposeInternal(true); 164 | } 165 | 166 | void disposeInternal(boolean disposeBrother) { 167 | if (m_Disposed) { 168 | return; 169 | } else { 170 | try { 171 | m_InnerChannel.close(); 172 | } catch (Exception e) { 173 | } 174 | 175 | if (m_BrotherTunnel != null && disposeBrother) { 176 | m_BrotherTunnel.disposeInternal(false);//把兄弟的资源也释放了。 177 | } 178 | 179 | m_InnerChannel = null; 180 | m_SendRemainBuffer = null; 181 | m_Selector = null; 182 | m_BrotherTunnel = null; 183 | m_Disposed = true; 184 | SessionCount--; 185 | 186 | onDispose(); 187 | } 188 | } 189 | } 190 | -------------------------------------------------------------------------------- /app/src/main/java/com/vm/shadowsocks/tunnel/httpconnect/HttpConnectConfig.java: -------------------------------------------------------------------------------- 1 | package com.vm.shadowsocks.tunnel.httpconnect; 2 | 3 | import android.net.Uri; 4 | 5 | import com.vm.shadowsocks.tunnel.Config; 6 | 7 | import java.net.InetSocketAddress; 8 | 9 | public class HttpConnectConfig extends Config { 10 | public String UserName; 11 | public String Password; 12 | 13 | public static HttpConnectConfig parse(String proxyInfo) { 14 | HttpConnectConfig config = new HttpConnectConfig(); 15 | Uri uri = Uri.parse(proxyInfo); 16 | String userInfoString = uri.getUserInfo(); 17 | if (userInfoString != null) { 18 | String[] userStrings = userInfoString.split(":"); 19 | config.UserName = userStrings[0]; 20 | if (userStrings.length >= 2) { 21 | config.Password = userStrings[1]; 22 | } 23 | } 24 | config.ServerAddress = new InetSocketAddress(uri.getHost(), uri.getPort()); 25 | return config; 26 | } 27 | 28 | @Override 29 | public boolean equals(Object o) { 30 | if (o == null) 31 | return false; 32 | return this.toString().equals(o.toString()); 33 | } 34 | 35 | @Override 36 | public String toString() { 37 | return String.format("http://%s:%s@%s", UserName, Password, ServerAddress); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /app/src/main/java/com/vm/shadowsocks/tunnel/httpconnect/HttpConnectTunnel.java: -------------------------------------------------------------------------------- 1 | package com.vm.shadowsocks.tunnel.httpconnect; 2 | 3 | import com.vm.shadowsocks.core.ProxyConfig; 4 | import com.vm.shadowsocks.tunnel.Tunnel; 5 | 6 | import java.io.IOException; 7 | import java.nio.ByteBuffer; 8 | import java.nio.channels.Selector; 9 | 10 | public class HttpConnectTunnel extends Tunnel { 11 | 12 | private boolean m_TunnelEstablished; 13 | private HttpConnectConfig m_Config; 14 | 15 | public HttpConnectTunnel(HttpConnectConfig config, Selector selector) throws IOException { 16 | super(config.ServerAddress, selector); 17 | m_Config = config; 18 | } 19 | 20 | @Override 21 | protected void onConnected(ByteBuffer buffer) throws Exception { 22 | String request = String.format("CONNECT %s:%d HTTP/1.0\r\nProxy-Connection: keep-alive\r\nUser-Agent: %s\r\nX-App-Install-ID: %s\r\n\r\n", 23 | m_DestAddress.getHostName(), 24 | m_DestAddress.getPort(), 25 | ProxyConfig.Instance.getUserAgent(), 26 | ProxyConfig.AppInstallID); 27 | 28 | buffer.clear(); 29 | buffer.put(request.getBytes()); 30 | buffer.flip(); 31 | if (this.write(buffer, true)) {//发送连接请求到代理服务器 32 | this.beginReceive();//开始接收代理服务器响应数据 33 | } 34 | } 35 | 36 | void trySendPartOfHeader(ByteBuffer buffer) throws Exception { 37 | int bytesSent = 0; 38 | if (buffer.remaining() > 10) { 39 | int pos = buffer.position() + buffer.arrayOffset(); 40 | String firString = new String(buffer.array(), pos, 10).toUpperCase(); 41 | if (firString.startsWith("GET /") || firString.startsWith("POST /")) { 42 | int limit = buffer.limit(); 43 | buffer.limit(buffer.position() + 10); 44 | super.write(buffer, false); 45 | bytesSent = 10 - buffer.remaining(); 46 | buffer.limit(limit); 47 | if (ProxyConfig.IS_DEBUG) 48 | System.out.printf("Send %d bytes(%s) to %s\n", bytesSent, firString, m_DestAddress); 49 | } 50 | } 51 | } 52 | 53 | 54 | @Override 55 | protected void beforeSend(ByteBuffer buffer) throws Exception { 56 | if (ProxyConfig.Instance.isIsolateHttpHostHeader()) { 57 | trySendPartOfHeader(buffer);//尝试发送请求头的一部分,让请求头的host在第二个包里面发送,从而绕过机房的白名单机制。 58 | } 59 | } 60 | 61 | @Override 62 | protected void afterReceived(ByteBuffer buffer) throws Exception { 63 | if (!m_TunnelEstablished) { 64 | //收到代理服务器响应数据 65 | //分析响应并判断是否连接成功 66 | String response = new String(buffer.array(), buffer.position(), 12); 67 | if (response.matches("^HTTP/1.[01] 200$")) { 68 | buffer.limit(buffer.position()); 69 | } else { 70 | throw new Exception(String.format("Proxy server responsed an error: %s", response)); 71 | } 72 | 73 | m_TunnelEstablished = true; 74 | super.onTunnelEstablished(); 75 | } 76 | } 77 | 78 | @Override 79 | protected boolean isTunnelEstablished() { 80 | return m_TunnelEstablished; 81 | } 82 | 83 | @Override 84 | protected void onDispose() { 85 | m_Config = null; 86 | } 87 | 88 | 89 | } 90 | -------------------------------------------------------------------------------- /app/src/main/java/com/vm/shadowsocks/tunnel/shadowsocks/AesCrypt.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2015, Blake 3 | * All Rights Reserved. 4 | * 5 | * Redistribution and use in source and binary forms, with or without 6 | * modification, are permitted provided that the following conditions are met: 7 | * 8 | * 1. Redistributions of source code must retain the above copyright notice, 9 | * this list of conditions and the following disclaimer. 10 | * 11 | * 2. Redistributions in binary form must reproduce the above copyright notice, 12 | * this list of conditions and the following disclaimer in the documentation 13 | * and/or other materials provided with the distribution. 14 | * 15 | * 3. The name of the author may not be used to endorse or promote 16 | * products derived from this software without specific prior 17 | * written permission. 18 | * 19 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 20 | * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 21 | * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 22 | * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE 23 | * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR 24 | * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF 25 | * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS 26 | * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 27 | * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 28 | * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 29 | * POSSIBILITY OF SUCH DAMAGE. 30 | */ 31 | 32 | package com.vm.shadowsocks.tunnel.shadowsocks; 33 | 34 | import org.bouncycastle.crypto.StreamBlockCipher; 35 | import org.bouncycastle.crypto.engines.AESFastEngine; 36 | import org.bouncycastle.crypto.modes.CFBBlockCipher; 37 | import org.bouncycastle.crypto.modes.OFBBlockCipher; 38 | 39 | import java.io.ByteArrayOutputStream; 40 | import java.security.InvalidAlgorithmParameterException; 41 | import java.util.HashMap; 42 | import java.util.Map; 43 | 44 | import javax.crypto.SecretKey; 45 | import javax.crypto.spec.SecretKeySpec; 46 | 47 | /** 48 | * AES Crypt implementation 49 | */ 50 | public class AesCrypt extends CryptBase { 51 | 52 | public final static String CIPHER_AES_128_CFB = "aes-128-cfb"; 53 | public final static String CIPHER_AES_192_CFB = "aes-192-cfb"; 54 | public final static String CIPHER_AES_256_CFB = "aes-256-cfb"; 55 | public final static String CIPHER_AES_128_OFB = "aes-128-ofb"; 56 | public final static String CIPHER_AES_192_OFB = "aes-192-ofb"; 57 | public final static String CIPHER_AES_256_OFB = "aes-256-ofb"; 58 | 59 | public static Map getCiphers() { 60 | Map ciphers = new HashMap(); 61 | ciphers.put(CIPHER_AES_128_CFB, AesCrypt.class.getName()); 62 | ciphers.put(CIPHER_AES_192_CFB, AesCrypt.class.getName()); 63 | ciphers.put(CIPHER_AES_256_CFB, AesCrypt.class.getName()); 64 | ciphers.put(CIPHER_AES_128_OFB, AesCrypt.class.getName()); 65 | ciphers.put(CIPHER_AES_192_OFB, AesCrypt.class.getName()); 66 | ciphers.put(CIPHER_AES_256_OFB, AesCrypt.class.getName()); 67 | 68 | return ciphers; 69 | } 70 | 71 | public AesCrypt(String name, String password) { 72 | super(name, password); 73 | } 74 | 75 | @Override 76 | public int getKeyLength() { 77 | if (_name.equals(CIPHER_AES_128_CFB) || _name.equals(CIPHER_AES_128_OFB)) { 78 | return 16; 79 | } else if (_name.equals(CIPHER_AES_192_CFB) || _name.equals(CIPHER_AES_192_OFB)) { 80 | return 24; 81 | } else if (_name.equals(CIPHER_AES_256_CFB) || _name.equals(CIPHER_AES_256_OFB)) { 82 | return 32; 83 | } 84 | 85 | return 0; 86 | } 87 | 88 | @Override 89 | protected StreamBlockCipher getCipher(boolean isEncrypted) throws InvalidAlgorithmParameterException { 90 | AESFastEngine engine = new AESFastEngine(); 91 | StreamBlockCipher cipher; 92 | 93 | if (_name.equals(CIPHER_AES_128_CFB)) { 94 | cipher = new CFBBlockCipher(engine, getIVLength() * 8); 95 | } else if (_name.equals(CIPHER_AES_192_CFB)) { 96 | cipher = new CFBBlockCipher(engine, getIVLength() * 8); 97 | } else if (_name.equals(CIPHER_AES_256_CFB)) { 98 | cipher = new CFBBlockCipher(engine, getIVLength() * 8); 99 | } else if (_name.equals(CIPHER_AES_128_OFB)) { 100 | cipher = new OFBBlockCipher(engine, getIVLength() * 8); 101 | } else if (_name.equals(CIPHER_AES_192_OFB)) { 102 | cipher = new OFBBlockCipher(engine, getIVLength() * 8); 103 | } else if (_name.equals(CIPHER_AES_256_OFB)) { 104 | cipher = new OFBBlockCipher(engine, getIVLength() * 8); 105 | } else { 106 | throw new InvalidAlgorithmParameterException(_name); 107 | } 108 | 109 | return cipher; 110 | } 111 | 112 | @Override 113 | public int getIVLength() { 114 | return 16; 115 | } 116 | 117 | @Override 118 | protected SecretKey getKey() { 119 | return new SecretKeySpec(_ssKey.getEncoded(), "AES"); 120 | } 121 | 122 | @Override 123 | protected void _encrypt(byte[] data, ByteArrayOutputStream stream) { 124 | int noBytesProcessed; 125 | byte[] buffer = new byte[data.length]; 126 | 127 | noBytesProcessed = encCipher.processBytes(data, 0, data.length, buffer, 0); 128 | stream.write(buffer, 0, noBytesProcessed); 129 | } 130 | 131 | @Override 132 | protected void _decrypt(byte[] data, ByteArrayOutputStream stream) { 133 | int noBytesProcessed; 134 | byte[] buffer = new byte[data.length]; 135 | 136 | noBytesProcessed = decCipher.processBytes(data, 0, data.length, buffer, 0); 137 | stream.write(buffer, 0, noBytesProcessed); 138 | } 139 | } 140 | -------------------------------------------------------------------------------- /app/src/main/java/com/vm/shadowsocks/tunnel/shadowsocks/BlowFishCrypt.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2015, Blake 3 | * All Rights Reserved. 4 | * 5 | * Redistribution and use in source and binary forms, with or without 6 | * modification, are permitted provided that the following conditions are met: 7 | * 8 | * 1. Redistributions of source code must retain the above copyright notice, 9 | * this list of conditions and the following disclaimer. 10 | * 11 | * 2. Redistributions in binary form must reproduce the above copyright notice, 12 | * this list of conditions and the following disclaimer in the documentation 13 | * and/or other materials provided with the distribution. 14 | * 15 | * 3. The name of the author may not be used to endorse or promote 16 | * products derived from this software without specific prior 17 | * written permission. 18 | * 19 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 20 | * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 21 | * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 22 | * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE 23 | * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR 24 | * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF 25 | * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS 26 | * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 27 | * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 28 | * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 29 | * POSSIBILITY OF SUCH DAMAGE. 30 | */ 31 | 32 | package com.vm.shadowsocks.tunnel.shadowsocks; 33 | 34 | import org.bouncycastle.crypto.StreamBlockCipher; 35 | import org.bouncycastle.crypto.engines.BlowfishEngine; 36 | import org.bouncycastle.crypto.modes.CFBBlockCipher; 37 | 38 | import java.io.ByteArrayOutputStream; 39 | import java.security.InvalidAlgorithmParameterException; 40 | import java.util.HashMap; 41 | import java.util.Map; 42 | 43 | import javax.crypto.SecretKey; 44 | import javax.crypto.spec.SecretKeySpec; 45 | 46 | /** 47 | * Blow fish cipher implementation 48 | */ 49 | public class BlowFishCrypt extends CryptBase { 50 | 51 | public final static String CIPHER_BLOWFISH_CFB = "bf-cfb"; 52 | 53 | public static Map getCiphers() { 54 | Map ciphers = new HashMap(); 55 | ciphers.put(CIPHER_BLOWFISH_CFB, BlowFishCrypt.class.getName()); 56 | 57 | return ciphers; 58 | } 59 | 60 | public BlowFishCrypt(String name, String password) { 61 | super(name, password); 62 | } 63 | 64 | @Override 65 | public int getKeyLength() { 66 | return 16; 67 | } 68 | 69 | @Override 70 | protected StreamBlockCipher getCipher(boolean isEncrypted) throws InvalidAlgorithmParameterException { 71 | BlowfishEngine engine = new BlowfishEngine(); 72 | StreamBlockCipher cipher; 73 | 74 | if (_name.equals(CIPHER_BLOWFISH_CFB)) { 75 | cipher = new CFBBlockCipher(engine, getIVLength() * 8); 76 | } else { 77 | throw new InvalidAlgorithmParameterException(_name); 78 | } 79 | 80 | return cipher; 81 | } 82 | 83 | @Override 84 | public int getIVLength() { 85 | return 8; 86 | } 87 | 88 | @Override 89 | protected SecretKey getKey() { 90 | return new SecretKeySpec(_ssKey.getEncoded(), "AES"); 91 | } 92 | 93 | @Override 94 | protected void _encrypt(byte[] data, ByteArrayOutputStream stream) { 95 | int noBytesProcessed; 96 | byte[] buffer = new byte[data.length]; 97 | 98 | noBytesProcessed = encCipher.processBytes(data, 0, data.length, buffer, 0); 99 | stream.write(buffer, 0, noBytesProcessed); 100 | } 101 | 102 | @Override 103 | protected void _decrypt(byte[] data, ByteArrayOutputStream stream) { 104 | int noBytesProcessed; 105 | byte[] buffer = new byte[data.length]; 106 | 107 | noBytesProcessed = decCipher.processBytes(data, 0, data.length, buffer, 0); 108 | stream.write(buffer, 0, noBytesProcessed); 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /app/src/main/java/com/vm/shadowsocks/tunnel/shadowsocks/CamelliaCrypt.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2015, Blake 3 | * All Rights Reserved. 4 | * 5 | * Redistribution and use in source and binary forms, with or without 6 | * modification, are permitted provided that the following conditions are met: 7 | * 8 | * 1. Redistributions of source code must retain the above copyright notice, 9 | * this list of conditions and the following disclaimer. 10 | * 11 | * 2. Redistributions in binary form must reproduce the above copyright notice, 12 | * this list of conditions and the following disclaimer in the documentation 13 | * and/or other materials provided with the distribution. 14 | * 15 | * 3. The name of the author may not be used to endorse or promote 16 | * products derived from this software without specific prior 17 | * written permission. 18 | * 19 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 20 | * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 21 | * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 22 | * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE 23 | * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR 24 | * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF 25 | * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS 26 | * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 27 | * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 28 | * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 29 | * POSSIBILITY OF SUCH DAMAGE. 30 | */ 31 | 32 | package com.vm.shadowsocks.tunnel.shadowsocks; 33 | 34 | import org.bouncycastle.crypto.StreamBlockCipher; 35 | import org.bouncycastle.crypto.engines.CamelliaEngine; 36 | import org.bouncycastle.crypto.modes.CFBBlockCipher; 37 | 38 | import java.io.ByteArrayOutputStream; 39 | import java.security.InvalidAlgorithmParameterException; 40 | import java.util.HashMap; 41 | import java.util.Map; 42 | 43 | import javax.crypto.SecretKey; 44 | import javax.crypto.spec.SecretKeySpec; 45 | 46 | /** 47 | * Camellia cipher implementation 48 | */ 49 | public class CamelliaCrypt extends CryptBase { 50 | 51 | public final static String CIPHER_CAMELLIA_128_CFB = "camellia-128-cfb"; 52 | public final static String CIPHER_CAMELLIA_192_CFB = "camellia-192-cfb"; 53 | public final static String CIPHER_CAMELLIA_256_CFB = "camellia-256-cfb"; 54 | 55 | public static Map getCiphers() { 56 | Map ciphers = new HashMap(); 57 | ciphers.put(CIPHER_CAMELLIA_128_CFB, CamelliaCrypt.class.getName()); 58 | ciphers.put(CIPHER_CAMELLIA_192_CFB, CamelliaCrypt.class.getName()); 59 | ciphers.put(CIPHER_CAMELLIA_256_CFB, CamelliaCrypt.class.getName()); 60 | 61 | return ciphers; 62 | } 63 | 64 | public CamelliaCrypt(String name, String password) { 65 | super(name, password); 66 | } 67 | 68 | @Override 69 | public int getKeyLength() { 70 | if (_name.equals(CIPHER_CAMELLIA_128_CFB)) { 71 | return 16; 72 | } else if (_name.equals(CIPHER_CAMELLIA_192_CFB)) { 73 | return 24; 74 | } else if (_name.equals(CIPHER_CAMELLIA_256_CFB)) { 75 | return 32; 76 | } 77 | 78 | return 0; 79 | } 80 | 81 | @Override 82 | protected StreamBlockCipher getCipher(boolean isEncrypted) throws InvalidAlgorithmParameterException { 83 | CamelliaEngine engine = new CamelliaEngine(); 84 | StreamBlockCipher cipher; 85 | 86 | if (_name.equals(CIPHER_CAMELLIA_128_CFB)) { 87 | cipher = new CFBBlockCipher(engine, getIVLength() * 8); 88 | } else if (_name.equals(CIPHER_CAMELLIA_192_CFB)) { 89 | cipher = new CFBBlockCipher(engine, getIVLength() * 8); 90 | } else if (_name.equals(CIPHER_CAMELLIA_256_CFB)) { 91 | cipher = new CFBBlockCipher(engine, getIVLength() * 8); 92 | } else { 93 | throw new InvalidAlgorithmParameterException(_name); 94 | } 95 | 96 | return cipher; 97 | } 98 | 99 | @Override 100 | public int getIVLength() { 101 | return 16; 102 | } 103 | 104 | @Override 105 | protected SecretKey getKey() { 106 | return new SecretKeySpec(_ssKey.getEncoded(), "AES"); 107 | } 108 | 109 | @Override 110 | protected void _encrypt(byte[] data, ByteArrayOutputStream stream) { 111 | int noBytesProcessed; 112 | byte[] buffer = new byte[data.length]; 113 | 114 | noBytesProcessed = encCipher.processBytes(data, 0, data.length, buffer, 0); 115 | stream.write(buffer, 0, noBytesProcessed); 116 | } 117 | 118 | @Override 119 | protected void _decrypt(byte[] data, ByteArrayOutputStream stream) { 120 | int noBytesProcessed; 121 | byte[] buffer = new byte[data.length]; 122 | 123 | noBytesProcessed = decCipher.processBytes(data, 0, data.length, buffer, 0); 124 | stream.write(buffer, 0, noBytesProcessed); 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /app/src/main/java/com/vm/shadowsocks/tunnel/shadowsocks/Chacha20Crypt.java: -------------------------------------------------------------------------------- 1 | package com.vm.shadowsocks.tunnel.shadowsocks; 2 | 3 | import org.bouncycastle.crypto.StreamCipher; 4 | import org.bouncycastle.crypto.engines.ChaChaEngine; 5 | import org.bouncycastle.crypto.engines.ChaCha7539Engine; 6 | 7 | import java.io.ByteArrayOutputStream; 8 | import java.security.InvalidAlgorithmParameterException; 9 | import java.util.HashMap; 10 | import java.util.Map; 11 | 12 | import javax.crypto.SecretKey; 13 | import javax.crypto.spec.SecretKeySpec; 14 | 15 | public class Chacha20Crypt extends CryptBase { 16 | public final static String CIPHER_CHACHA20 = "chacha20"; 17 | public final static String CIPHER_CHACHA20_IETF = "chacha20-ietf"; 18 | 19 | public static Map getCiphers() { 20 | Map ciphers = new HashMap<>(); 21 | ciphers.put(CIPHER_CHACHA20, Chacha20Crypt.class.getName()); 22 | ciphers.put(CIPHER_CHACHA20_IETF, Chacha20Crypt.class.getName()); 23 | return ciphers; 24 | } 25 | 26 | public Chacha20Crypt(String name, String password) { 27 | super(name, password); 28 | } 29 | 30 | @Override 31 | protected StreamCipher getCipher(boolean isEncrypted) throws InvalidAlgorithmParameterException { 32 | if (_name.equals(CIPHER_CHACHA20)) { 33 | return new ChaChaEngine(); 34 | } 35 | else if (_name.equals(CIPHER_CHACHA20_IETF)) { 36 | return new ChaCha7539Engine(); 37 | } 38 | return null; 39 | } 40 | 41 | @Override 42 | protected SecretKey getKey() { 43 | return new SecretKeySpec(_ssKey.getEncoded(), "AES"); 44 | 45 | } 46 | 47 | @Override 48 | protected void _encrypt(byte[] data, ByteArrayOutputStream stream) { 49 | int noBytesProcessed; 50 | byte[] buffer = new byte[data.length]; 51 | 52 | noBytesProcessed = encCipher.processBytes(data, 0, data.length, buffer, 0); 53 | stream.write(buffer, 0, noBytesProcessed); 54 | } 55 | 56 | @Override 57 | protected void _decrypt(byte[] data, ByteArrayOutputStream stream) { 58 | int BytesProcessedNum; 59 | byte[] buffer = new byte[data.length]; 60 | BytesProcessedNum = decCipher.processBytes(data, 0, data.length, buffer, 0); 61 | stream.write(buffer, 0, BytesProcessedNum); 62 | 63 | } 64 | 65 | @Override 66 | public int getKeyLength() { 67 | if (_name.equals(CIPHER_CHACHA20) || _name.equals(CIPHER_CHACHA20_IETF)) { 68 | return 32; 69 | } 70 | return 0; 71 | } 72 | 73 | @Override 74 | public int getIVLength() { 75 | if (_name.equals(CIPHER_CHACHA20)) { 76 | return 8; 77 | } 78 | else if (_name.equals(CIPHER_CHACHA20_IETF)) { 79 | return 12; 80 | } 81 | return 0; 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /app/src/main/java/com/vm/shadowsocks/tunnel/shadowsocks/CryptBase.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2015, Blake 3 | * All Rights Reserved. 4 | * 5 | * Redistribution and use in source and binary forms, with or without 6 | * modification, are permitted provided that the following conditions are met: 7 | * 8 | * 1. Redistributions of source code must retain the above copyright notice, 9 | * this list of conditions and the following disclaimer. 10 | * 11 | * 2. Redistributions in binary form must reproduce the above copyright notice, 12 | * this list of conditions and the following disclaimer in the documentation 13 | * and/or other materials provided with the distribution. 14 | * 15 | * 3. The name of the author may not be used to endorse or promote 16 | * products derived from this software without specific prior 17 | * written permission. 18 | * 19 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 20 | * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 21 | * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 22 | * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE 23 | * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR 24 | * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF 25 | * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS 26 | * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 27 | * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 28 | * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 29 | * POSSIBILITY OF SUCH DAMAGE. 30 | */ 31 | 32 | package com.vm.shadowsocks.tunnel.shadowsocks; 33 | 34 | import org.bouncycastle.crypto.CipherParameters; 35 | import org.bouncycastle.crypto.StreamCipher; 36 | import org.bouncycastle.crypto.params.KeyParameter; 37 | import org.bouncycastle.crypto.params.ParametersWithIV; 38 | 39 | import java.io.ByteArrayOutputStream; 40 | import java.io.IOException; 41 | import java.security.InvalidAlgorithmParameterException; 42 | import java.security.MessageDigest; 43 | import java.security.SecureRandom; 44 | import java.util.concurrent.locks.Lock; 45 | import java.util.concurrent.locks.ReentrantLock; 46 | import java.util.logging.Logger; 47 | 48 | import javax.crypto.SecretKey; 49 | 50 | /** 51 | * Crypt base class implementation 52 | */ 53 | public abstract class CryptBase implements ICrypt { 54 | 55 | protected abstract StreamCipher getCipher(boolean isEncrypted) throws InvalidAlgorithmParameterException; 56 | 57 | protected abstract SecretKey getKey(); 58 | 59 | protected abstract void _encrypt(byte[] data, ByteArrayOutputStream stream); 60 | 61 | protected abstract void _decrypt(byte[] data, ByteArrayOutputStream stream); 62 | 63 | protected CipherParameters getCipherParameters(byte[] iv){ 64 | _decryptIV = new byte[_ivLength]; 65 | System.arraycopy(iv, 0, _decryptIV, 0, _ivLength); 66 | return new ParametersWithIV(new KeyParameter(_key.getEncoded()), _decryptIV); 67 | } 68 | 69 | protected final String _name; 70 | protected final SecretKey _key; 71 | protected final ShadowSocksKey _ssKey; 72 | protected final int _ivLength; 73 | protected final int _keyLength; 74 | protected boolean _encryptIVSet; 75 | protected boolean _decryptIVSet; 76 | protected byte[] _encryptIV; 77 | protected byte[] _decryptIV; 78 | protected final Lock encLock = new ReentrantLock(); 79 | protected final Lock decLock = new ReentrantLock(); 80 | protected StreamCipher encCipher; 81 | protected StreamCipher decCipher; 82 | private Logger logger = Logger.getLogger(CryptBase.class.getName()); 83 | 84 | public CryptBase(String name, String password) { 85 | _name = name.toLowerCase(); 86 | _ivLength = getIVLength(); 87 | _keyLength = getKeyLength(); 88 | _ssKey = new ShadowSocksKey(password, _keyLength); 89 | _key = getKey(); 90 | } 91 | 92 | protected void setIV(byte[] iv, boolean isEncrypt) { 93 | if (_ivLength == 0) { 94 | return; 95 | } 96 | 97 | CipherParameters cipherParameters = null; 98 | 99 | if (isEncrypt) { 100 | cipherParameters = getCipherParameters(iv); 101 | try { 102 | encCipher = getCipher(isEncrypt); 103 | } catch (InvalidAlgorithmParameterException e) { 104 | logger.info(e.toString()); 105 | } 106 | encCipher.init(isEncrypt, cipherParameters); 107 | } else { 108 | _decryptIV = new byte[_ivLength]; 109 | System.arraycopy(iv, 0, _decryptIV, 0, _ivLength); 110 | cipherParameters = getCipherParameters(iv); 111 | try { 112 | decCipher = getCipher(isEncrypt); 113 | } catch (InvalidAlgorithmParameterException e) { 114 | logger.info(e.toString()); 115 | } 116 | decCipher.init(isEncrypt, cipherParameters); 117 | } 118 | } 119 | 120 | public byte[] encrypt(byte[] data) { 121 | ByteArrayOutputStream stream = new ByteArrayOutputStream(); 122 | encrypt(data, stream); 123 | return stream.toByteArray(); 124 | } 125 | 126 | public byte[] decrypt(byte[] data) { 127 | ByteArrayOutputStream stream = new ByteArrayOutputStream(); 128 | decrypt(data, stream); 129 | return stream.toByteArray(); 130 | } 131 | 132 | @Override 133 | public void encrypt(byte[] data, ByteArrayOutputStream stream) { 134 | synchronized (encLock) { 135 | stream.reset(); 136 | if (!_encryptIVSet) { 137 | _encryptIVSet = true; 138 | byte[] iv = new byte[_ivLength]; 139 | new SecureRandom().nextBytes(iv); 140 | setIV(iv, true); 141 | try { 142 | stream.write(iv); 143 | } catch (IOException e) { 144 | logger.info(e.toString()); 145 | } 146 | 147 | } 148 | 149 | _encrypt(data, stream); 150 | } 151 | } 152 | 153 | @Override 154 | public void encrypt(byte[] data, int length, ByteArrayOutputStream stream) { 155 | byte[] d = new byte[length]; 156 | System.arraycopy(data, 0, d, 0, length); 157 | encrypt(d, stream); 158 | } 159 | 160 | @Override 161 | public void decrypt(byte[] data, ByteArrayOutputStream stream) { 162 | byte[] temp; 163 | 164 | synchronized (decLock) { 165 | stream.reset(); 166 | if (!_decryptIVSet) { 167 | _decryptIVSet = true; 168 | setIV(data, false); 169 | temp = new byte[data.length - _ivLength]; 170 | System.arraycopy(data, _ivLength, temp, 0, data.length - _ivLength); 171 | } else { 172 | temp = data; 173 | } 174 | 175 | _decrypt(temp, stream); 176 | } 177 | } 178 | 179 | @Override 180 | public void decrypt(byte[] data, int length, ByteArrayOutputStream stream) { 181 | byte[] d = new byte[length]; 182 | System.arraycopy(data, 0, d, 0, length); 183 | decrypt(d, stream); 184 | } 185 | 186 | 187 | public static byte[] md5Digest(byte[] input) { 188 | try { 189 | MessageDigest md5 = MessageDigest.getInstance("MD5"); 190 | return md5.digest(input); 191 | } catch (Exception e) { 192 | throw new RuntimeException(e); 193 | } 194 | } 195 | } 196 | -------------------------------------------------------------------------------- /app/src/main/java/com/vm/shadowsocks/tunnel/shadowsocks/CryptFactory.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2015, Blake 3 | * All Rights Reserved. 4 | * 5 | * Redistribution and use in source and binary forms, with or without 6 | * modification, are permitted provided that the following conditions are met: 7 | * 8 | * 1. Redistributions of source code must retain the above copyright notice, 9 | * this list of conditions and the following disclaimer. 10 | * 11 | * 2. Redistributions in binary form must reproduce the above copyright notice, 12 | * this list of conditions and the following disclaimer in the documentation 13 | * and/or other materials provided with the distribution. 14 | * 15 | * 3. The name of the author may not be used to endorse or promote 16 | * products derived from this software without specific prior 17 | * written permission. 18 | * 19 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 20 | * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 21 | * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 22 | * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE 23 | * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR 24 | * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF 25 | * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS 26 | * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 27 | * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 28 | * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 29 | * POSSIBILITY OF SUCH DAMAGE. 30 | */ 31 | 32 | package com.vm.shadowsocks.tunnel.shadowsocks; 33 | 34 | import java.lang.reflect.Constructor; 35 | import java.util.ArrayList; 36 | import java.util.Collections; 37 | import java.util.HashMap; 38 | import java.util.List; 39 | import java.util.Map; 40 | import java.util.logging.Logger; 41 | 42 | /** 43 | * Crypt factory 44 | */ 45 | public class CryptFactory { 46 | private static final Map crypts = new HashMap() {{ 47 | putAll(AesCrypt.getCiphers()); 48 | putAll(CamelliaCrypt.getCiphers()); 49 | putAll(BlowFishCrypt.getCiphers()); 50 | putAll(SeedCrypt.getCiphers()); 51 | putAll(Chacha20Crypt.getCiphers()); 52 | putAll(Rc4Md5Crypt.getCiphers()); 53 | // TODO: other crypts 54 | }}; 55 | private static Logger logger = Logger.getLogger(CryptFactory.class.getName()); 56 | 57 | public static boolean isCipherExisted(String name) { 58 | return (crypts.get(name) != null); 59 | } 60 | 61 | public static ICrypt get(String name, String password) { 62 | try { 63 | Object obj = getObj(crypts.get(name), String.class, name, String.class, password); 64 | return (ICrypt) obj; 65 | 66 | } catch (Exception e) { 67 | logger.info(e.getMessage()); 68 | } 69 | 70 | return null; 71 | } 72 | 73 | public static List getSupportedCiphers() { 74 | List sortedKeys = new ArrayList(crypts.keySet()); 75 | Collections.sort(sortedKeys); 76 | return sortedKeys; 77 | } 78 | 79 | public static Object getObj(String className, Object... args) { 80 | Object retValue = null; 81 | try { 82 | Class c = Class.forName(className); 83 | if (args.length == 0) { 84 | retValue = c.newInstance(); 85 | } else if ((args.length & 1) == 0) { 86 | // args should come with pairs, for example 87 | // String.class, "arg1_value", String.class, "arg2_value" 88 | Class[] oParam = new Class[args.length / 2]; 89 | for (int arg_i = 0, i = 0; arg_i < args.length; arg_i += 2, i++) { 90 | oParam[i] = (Class) args[arg_i]; 91 | } 92 | 93 | Constructor constructor = c.getConstructor(oParam); 94 | Object[] paramObjs = new Object[args.length / 2]; 95 | for (int arg_i = 1, i = 0; arg_i < args.length; arg_i += 2, i++) { 96 | paramObjs[i] = args[arg_i]; 97 | } 98 | retValue = constructor.newInstance(paramObjs); 99 | } 100 | } catch (Exception e) { 101 | e.printStackTrace(); 102 | } 103 | 104 | return retValue; 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /app/src/main/java/com/vm/shadowsocks/tunnel/shadowsocks/ICrypt.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2015, Blake 3 | * All Rights Reserved. 4 | * 5 | * Redistribution and use in source and binary forms, with or without 6 | * modification, are permitted provided that the following conditions are met: 7 | * 8 | * 1. Redistributions of source code must retain the above copyright notice, 9 | * this list of conditions and the following disclaimer. 10 | * 11 | * 2. Redistributions in binary form must reproduce the above copyright notice, 12 | * this list of conditions and the following disclaimer in the documentation 13 | * and/or other materials provided with the distribution. 14 | * 15 | * 3. The name of the author may not be used to endorse or promote 16 | * products derived from this software without specific prior 17 | * written permission. 18 | * 19 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 20 | * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 21 | * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 22 | * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE 23 | * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR 24 | * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF 25 | * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS 26 | * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 27 | * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 28 | * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 29 | * POSSIBILITY OF SUCH DAMAGE. 30 | */ 31 | 32 | package com.vm.shadowsocks.tunnel.shadowsocks; 33 | 34 | import java.io.ByteArrayOutputStream; 35 | 36 | /** 37 | * Interface of crypt 38 | */ 39 | public interface ICrypt { 40 | byte[] encrypt(byte[] data); 41 | 42 | byte[] decrypt(byte[] data); 43 | 44 | void encrypt(byte[] data, ByteArrayOutputStream stream); 45 | 46 | void encrypt(byte[] data, int length, ByteArrayOutputStream stream); 47 | 48 | void decrypt(byte[] data, ByteArrayOutputStream stream); 49 | 50 | void decrypt(byte[] data, int length, ByteArrayOutputStream stream); 51 | 52 | int getIVLength(); 53 | 54 | int getKeyLength(); 55 | } 56 | -------------------------------------------------------------------------------- /app/src/main/java/com/vm/shadowsocks/tunnel/shadowsocks/Rc4Md5Crypt.java: -------------------------------------------------------------------------------- 1 | package com.vm.shadowsocks.tunnel.shadowsocks; 2 | 3 | import org.bouncycastle.crypto.CipherParameters; 4 | import org.bouncycastle.crypto.StreamCipher; 5 | import org.bouncycastle.crypto.engines.RC4Engine; 6 | import org.bouncycastle.crypto.params.KeyParameter; 7 | 8 | import java.io.ByteArrayOutputStream; 9 | import java.security.InvalidAlgorithmParameterException; 10 | import java.util.HashMap; 11 | import java.util.Map; 12 | 13 | import javax.crypto.SecretKey; 14 | import javax.crypto.spec.SecretKeySpec; 15 | 16 | 17 | public class Rc4Md5Crypt extends CryptBase { 18 | public static String CIPHER_RC4_MD5 = "rc4-md5"; 19 | 20 | public static Map getCiphers() { 21 | Map ciphers = new HashMap(); 22 | ciphers.put(CIPHER_RC4_MD5, Rc4Md5Crypt.class.getName()); 23 | return ciphers; 24 | } 25 | 26 | public Rc4Md5Crypt(String name, String password) { 27 | super(name, password); 28 | } 29 | 30 | @Override 31 | protected StreamCipher getCipher(boolean isEncrypted) throws InvalidAlgorithmParameterException { 32 | return new RC4Engine(); 33 | } 34 | 35 | @Override 36 | protected SecretKey getKey() { 37 | return new SecretKeySpec(_ssKey.getEncoded(), "AES"); 38 | } 39 | 40 | @Override 41 | protected void _encrypt(byte[] data, ByteArrayOutputStream stream) { 42 | int noBytesProcessed; 43 | byte[] buffer = new byte[data.length]; 44 | 45 | noBytesProcessed = encCipher.processBytes(data, 0, data.length, buffer, 0); 46 | stream.write(buffer, 0, noBytesProcessed); 47 | } 48 | 49 | @Override 50 | protected void _decrypt(byte[] data, ByteArrayOutputStream stream) { 51 | int noBytesProcessed; 52 | byte[] buffer = new byte[data.length]; 53 | 54 | noBytesProcessed = decCipher.processBytes(data, 0, data.length, buffer, 0); 55 | stream.write(buffer, 0, noBytesProcessed); 56 | } 57 | 58 | @Override 59 | public int getIVLength() { 60 | return 16; 61 | } 62 | 63 | @Override 64 | public int getKeyLength() { 65 | return 16; 66 | } 67 | 68 | @Override 69 | protected CipherParameters getCipherParameters(byte[] iv) { 70 | byte[] bts = new byte[_keyLength + _ivLength]; 71 | System.arraycopy(_key.getEncoded(), 0, bts, 0, _keyLength); 72 | System.arraycopy(iv, 0, bts, _keyLength, _ivLength); 73 | return new KeyParameter(md5Digest(bts)); 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /app/src/main/java/com/vm/shadowsocks/tunnel/shadowsocks/SeedCrypt.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2015, Blake 3 | * All Rights Reserved. 4 | * 5 | * Redistribution and use in source and binary forms, with or without 6 | * modification, are permitted provided that the following conditions are met: 7 | * 8 | * 1. Redistributions of source code must retain the above copyright notice, 9 | * this list of conditions and the following disclaimer. 10 | * 11 | * 2. Redistributions in binary form must reproduce the above copyright notice, 12 | * this list of conditions and the following disclaimer in the documentation 13 | * and/or other materials provided with the distribution. 14 | * 15 | * 3. The name of the author may not be used to endorse or promote 16 | * products derived from this software without specific prior 17 | * written permission. 18 | * 19 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 20 | * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 21 | * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 22 | * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE 23 | * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR 24 | * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF 25 | * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS 26 | * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 27 | * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 28 | * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 29 | * POSSIBILITY OF SUCH DAMAGE. 30 | */ 31 | 32 | package com.vm.shadowsocks.tunnel.shadowsocks; 33 | 34 | import org.bouncycastle.crypto.StreamBlockCipher; 35 | import org.bouncycastle.crypto.engines.SEEDEngine; 36 | import org.bouncycastle.crypto.modes.CFBBlockCipher; 37 | 38 | import java.io.ByteArrayOutputStream; 39 | import java.security.InvalidAlgorithmParameterException; 40 | import java.util.HashMap; 41 | import java.util.Map; 42 | 43 | import javax.crypto.SecretKey; 44 | import javax.crypto.spec.SecretKeySpec; 45 | 46 | /** 47 | * Seed cipher implementation 48 | */ 49 | public class SeedCrypt extends CryptBase { 50 | 51 | public final static String CIPHER_SEED_CFB = "seed-cfb"; 52 | 53 | public static Map getCiphers() { 54 | Map ciphers = new HashMap(); 55 | ciphers.put(CIPHER_SEED_CFB, SeedCrypt.class.getName()); 56 | 57 | return ciphers; 58 | } 59 | 60 | public SeedCrypt(String name, String password) { 61 | super(name, password); 62 | } 63 | 64 | @Override 65 | public int getKeyLength() { 66 | return 16; 67 | } 68 | 69 | @Override 70 | protected StreamBlockCipher getCipher(boolean isEncrypted) throws InvalidAlgorithmParameterException { 71 | SEEDEngine engine = new SEEDEngine(); 72 | StreamBlockCipher cipher; 73 | 74 | if (_name.equals(CIPHER_SEED_CFB)) { 75 | cipher = new CFBBlockCipher(engine, getIVLength() * 8); 76 | } else { 77 | throw new InvalidAlgorithmParameterException(_name); 78 | } 79 | 80 | return cipher; 81 | } 82 | 83 | @Override 84 | public int getIVLength() { 85 | return 16; 86 | } 87 | 88 | @Override 89 | protected SecretKey getKey() { 90 | return new SecretKeySpec(_ssKey.getEncoded(), "AES"); 91 | } 92 | 93 | @Override 94 | protected void _encrypt(byte[] data, ByteArrayOutputStream stream) { 95 | int noBytesProcessed; 96 | byte[] buffer = new byte[data.length]; 97 | 98 | noBytesProcessed = encCipher.processBytes(data, 0, data.length, buffer, 0); 99 | stream.write(buffer, 0, noBytesProcessed); 100 | } 101 | 102 | @Override 103 | protected void _decrypt(byte[] data, ByteArrayOutputStream stream) { 104 | int noBytesProcessed; 105 | byte[] buffer = new byte[data.length]; 106 | 107 | noBytesProcessed = decCipher.processBytes(data, 0, data.length, buffer, 0); 108 | stream.write(buffer, 0, noBytesProcessed); 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /app/src/main/java/com/vm/shadowsocks/tunnel/shadowsocks/ShadowSocksKey.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2015, Blake 3 | * All Rights Reserved. 4 | * 5 | * Redistribution and use in source and binary forms, with or without 6 | * modification, are permitted provided that the following conditions are met: 7 | * 8 | * 1. Redistributions of source code must retain the above copyright notice, 9 | * this list of conditions and the following disclaimer. 10 | * 11 | * 2. Redistributions in binary form must reproduce the above copyright notice, 12 | * this list of conditions and the following disclaimer in the documentation 13 | * and/or other materials provided with the distribution. 14 | * 15 | * 3. The name of the author may not be used to endorse or promote 16 | * products derived from this software without specific prior 17 | * written permission. 18 | * 19 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 20 | * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 21 | * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 22 | * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE 23 | * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR 24 | * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF 25 | * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS 26 | * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 27 | * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 28 | * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 29 | * POSSIBILITY OF SUCH DAMAGE. 30 | */ 31 | 32 | package com.vm.shadowsocks.tunnel.shadowsocks; 33 | 34 | import java.io.UnsupportedEncodingException; 35 | import java.security.MessageDigest; 36 | import java.util.logging.Logger; 37 | 38 | import javax.crypto.SecretKey; 39 | 40 | /** 41 | * Shadowsocks key generator 42 | */ 43 | public class ShadowSocksKey implements SecretKey { 44 | 45 | private Logger logger = Logger.getLogger(ShadowSocksKey.class.getName()); 46 | private final static int KEY_LENGTH = 32; 47 | private byte[] _key; 48 | private int _length; 49 | 50 | public ShadowSocksKey(String password) { 51 | _length = KEY_LENGTH; 52 | _key = init(password); 53 | } 54 | 55 | public ShadowSocksKey(String password, int length) { 56 | // TODO: Invalid key length 57 | _length = length; 58 | _key = init(password); 59 | } 60 | 61 | private byte[] init(String password) { 62 | MessageDigest md = null; 63 | byte[] keys = new byte[KEY_LENGTH]; 64 | byte[] temp = null; 65 | byte[] hash = null; 66 | byte[] passwordBytes = null; 67 | int i = 0; 68 | 69 | try { 70 | md = MessageDigest.getInstance("MD5"); 71 | passwordBytes = password.getBytes("UTF-8"); 72 | } catch (UnsupportedEncodingException e) { 73 | logger.info("ShadowSocksKey: Unsupported string encoding"); 74 | } catch (Exception e) { 75 | return null; 76 | } 77 | 78 | while (i < keys.length) { 79 | if (i == 0) { 80 | hash = md.digest(passwordBytes); 81 | temp = new byte[passwordBytes.length + hash.length]; 82 | } else { 83 | System.arraycopy(hash, 0, temp, 0, hash.length); 84 | System.arraycopy(passwordBytes, 0, temp, hash.length, passwordBytes.length); 85 | hash = md.digest(temp); 86 | } 87 | System.arraycopy(hash, 0, keys, i, hash.length); 88 | i += hash.length; 89 | } 90 | 91 | if (_length != KEY_LENGTH) { 92 | byte[] keysl = new byte[_length]; 93 | System.arraycopy(keys, 0, keysl, 0, _length); 94 | return keysl; 95 | } 96 | return keys; 97 | } 98 | 99 | @Override 100 | public String getAlgorithm() { 101 | return "shadowsocks"; 102 | } 103 | 104 | @Override 105 | public String getFormat() { 106 | return "RAW"; 107 | } 108 | 109 | @Override 110 | public byte[] getEncoded() { 111 | return _key; 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /app/src/main/java/com/vm/shadowsocks/tunnel/shadowsocks/ShadowsocksConfig.java: -------------------------------------------------------------------------------- 1 | package com.vm.shadowsocks.tunnel.shadowsocks; 2 | 3 | import android.net.Uri; 4 | import android.util.Base64; 5 | 6 | import com.vm.shadowsocks.tunnel.Config; 7 | 8 | import java.net.InetSocketAddress; 9 | 10 | public class ShadowsocksConfig extends Config { 11 | public String EncryptMethod; 12 | public String Password; 13 | 14 | public static ShadowsocksConfig parse(String proxyInfo) throws Exception { 15 | ShadowsocksConfig config = new ShadowsocksConfig(); 16 | Uri uri = Uri.parse(proxyInfo); 17 | if (uri.getPort() == -1) { 18 | String base64String = uri.getHost(); 19 | proxyInfo = "ss://" + new String(Base64.decode(base64String.getBytes("ASCII"), Base64.DEFAULT)); 20 | uri = Uri.parse(proxyInfo); 21 | } 22 | 23 | String userInfoString = uri.getUserInfo(); 24 | if (userInfoString != null) { 25 | String[] userStrings = userInfoString.split(":"); 26 | config.EncryptMethod = userStrings[0]; 27 | if (userStrings.length >= 2) { 28 | config.Password = userStrings[1]; 29 | } 30 | } 31 | if (!CryptFactory.isCipherExisted(config.EncryptMethod)) { 32 | throw new Exception(String.format("Method: %s does not support", config.EncryptMethod)); 33 | } 34 | config.ServerAddress = new InetSocketAddress(uri.getHost(), uri.getPort()); 35 | return config; 36 | } 37 | 38 | @Override 39 | public boolean equals(Object o) { 40 | if (o == null) 41 | return false; 42 | return this.toString().equals(o.toString()); 43 | } 44 | 45 | @Override 46 | public String toString() { 47 | return String.format("ss://%s:%s@%s", EncryptMethod, Password, ServerAddress); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /app/src/main/java/com/vm/shadowsocks/tunnel/shadowsocks/ShadowsocksTunnel.java: -------------------------------------------------------------------------------- 1 | package com.vm.shadowsocks.tunnel.shadowsocks; 2 | 3 | import com.vm.shadowsocks.tunnel.Tunnel; 4 | 5 | import java.nio.ByteBuffer; 6 | import java.nio.channels.Selector; 7 | 8 | public class ShadowsocksTunnel extends Tunnel { 9 | 10 | private ICrypt m_Encryptor; 11 | private ShadowsocksConfig m_Config; 12 | private boolean m_TunnelEstablished; 13 | 14 | public ShadowsocksTunnel(ShadowsocksConfig config, Selector selector) throws Exception { 15 | super(config.ServerAddress, selector); 16 | m_Config = config; 17 | m_Encryptor = CryptFactory.get(m_Config.EncryptMethod, m_Config.Password); 18 | 19 | } 20 | 21 | @Override 22 | protected void onConnected(ByteBuffer buffer) throws Exception { 23 | 24 | buffer.clear(); 25 | // https://shadowsocks.org/en/spec/protocol.html 26 | 27 | buffer.put((byte) 0x03);//domain 28 | byte[] domainBytes = m_DestAddress.getHostName().getBytes(); 29 | buffer.put((byte) domainBytes.length);//domain length; 30 | buffer.put(domainBytes); 31 | buffer.putShort((short) m_DestAddress.getPort()); 32 | buffer.flip(); 33 | byte[] _header = new byte[buffer.limit()]; 34 | buffer.get(_header); 35 | 36 | buffer.clear(); 37 | buffer.put(m_Encryptor.encrypt(_header)); 38 | buffer.flip(); 39 | 40 | if (write(buffer, true)) { 41 | m_TunnelEstablished = true; 42 | onTunnelEstablished(); 43 | } else { 44 | m_TunnelEstablished = true; 45 | this.beginReceive(); 46 | } 47 | } 48 | 49 | @Override 50 | protected boolean isTunnelEstablished() { 51 | return m_TunnelEstablished; 52 | } 53 | 54 | @Override 55 | protected void beforeSend(ByteBuffer buffer) throws Exception { 56 | 57 | byte[] bytes = new byte[buffer.limit()]; 58 | buffer.get(bytes); 59 | 60 | byte[] newbytes = m_Encryptor.encrypt(bytes); 61 | 62 | buffer.clear(); 63 | buffer.put(newbytes); 64 | buffer.flip(); 65 | } 66 | 67 | @Override 68 | protected void afterReceived(ByteBuffer buffer) throws Exception { 69 | byte[] bytes = new byte[buffer.limit()]; 70 | buffer.get(bytes); 71 | byte[] newbytes = m_Encryptor.decrypt(bytes); 72 | String s = new String(newbytes); 73 | buffer.clear(); 74 | buffer.put(newbytes); 75 | buffer.flip(); 76 | } 77 | 78 | @Override 79 | protected void onDispose() { 80 | m_Config = null; 81 | m_Encryptor = null; 82 | } 83 | 84 | } 85 | -------------------------------------------------------------------------------- /app/src/main/java/com/vm/shadowsocks/ui/AppManager.java: -------------------------------------------------------------------------------- 1 | package com.vm.shadowsocks.ui; 2 | 3 | import android.animation.Animator; 4 | import android.app.ActionBar; 5 | import android.app.Activity; 6 | import android.content.Intent; 7 | import android.content.pm.PackageManager; 8 | import android.content.pm.ResolveInfo; 9 | import android.graphics.drawable.Drawable; 10 | import android.os.Bundle; 11 | import android.support.annotation.Nullable; 12 | import android.support.v7.widget.DefaultItemAnimator; 13 | import android.support.v7.widget.LinearLayoutManager; 14 | import android.support.v7.widget.RecyclerView; 15 | import android.view.LayoutInflater; 16 | import android.view.MenuItem; 17 | import android.view.View; 18 | import android.view.ViewGroup; 19 | import android.widget.ImageView; 20 | import android.widget.Switch; 21 | 22 | import com.futuremind.recyclerviewfastscroll.FastScroller; 23 | import com.futuremind.recyclerviewfastscroll.SectionTitleProvider; 24 | import com.vm.shadowsocks.R; 25 | import com.vm.shadowsocks.core.AppInfo; 26 | import com.vm.shadowsocks.core.AppProxyManager; 27 | 28 | import java.util.Collections; 29 | import java.util.List; 30 | 31 | import io.reactivex.Observable; 32 | import io.reactivex.ObservableEmitter; 33 | import io.reactivex.ObservableOnSubscribe; 34 | import io.reactivex.Observer; 35 | import io.reactivex.disposables.Disposable; 36 | import io.reactivex.schedulers.Schedulers; 37 | import io.reactivex.android.schedulers.AndroidSchedulers; 38 | 39 | /** 40 | * Created by so898 on 2017/5/3. 41 | */ 42 | 43 | public class AppManager extends Activity{ 44 | private View loadingView; 45 | private RecyclerView appListView; 46 | private FastScroller fastScroller; 47 | private AppManagerAdapter adapter; 48 | 49 | @Override 50 | public void onCreate(@Nullable Bundle savedInstanceState) { 51 | super.onCreate(savedInstanceState); 52 | this.setContentView(R.layout.layout_apps); 53 | 54 | ActionBar actionBar = getActionBar(); 55 | if (actionBar != null) 56 | actionBar.setDisplayHomeAsUpEnabled(true); 57 | 58 | loadingView = findViewById(R.id.loading); 59 | appListView = (RecyclerView)findViewById(R.id.list); 60 | appListView.setLayoutManager(new LinearLayoutManager(this, LinearLayoutManager.VERTICAL, false)); 61 | appListView.setItemAnimator(new DefaultItemAnimator()); 62 | fastScroller = (FastScroller)findViewById(R.id.fastscroller); 63 | 64 | Observable> observable = Observable.create(new ObservableOnSubscribe>() { 65 | @Override 66 | public void subscribe(ObservableEmitter> appInfo) throws Exception { 67 | queryAppInfo(); 68 | adapter = new AppManagerAdapter(); 69 | appInfo.onComplete(); 70 | } 71 | }).subscribeOn(Schedulers.io()).observeOn(AndroidSchedulers.mainThread()); 72 | Observer> observer = new Observer>() { 73 | @Override 74 | public void onSubscribe(Disposable d) {} 75 | 76 | @Override 77 | public void onNext(List aLong) {} 78 | 79 | @Override 80 | public void onError(Throwable e) {} 81 | 82 | @Override 83 | public void onComplete() { 84 | appListView.setAdapter(adapter); 85 | fastScroller.setRecyclerView(appListView); 86 | long shortAnimTime = 1; 87 | appListView.setAlpha(0); 88 | appListView.setVisibility(View.VISIBLE); 89 | appListView.animate().alpha(1).setDuration(shortAnimTime); 90 | loadingView.animate().alpha(0).setDuration(shortAnimTime).setListener(new Animator.AnimatorListener() { 91 | @Override 92 | public void onAnimationStart(Animator animator) {} 93 | 94 | @Override 95 | public void onAnimationEnd(Animator animator) { 96 | loadingView.setVisibility(View.GONE); 97 | } 98 | 99 | @Override 100 | public void onAnimationCancel(Animator animator) {} 101 | 102 | @Override 103 | public void onAnimationRepeat(Animator animator) {} 104 | }); 105 | } 106 | }; 107 | observable.subscribe(observer); 108 | } 109 | 110 | public void queryAppInfo() { 111 | PackageManager pm = this.getPackageManager(); // 获得PackageManager对象 112 | Intent mainIntent = new Intent(Intent.ACTION_MAIN, null); 113 | mainIntent.addCategory(Intent.CATEGORY_LAUNCHER); 114 | List resolveInfos = pm.queryIntentActivities(mainIntent, 0); 115 | Collections.sort(resolveInfos, new ResolveInfo.DisplayNameComparator(pm)); 116 | if (AppProxyManager.Instance.mlistAppInfo != null) { 117 | AppProxyManager.Instance.mlistAppInfo.clear(); 118 | for (ResolveInfo reInfo : resolveInfos) { 119 | String pkgName = reInfo.activityInfo.packageName; // 获得应用程序的包名 120 | String appLabel = (String) reInfo.loadLabel(pm); // 获得应用程序的Label 121 | Drawable icon = reInfo.loadIcon(pm); // 获得应用程序图标 122 | AppInfo appInfo = new AppInfo(); 123 | appInfo.setAppLabel(appLabel); 124 | appInfo.setPkgName(pkgName); 125 | appInfo.setAppIcon(icon); 126 | if (!appInfo.getPkgName().equals("com.vm.shadowsocks"))//App本身会强制加入代理列表 127 | AppProxyManager.Instance.mlistAppInfo.add(appInfo); 128 | } 129 | } 130 | } 131 | 132 | @Override 133 | public boolean onOptionsItemSelected(MenuItem item) { 134 | switch (item.getItemId()) { 135 | case android.R.id.home: 136 | finish(); 137 | return true; 138 | default: 139 | return super.onOptionsItemSelected(item); 140 | } 141 | } 142 | 143 | } 144 | 145 | class AppViewHolder extends RecyclerView.ViewHolder implements View.OnClickListener { 146 | private ImageView icon = (ImageView)itemView.findViewById(R.id.itemicon); 147 | private Switch check = (Switch)itemView.findViewById(R.id.itemcheck); 148 | private AppInfo item; 149 | private Boolean proxied = false; 150 | 151 | AppViewHolder(View itemView) { 152 | super(itemView); 153 | itemView.setOnClickListener(this); 154 | } 155 | 156 | void bind(AppInfo app) { 157 | this.item = app; 158 | proxied = AppProxyManager.Instance.isAppProxy(app.getPkgName()); 159 | icon.setImageDrawable(app.getAppIcon()); 160 | check.setText(app.getAppLabel()); 161 | check.setChecked(proxied); 162 | } 163 | 164 | @Override 165 | public void onClick(View view) { 166 | if (proxied) { 167 | AppProxyManager.Instance.removeProxyApp(item.getPkgName()); 168 | check.setChecked(false); 169 | } else { 170 | AppProxyManager.Instance.addProxyApp(item.getPkgName()); 171 | check.setChecked(true); 172 | } 173 | proxied = !proxied; 174 | } 175 | } 176 | 177 | class AppManagerAdapter extends RecyclerView.Adapter implements SectionTitleProvider { 178 | 179 | 180 | @Override 181 | public AppViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { 182 | return new AppViewHolder(LayoutInflater.from(parent.getContext()).inflate(R.layout.layout_apps_item, parent, false)); 183 | } 184 | 185 | @Override 186 | public void onBindViewHolder(AppViewHolder holder, int position) { 187 | AppInfo appInfo = AppProxyManager.Instance.mlistAppInfo.get(position); 188 | holder.bind(appInfo); 189 | } 190 | 191 | @Override 192 | public int getItemCount() { 193 | return AppProxyManager.Instance.mlistAppInfo.size(); 194 | } 195 | 196 | @Override 197 | public String getSectionTitle(int position) { 198 | AppInfo appInfo = AppProxyManager.Instance.mlistAppInfo.get(position); 199 | return appInfo.getAppLabel(); 200 | } 201 | } 202 | -------------------------------------------------------------------------------- /app/src/main/java/com/vm/shadowsocks/ui/MainActivity.java: -------------------------------------------------------------------------------- 1 | package com.vm.shadowsocks.ui; 2 | 3 | import android.annotation.SuppressLint; 4 | import android.app.Activity; 5 | import android.app.AlertDialog; 6 | import android.content.DialogInterface; 7 | import android.content.DialogInterface.OnClickListener; 8 | import android.content.Intent; 9 | import android.content.SharedPreferences; 10 | import android.content.SharedPreferences.Editor; 11 | import android.content.pm.PackageManager; 12 | import android.net.Uri; 13 | import android.os.Bundle; 14 | import android.text.InputType; 15 | import android.text.TextUtils; 16 | import android.util.Log; 17 | import android.view.Menu; 18 | import android.view.MenuItem; 19 | import android.view.View; 20 | import android.view.ViewGroup; 21 | import android.widget.CompoundButton; 22 | import android.widget.CompoundButton.OnCheckedChangeListener; 23 | import android.widget.EditText; 24 | import android.widget.ScrollView; 25 | import android.widget.Switch; 26 | import android.widget.TextView; 27 | import android.widget.Toast; 28 | 29 | import com.google.zxing.integration.android.IntentIntegrator; 30 | import com.google.zxing.integration.android.IntentResult; 31 | import com.vm.shadowsocks.R; 32 | import com.vm.shadowsocks.core.AppInfo; 33 | import com.vm.shadowsocks.core.AppProxyManager; 34 | import com.vm.shadowsocks.core.LocalVpnService; 35 | import com.vm.shadowsocks.core.ProxyConfig; 36 | 37 | import java.util.Calendar; 38 | 39 | public class MainActivity extends Activity implements 40 | View.OnClickListener, 41 | OnCheckedChangeListener, 42 | LocalVpnService.onStatusChangedListener { 43 | 44 | private static String GL_HISTORY_LOGS; 45 | 46 | private static final String TAG = MainActivity.class.getSimpleName(); 47 | 48 | private static final String CONFIG_URL_KEY = "CONFIG_URL_KEY"; 49 | 50 | private static final int START_VPN_SERVICE_REQUEST_CODE = 1985; 51 | 52 | private Switch switchProxy; 53 | private TextView textViewLog; 54 | private ScrollView scrollViewLog; 55 | private TextView textViewProxyUrl, textViewProxyApp; 56 | private Calendar mCalendar; 57 | 58 | @Override 59 | protected void onCreate(Bundle savedInstanceState) { 60 | super.onCreate(savedInstanceState); 61 | setContentView(R.layout.activity_main); 62 | 63 | scrollViewLog = (ScrollView) findViewById(R.id.scrollViewLog); 64 | textViewLog = (TextView) findViewById(R.id.textViewLog); 65 | findViewById(R.id.ProxyUrlLayout).setOnClickListener(this); 66 | findViewById(R.id.AppSelectLayout).setOnClickListener(this); 67 | 68 | textViewProxyUrl = (TextView) findViewById(R.id.textViewProxyUrl); 69 | String ProxyUrl = readProxyUrl(); 70 | if (TextUtils.isEmpty(ProxyUrl)) { 71 | textViewProxyUrl.setText(R.string.config_not_set_value); 72 | } else { 73 | textViewProxyUrl.setText(ProxyUrl); 74 | } 75 | 76 | textViewLog.setText(GL_HISTORY_LOGS); 77 | scrollViewLog.fullScroll(ScrollView.FOCUS_DOWN); 78 | 79 | mCalendar = Calendar.getInstance(); 80 | LocalVpnService.addOnStatusChangedListener(this); 81 | 82 | //Pre-App Proxy 83 | if (AppProxyManager.isLollipopOrAbove){ 84 | new AppProxyManager(this); 85 | textViewProxyApp = (TextView) findViewById(R.id.textViewAppSelectDetail); 86 | } else { 87 | ((ViewGroup) findViewById(R.id.AppSelectLayout).getParent()).removeView(findViewById(R.id.AppSelectLayout)); 88 | ((ViewGroup) findViewById(R.id.textViewAppSelectLine).getParent()).removeView(findViewById(R.id.textViewAppSelectLine)); 89 | } 90 | } 91 | 92 | String readProxyUrl() { 93 | SharedPreferences preferences = getSharedPreferences("shadowsocksProxyUrl", MODE_PRIVATE); 94 | return preferences.getString(CONFIG_URL_KEY, ""); 95 | } 96 | 97 | void setProxyUrl(String ProxyUrl) { 98 | SharedPreferences preferences = getSharedPreferences("shadowsocksProxyUrl", MODE_PRIVATE); 99 | Editor editor = preferences.edit(); 100 | editor.putString(CONFIG_URL_KEY, ProxyUrl); 101 | editor.apply(); 102 | } 103 | 104 | String getVersionName() { 105 | PackageManager packageManager = getPackageManager(); 106 | if (packageManager == null) { 107 | Log.e(TAG, "null package manager is impossible"); 108 | return null; 109 | } 110 | 111 | try { 112 | return packageManager.getPackageInfo(getPackageName(), 0).versionName; 113 | } catch (PackageManager.NameNotFoundException e) { 114 | Log.e(TAG, "package not found is impossible", e); 115 | return null; 116 | } 117 | } 118 | 119 | boolean isValidUrl(String url) { 120 | try { 121 | if (url == null || url.isEmpty()) 122 | return false; 123 | 124 | if (url.startsWith("ss://")) {//file path 125 | return true; 126 | } else { //url 127 | Uri uri = Uri.parse(url); 128 | if (!"http".equals(uri.getScheme()) && !"https".equals(uri.getScheme())) 129 | return false; 130 | if (uri.getHost() == null) 131 | return false; 132 | } 133 | return true; 134 | } catch (Exception e) { 135 | return false; 136 | } 137 | } 138 | 139 | @Override 140 | public void onClick(View v) { 141 | if (switchProxy.isChecked()) { 142 | return; 143 | } 144 | 145 | if (v.getTag().toString().equals("ProxyUrl")){ 146 | new AlertDialog.Builder(this) 147 | .setTitle(R.string.config_url) 148 | .setItems(new CharSequence[]{ 149 | getString(R.string.config_url_scan), 150 | getString(R.string.config_url_manual) 151 | }, new OnClickListener() { 152 | @Override 153 | public void onClick(DialogInterface dialogInterface, int i) { 154 | switch (i) { 155 | case 0: 156 | scanForProxyUrl(); 157 | break; 158 | case 1: 159 | showProxyUrlInputDialog(); 160 | break; 161 | } 162 | } 163 | }) 164 | .show(); 165 | } else if (v.getTag().toString().equals("AppSelect")){ 166 | System.out.println("abc"); 167 | startActivity(new Intent(this, AppManager.class)); 168 | } 169 | } 170 | 171 | private void scanForProxyUrl() { 172 | new IntentIntegrator(this) 173 | .setPrompt(getString(R.string.config_url_scan_hint)) 174 | .initiateScan(IntentIntegrator.QR_CODE_TYPES); 175 | } 176 | 177 | private void showProxyUrlInputDialog() { 178 | final EditText editText = new EditText(this); 179 | editText.setInputType(InputType.TYPE_TEXT_VARIATION_URI); 180 | editText.setHint(getString(R.string.config_url_hint)); 181 | editText.setText(readProxyUrl()); 182 | 183 | new AlertDialog.Builder(this) 184 | .setTitle(R.string.config_url) 185 | .setView(editText) 186 | .setPositiveButton(R.string.btn_ok, new OnClickListener() { 187 | @Override 188 | public void onClick(DialogInterface dialog, int which) { 189 | if (editText.getText() == null) { 190 | return; 191 | } 192 | 193 | String ProxyUrl = editText.getText().toString().trim(); 194 | if (isValidUrl(ProxyUrl)) { 195 | setProxyUrl(ProxyUrl); 196 | textViewProxyUrl.setText(ProxyUrl); 197 | } else { 198 | Toast.makeText(MainActivity.this, R.string.err_invalid_url, Toast.LENGTH_SHORT).show(); 199 | } 200 | } 201 | }) 202 | .setNegativeButton(R.string.btn_cancel, null) 203 | .show(); 204 | } 205 | 206 | @SuppressLint("DefaultLocale") 207 | @Override 208 | public void onLogReceived(String logString) { 209 | mCalendar.setTimeInMillis(System.currentTimeMillis()); 210 | logString = String.format("[%1$02d:%2$02d:%3$02d] %4$s\n", 211 | mCalendar.get(Calendar.HOUR_OF_DAY), 212 | mCalendar.get(Calendar.MINUTE), 213 | mCalendar.get(Calendar.SECOND), 214 | logString); 215 | 216 | System.out.println(logString); 217 | 218 | if (textViewLog.getLineCount() > 200) { 219 | textViewLog.setText(""); 220 | } 221 | textViewLog.append(logString); 222 | scrollViewLog.fullScroll(ScrollView.FOCUS_DOWN); 223 | GL_HISTORY_LOGS = textViewLog.getText() == null ? "" : textViewLog.getText().toString(); 224 | } 225 | 226 | @Override 227 | public void onStatusChanged(String status, Boolean isRunning) { 228 | switchProxy.setEnabled(true); 229 | switchProxy.setChecked(isRunning); 230 | onLogReceived(status); 231 | Toast.makeText(this, status, Toast.LENGTH_SHORT).show(); 232 | } 233 | 234 | @Override 235 | public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) { 236 | if (LocalVpnService.IsRunning != isChecked) { 237 | switchProxy.setEnabled(false); 238 | if (isChecked) { 239 | Intent intent = LocalVpnService.prepare(this); 240 | if (intent == null) { 241 | startVPNService(); 242 | } else { 243 | startActivityForResult(intent, START_VPN_SERVICE_REQUEST_CODE); 244 | } 245 | } else { 246 | LocalVpnService.IsRunning = false; 247 | } 248 | } 249 | } 250 | 251 | private void startVPNService() { 252 | String ProxyUrl = readProxyUrl(); 253 | if (!isValidUrl(ProxyUrl)) { 254 | Toast.makeText(this, R.string.err_invalid_url, Toast.LENGTH_SHORT).show(); 255 | switchProxy.post(new Runnable() { 256 | @Override 257 | public void run() { 258 | switchProxy.setChecked(false); 259 | switchProxy.setEnabled(true); 260 | } 261 | }); 262 | return; 263 | } 264 | 265 | textViewLog.setText(""); 266 | GL_HISTORY_LOGS = null; 267 | onLogReceived("starting..."); 268 | LocalVpnService.ProxyUrl = ProxyUrl; 269 | startService(new Intent(this, LocalVpnService.class)); 270 | } 271 | 272 | @Override 273 | protected void onActivityResult(int requestCode, int resultCode, Intent intent) { 274 | if (requestCode == START_VPN_SERVICE_REQUEST_CODE) { 275 | if (resultCode == RESULT_OK) { 276 | startVPNService(); 277 | } else { 278 | switchProxy.setChecked(false); 279 | switchProxy.setEnabled(true); 280 | onLogReceived("canceled."); 281 | } 282 | return; 283 | } 284 | 285 | IntentResult scanResult = IntentIntegrator.parseActivityResult(requestCode, resultCode, intent); 286 | if (scanResult != null) { 287 | String ProxyUrl = scanResult.getContents(); 288 | if (isValidUrl(ProxyUrl)) { 289 | setProxyUrl(ProxyUrl); 290 | textViewProxyUrl.setText(ProxyUrl); 291 | } else { 292 | Toast.makeText(MainActivity.this, R.string.err_invalid_url, Toast.LENGTH_SHORT).show(); 293 | } 294 | return; 295 | } 296 | 297 | super.onActivityResult(requestCode, resultCode, intent); 298 | } 299 | 300 | @Override 301 | public boolean onCreateOptionsMenu(Menu menu) { 302 | getMenuInflater().inflate(R.menu.main_activity_actions, menu); 303 | 304 | MenuItem menuItem = menu.findItem(R.id.menu_item_switch); 305 | if (menuItem == null) { 306 | return false; 307 | } 308 | 309 | switchProxy = (Switch) menuItem.getActionView(); 310 | if (switchProxy == null) { 311 | return false; 312 | } 313 | 314 | switchProxy.setChecked(LocalVpnService.IsRunning); 315 | switchProxy.setOnCheckedChangeListener(this); 316 | 317 | return true; 318 | } 319 | 320 | @Override 321 | public boolean onOptionsItemSelected(MenuItem item) { 322 | switch (item.getItemId()) { 323 | case R.id.menu_item_about: 324 | new AlertDialog.Builder(this) 325 | .setTitle(getString(R.string.app_name) + getVersionName()) 326 | .setMessage(R.string.about_info) 327 | .setPositiveButton(R.string.btn_ok, null) 328 | .setNegativeButton(R.string.btn_more, new OnClickListener() { 329 | @Override 330 | public void onClick(DialogInterface dialog, int which) { 331 | startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse("https://github.com/dawei101/shadowsocks-android-java"))); 332 | } 333 | }) 334 | .show(); 335 | 336 | return true; 337 | case R.id.menu_item_exit: 338 | if (!LocalVpnService.IsRunning) { 339 | finish(); 340 | return true; 341 | } 342 | 343 | new AlertDialog.Builder(this) 344 | .setTitle(R.string.menu_item_exit) 345 | .setMessage(R.string.exit_confirm_info) 346 | .setPositiveButton(R.string.btn_ok, new OnClickListener() { 347 | @Override 348 | public void onClick(DialogInterface dialog, int which) { 349 | LocalVpnService.IsRunning = false; 350 | LocalVpnService.Instance.disconnectVPN(); 351 | stopService(new Intent(MainActivity.this, LocalVpnService.class)); 352 | System.runFinalization(); 353 | System.exit(0); 354 | } 355 | }) 356 | .setNegativeButton(R.string.btn_cancel, null) 357 | .show(); 358 | 359 | return true; 360 | case R.id.menu_item_toggle_global: 361 | ProxyConfig.Instance.globalMode = !ProxyConfig.Instance.globalMode; 362 | if (ProxyConfig.Instance.globalMode) { 363 | onLogReceived("Proxy global mode is on"); 364 | } else { 365 | onLogReceived("Proxy global mode is off"); 366 | } 367 | default: 368 | return super.onOptionsItemSelected(item); 369 | } 370 | } 371 | 372 | @Override 373 | protected void onResume() { 374 | super.onResume(); 375 | if (AppProxyManager.isLollipopOrAbove) { 376 | if (AppProxyManager.Instance.proxyAppInfo.size() != 0) { 377 | String tmpString = ""; 378 | for (AppInfo app : AppProxyManager.Instance.proxyAppInfo) { 379 | tmpString += app.getAppLabel() + ", "; 380 | } 381 | textViewProxyApp.setText(tmpString); 382 | } 383 | } 384 | } 385 | 386 | @Override 387 | protected void onDestroy() { 388 | LocalVpnService.removeOnStatusChangedListener(this); 389 | super.onDestroy(); 390 | } 391 | 392 | } 393 | -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxhdpi/shadowsocks.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dawei101/shadowsocks-android-java/0c38e592dd10a4fad3dbe7dca351b4cd27b2f1bf/app/src/main/res/drawable-xxhdpi/shadowsocks.png -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_main.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 13 | 14 | 20 | 21 | 27 | 28 | 29 | 30 | 37 | 38 | 45 | 46 | 52 | 53 | 59 | 60 | 61 | 62 | 70 | 71 | 75 | 76 | 80 | 81 | 87 | 88 | 89 | 90 | 91 | 94 | 95 | 98 | 99 | 102 | 103 | 104 | 105 | 106 | -------------------------------------------------------------------------------- /app/src/main/res/layout/layout_apps.xml: -------------------------------------------------------------------------------- 1 | 2 | 10 | 13 | 17 | 22 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /app/src/main/res/layout/layout_apps_item.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | 9 | 18 | 19 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /app/src/main/res/menu/main_activity_actions.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 11 | 14 | 17 | 18 | -------------------------------------------------------------------------------- /app/src/main/res/raw/ipmask: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dawei101/shadowsocks-android-java/0c38e592dd10a4fad3dbe7dca351b4cd27b2f1bf/app/src/main/res/raw/ipmask -------------------------------------------------------------------------------- /app/src/main/res/values-sw600dp/dimens.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /app/src/main/res/values-sw720dp-land/dimens.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 7 | 128dp 8 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/values-v11/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 7 | 10 | 11 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /app/src/main/res/values-v14/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | 11 | 12 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /app/src/main/res/values-v21/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | 11 | 12 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /app/src/main/res/values-zh-rCN/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | Shadowsocks 4 | 代理 5 | 开/关全局模式 6 | 关于 7 | 退出 8 | 配置地址 9 | 未设置 10 | 扫描二维码 11 | 手动输入 12 | 扫描配置地址的二维码 13 | 选择使用代理的软件 14 | 未选择 15 | 使用代理的软件 16 | 确定 17 | 取消 18 | 更多… 19 | 已连接 20 | 已断开 21 | Shadowsocks android 由dawei开发维护,可以在任意Android 4.0+上使用,无需ROOT权限。它是基于SmartProxy与Shadowsocks-Java改装。 22 | 这将会断开VPN连接,确定要退出吗? 23 | 无效的配置地址 24 | ss://method:password@host:port 25 | 26 | -------------------------------------------------------------------------------- /app/src/main/res/values/dimens.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 16dp 5 | 16dp 6 | 7 | 8 | -------------------------------------------------------------------------------- /app/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | Shadowsocks 4 | Toggle global mode 5 | Proxy 6 | About 7 | Exit 8 | Config URL 9 | Not set 10 | Scan an QR code 11 | Type manually 12 | Scan the QR code of your config url 13 | Select App 14 | No 15 | Per-App Proxy 16 | OK 17 | Cancel 18 | More… 19 | connected 20 | disconnected 21 | Shadowsocks-android is writen by dawei. It could run on any Android 4.0+ device , no need to root, and it is modified based on SmartProxy and Shadowsocks-Java 22 | This will disconnect the VPN connection, Are you sure to exit? 23 | The config URL is invalid. 24 | ss://method:password@host:port 25 | 26 | -------------------------------------------------------------------------------- /app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 7 | 14 | 15 | 16 | 19 | 20 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | // Top-level build file where you can add configuration options common to all sub-projects/modules. 2 | 3 | buildscript { 4 | repositories { 5 | jcenter() 6 | } 7 | dependencies { 8 | classpath 'com.android.tools.build:gradle:2.3.3' 9 | 10 | // NOTE: Do not place your application dependencies here; they belong 11 | // in the individual module build.gradle files 12 | } 13 | } 14 | 15 | allprojects { 16 | repositories { 17 | jcenter() 18 | } 19 | } 20 | 21 | task clean(type: Delete) { 22 | delete rootProject.buildDir 23 | } 24 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | # Project-wide Gradle settings. 2 | 3 | # IDE (e.g. Android Studio) users: 4 | # Gradle settings configured through the IDE *will override* 5 | # any settings specified in this file. 6 | 7 | # For more details on how to configure your build environment visit 8 | # http://www.gradle.org/docs/current/userguide/build_environment.html 9 | 10 | # Specifies the JVM arguments used for the daemon process. 11 | # The setting is particularly useful for tweaking memory settings. 12 | # Default value: -Xmx10248m -XX:MaxPermSize=256m 13 | # org.gradle.jvmargs=-Xmx2048m -XX:MaxPermSize=512m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8 14 | 15 | # When configured, Gradle will run in incubating parallel mode. 16 | # This option should only be used with decoupled projects. More details, visit 17 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects 18 | # org.gradle.parallel=true -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dawei101/shadowsocks-android-java/0c38e592dd10a4fad3dbe7dca351b4cd27b2f1bf/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Wed Aug 16 23:07:29 CST 2017 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | distributionUrl=https\://services.gradle.org/distributions/gradle-3.3-all.zip 7 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | ############################################################################## 4 | ## 5 | ## Gradle start up script for UN*X 6 | ## 7 | ############################################################################## 8 | 9 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 10 | DEFAULT_JVM_OPTS="" 11 | 12 | APP_NAME="Gradle" 13 | APP_BASE_NAME=`basename "$0"` 14 | 15 | # Use the maximum available, or set MAX_FD != -1 to use that value. 16 | MAX_FD="maximum" 17 | 18 | warn ( ) { 19 | echo "$*" 20 | } 21 | 22 | die ( ) { 23 | echo 24 | echo "$*" 25 | echo 26 | exit 1 27 | } 28 | 29 | # OS specific support (must be 'true' or 'false'). 30 | cygwin=false 31 | msys=false 32 | darwin=false 33 | case "`uname`" in 34 | CYGWIN* ) 35 | cygwin=true 36 | ;; 37 | Darwin* ) 38 | darwin=true 39 | ;; 40 | MINGW* ) 41 | msys=true 42 | ;; 43 | esac 44 | 45 | # Attempt to set APP_HOME 46 | # Resolve links: $0 may be a link 47 | PRG="$0" 48 | # Need this for relative symlinks. 49 | while [ -h "$PRG" ] ; do 50 | ls=`ls -ld "$PRG"` 51 | link=`expr "$ls" : '.*-> \(.*\)$'` 52 | if expr "$link" : '/.*' > /dev/null; then 53 | PRG="$link" 54 | else 55 | PRG=`dirname "$PRG"`"/$link" 56 | fi 57 | done 58 | SAVED="`pwd`" 59 | cd "`dirname \"$PRG\"`/" >/dev/null 60 | APP_HOME="`pwd -P`" 61 | cd "$SAVED" >/dev/null 62 | 63 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 64 | 65 | # Determine the Java command to use to start the JVM. 66 | if [ -n "$JAVA_HOME" ] ; then 67 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 68 | # IBM's JDK on AIX uses strange locations for the executables 69 | JAVACMD="$JAVA_HOME/jre/sh/java" 70 | else 71 | JAVACMD="$JAVA_HOME/bin/java" 72 | fi 73 | if [ ! -x "$JAVACMD" ] ; then 74 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 75 | 76 | Please set the JAVA_HOME variable in your environment to match the 77 | location of your Java installation." 78 | fi 79 | else 80 | JAVACMD="java" 81 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 82 | 83 | Please set the JAVA_HOME variable in your environment to match the 84 | location of your Java installation." 85 | fi 86 | 87 | # Increase the maximum file descriptors if we can. 88 | if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then 89 | MAX_FD_LIMIT=`ulimit -H -n` 90 | if [ $? -eq 0 ] ; then 91 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 92 | MAX_FD="$MAX_FD_LIMIT" 93 | fi 94 | ulimit -n $MAX_FD 95 | if [ $? -ne 0 ] ; then 96 | warn "Could not set maximum file descriptor limit: $MAX_FD" 97 | fi 98 | else 99 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 100 | fi 101 | fi 102 | 103 | # For Darwin, add options to specify how the application appears in the dock 104 | if $darwin; then 105 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 106 | fi 107 | 108 | # For Cygwin, switch paths to Windows format before running java 109 | if $cygwin ; then 110 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 111 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 112 | JAVACMD=`cygpath --unix "$JAVACMD"` 113 | 114 | # We build the pattern for arguments to be converted via cygpath 115 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 116 | SEP="" 117 | for dir in $ROOTDIRSRAW ; do 118 | ROOTDIRS="$ROOTDIRS$SEP$dir" 119 | SEP="|" 120 | done 121 | OURCYGPATTERN="(^($ROOTDIRS))" 122 | # Add a user-defined pattern to the cygpath arguments 123 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 124 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 125 | fi 126 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 127 | i=0 128 | for arg in "$@" ; do 129 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 130 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 131 | 132 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 133 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 134 | else 135 | eval `echo args$i`="\"$arg\"" 136 | fi 137 | i=$((i+1)) 138 | done 139 | case $i in 140 | (0) set -- ;; 141 | (1) set -- "$args0" ;; 142 | (2) set -- "$args0" "$args1" ;; 143 | (3) set -- "$args0" "$args1" "$args2" ;; 144 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 145 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 146 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 147 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 148 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 149 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 150 | esac 151 | fi 152 | 153 | # Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules 154 | function splitJvmOpts() { 155 | JVM_OPTS=("$@") 156 | } 157 | eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS 158 | JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" 159 | 160 | exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" 161 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @if "%DEBUG%" == "" @echo off 2 | @rem ########################################################################## 3 | @rem 4 | @rem Gradle startup script for Windows 5 | @rem 6 | @rem ########################################################################## 7 | 8 | @rem Set local scope for the variables with windows NT shell 9 | if "%OS%"=="Windows_NT" setlocal 10 | 11 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 12 | set DEFAULT_JVM_OPTS= 13 | 14 | set DIRNAME=%~dp0 15 | if "%DIRNAME%" == "" set DIRNAME=. 16 | set APP_BASE_NAME=%~n0 17 | set APP_HOME=%DIRNAME% 18 | 19 | @rem Find java.exe 20 | if defined JAVA_HOME goto findJavaFromJavaHome 21 | 22 | set JAVA_EXE=java.exe 23 | %JAVA_EXE% -version >NUL 2>&1 24 | if "%ERRORLEVEL%" == "0" goto init 25 | 26 | echo. 27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 28 | echo. 29 | echo Please set the JAVA_HOME variable in your environment to match the 30 | echo location of your Java installation. 31 | 32 | goto fail 33 | 34 | :findJavaFromJavaHome 35 | set JAVA_HOME=%JAVA_HOME:"=% 36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 37 | 38 | if exist "%JAVA_EXE%" goto init 39 | 40 | echo. 41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 42 | echo. 43 | echo Please set the JAVA_HOME variable in your environment to match the 44 | echo location of your Java installation. 45 | 46 | goto fail 47 | 48 | :init 49 | @rem Get command-line arguments, handling Windowz variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | if "%@eval[2+2]" == "4" goto 4NT_args 53 | 54 | :win9xME_args 55 | @rem Slurp the command line arguments. 56 | set CMD_LINE_ARGS= 57 | set _SKIP=2 58 | 59 | :win9xME_args_slurp 60 | if "x%~1" == "x" goto execute 61 | 62 | set CMD_LINE_ARGS=%* 63 | goto execute 64 | 65 | :4NT_args 66 | @rem Get arguments from the 4NT Shell from JP Software 67 | set CMD_LINE_ARGS=%$ 68 | 69 | :execute 70 | @rem Setup the command line 71 | 72 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 73 | 74 | @rem Execute Gradle 75 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 76 | 77 | :end 78 | @rem End local scope for the variables with windows NT shell 79 | if "%ERRORLEVEL%"=="0" goto mainEnd 80 | 81 | :fail 82 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 83 | rem the _cmd.exe /c_ return code! 84 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 85 | exit /b 1 86 | 87 | :mainEnd 88 | if "%OS%"=="Windows_NT" endlocal 89 | 90 | :omega 91 | -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | include ':app' 2 | --------------------------------------------------------------------------------