├── .gitignore ├── .idea ├── codeStyles │ └── Project.xml ├── gradle.xml ├── misc.xml ├── modules.xml └── vcs.xml ├── LICENSE.txt ├── LocalVPN.iml ├── README.md ├── app ├── .gitignore ├── app.iml ├── build.gradle ├── proguard-rules.pro └── src │ └── main │ ├── AndroidManifest.xml │ ├── java │ └── xyz │ │ └── hexene │ │ └── localvpn │ │ ├── ByteBufferPool.java │ │ ├── LRUCache.java │ │ ├── LocalVPN.java │ │ ├── LocalVPNService.java │ │ ├── Packet.java │ │ ├── TCB.java │ │ ├── TCPInput.java │ │ ├── TCPOutput.java │ │ ├── UDPInput.java │ │ └── UDPOutput.java │ └── res │ ├── drawable-hdpi │ └── ic_launcher.png │ ├── drawable-mdpi │ └── ic_launcher.png │ ├── drawable-xhdpi │ └── ic_launcher.png │ ├── drawable-xxhdpi │ └── ic_launcher.png │ ├── layout │ └── activity_local_vpn.xml │ ├── menu │ └── menu_local_vpn.xml │ ├── values-w820dp │ └── dimens.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 | .gradle 2 | /local.properties 3 | /.idea/workspace.xml 4 | /.idea/libraries 5 | .DS_Store 6 | /build 7 | -------------------------------------------------------------------------------- /.idea/codeStyles/Project.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 15 | 16 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /.idea/gradle.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 17 | 18 | -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 19 | 31 | 32 | 33 | 34 | 35 | 36 | 38 | -------------------------------------------------------------------------------- /.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | 204 | -------------------------------------------------------------------------------- /LocalVPN.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # LocalVPN 2 | A packet interceptor for Android built on top of VpnService 3 | 4 | License: Apache v2.0 5 | 6 | Early alpha, will eat your cat! 7 | -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /app/app.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 8 | 9 | 10 | 11 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | -------------------------------------------------------------------------------- /app/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.application' 2 | 3 | android { 4 | compileSdkVersion 21 5 | buildToolsVersion '28.0.3' 6 | 7 | defaultConfig { 8 | applicationId "xyz.hexene.localvpn" 9 | minSdkVersion 14 10 | targetSdkVersion 21 11 | versionCode 1 12 | versionName "1.0" 13 | } 14 | buildTypes { 15 | release { 16 | minifyEnabled false 17 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 18 | } 19 | } 20 | } 21 | 22 | dependencies { 23 | implementation fileTree(dir: 'libs', include: ['*.jar']) 24 | implementation 'com.android.support:appcompat-v7:21.0.3' 25 | } 26 | -------------------------------------------------------------------------------- /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 /Users/i069076/Library/Android/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 | -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 10 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /app/src/main/java/xyz/hexene/localvpn/ByteBufferPool.java: -------------------------------------------------------------------------------- 1 | /* 2 | ** Copyright 2015, Mohamed Naufal 3 | ** 4 | ** Licensed under the Apache License, Version 2.0 (the "License"); 5 | ** you may not use this file except in compliance with the License. 6 | ** You may obtain a copy of the License at 7 | ** 8 | ** http://www.apache.org/licenses/LICENSE-2.0 9 | ** 10 | ** Unless required by applicable law or agreed to in writing, software 11 | ** distributed under the License is distributed on an "AS IS" BASIS, 12 | ** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | ** See the License for the specific language governing permissions and 14 | ** limitations under the License. 15 | */ 16 | 17 | package xyz.hexene.localvpn; 18 | 19 | import java.nio.ByteBuffer; 20 | import java.util.concurrent.ConcurrentLinkedQueue; 21 | 22 | public class ByteBufferPool 23 | { 24 | private static final int BUFFER_SIZE = 16384; // XXX: Is this ideal? 25 | private static ConcurrentLinkedQueue pool = new ConcurrentLinkedQueue<>(); 26 | 27 | public static ByteBuffer acquire() 28 | { 29 | ByteBuffer buffer = pool.poll(); 30 | if (buffer == null) 31 | buffer = ByteBuffer.allocateDirect(BUFFER_SIZE); // Using DirectBuffer for zero-copy 32 | return buffer; 33 | } 34 | 35 | public static void release(ByteBuffer buffer) 36 | { 37 | buffer.clear(); 38 | pool.offer(buffer); 39 | } 40 | 41 | public static void clear() 42 | { 43 | pool.clear(); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /app/src/main/java/xyz/hexene/localvpn/LRUCache.java: -------------------------------------------------------------------------------- 1 | /* 2 | ** Copyright 2015, Mohamed Naufal 3 | ** 4 | ** Licensed under the Apache License, Version 2.0 (the "License"); 5 | ** you may not use this file except in compliance with the License. 6 | ** You may obtain a copy of the License at 7 | ** 8 | ** http://www.apache.org/licenses/LICENSE-2.0 9 | ** 10 | ** Unless required by applicable law or agreed to in writing, software 11 | ** distributed under the License is distributed on an "AS IS" BASIS, 12 | ** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | ** See the License for the specific language governing permissions and 14 | ** limitations under the License. 15 | */ 16 | 17 | package xyz.hexene.localvpn; 18 | 19 | import java.util.LinkedHashMap; 20 | 21 | public class LRUCache extends LinkedHashMap 22 | { 23 | private int maxSize; 24 | private CleanupCallback callback; 25 | 26 | public LRUCache(int maxSize, CleanupCallback callback) 27 | { 28 | super(maxSize + 1, 1, true); 29 | 30 | this.maxSize = maxSize; 31 | this.callback = callback; 32 | } 33 | 34 | @Override 35 | protected boolean removeEldestEntry(Entry eldest) 36 | { 37 | if (size() > maxSize) 38 | { 39 | callback.cleanup(eldest); 40 | return true; 41 | } 42 | return false; 43 | } 44 | 45 | public static interface CleanupCallback 46 | { 47 | public void cleanup(Entry eldest); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /app/src/main/java/xyz/hexene/localvpn/LocalVPN.java: -------------------------------------------------------------------------------- 1 | /* 2 | ** Copyright 2015, Mohamed Naufal 3 | ** 4 | ** Licensed under the Apache License, Version 2.0 (the "License"); 5 | ** you may not use this file except in compliance with the License. 6 | ** You may obtain a copy of the License at 7 | ** 8 | ** http://www.apache.org/licenses/LICENSE-2.0 9 | ** 10 | ** Unless required by applicable law or agreed to in writing, software 11 | ** distributed under the License is distributed on an "AS IS" BASIS, 12 | ** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | ** See the License for the specific language governing permissions and 14 | ** limitations under the License. 15 | */ 16 | 17 | package xyz.hexene.localvpn; 18 | 19 | import android.content.BroadcastReceiver; 20 | import android.content.Context; 21 | import android.content.Intent; 22 | import android.content.IntentFilter; 23 | import android.net.VpnService; 24 | import android.support.v4.content.LocalBroadcastManager; 25 | import android.support.v7.app.ActionBarActivity; 26 | import android.os.Bundle; 27 | import android.view.View; 28 | import android.widget.Button; 29 | 30 | 31 | public class LocalVPN extends ActionBarActivity 32 | { 33 | private static final int VPN_REQUEST_CODE = 0x0F; 34 | 35 | private boolean waitingForVPNStart; 36 | 37 | private BroadcastReceiver vpnStateReceiver = new BroadcastReceiver() 38 | { 39 | @Override 40 | public void onReceive(Context context, Intent intent) 41 | { 42 | if (LocalVPNService.BROADCAST_VPN_STATE.equals(intent.getAction())) 43 | { 44 | if (intent.getBooleanExtra("running", false)) 45 | waitingForVPNStart = false; 46 | } 47 | } 48 | }; 49 | 50 | @Override 51 | protected void onCreate(Bundle savedInstanceState) 52 | { 53 | super.onCreate(savedInstanceState); 54 | setContentView(R.layout.activity_local_vpn); 55 | final Button vpnButton = (Button)findViewById(R.id.vpn); 56 | vpnButton.setOnClickListener(new View.OnClickListener() 57 | { 58 | @Override 59 | public void onClick(View v) 60 | { 61 | startVPN(); 62 | } 63 | }); 64 | waitingForVPNStart = false; 65 | LocalBroadcastManager.getInstance(this).registerReceiver(vpnStateReceiver, 66 | new IntentFilter(LocalVPNService.BROADCAST_VPN_STATE)); 67 | } 68 | 69 | private void startVPN() 70 | { 71 | Intent vpnIntent = VpnService.prepare(this); 72 | if (vpnIntent != null) 73 | startActivityForResult(vpnIntent, VPN_REQUEST_CODE); 74 | else 75 | onActivityResult(VPN_REQUEST_CODE, RESULT_OK, null); 76 | } 77 | 78 | @Override 79 | protected void onActivityResult(int requestCode, int resultCode, Intent data) 80 | { 81 | super.onActivityResult(requestCode, resultCode, data); 82 | if (requestCode == VPN_REQUEST_CODE && resultCode == RESULT_OK) 83 | { 84 | waitingForVPNStart = true; 85 | startService(new Intent(this, LocalVPNService.class)); 86 | enableButton(false); 87 | } 88 | } 89 | 90 | @Override 91 | protected void onResume() { 92 | super.onResume(); 93 | 94 | enableButton(!waitingForVPNStart && !LocalVPNService.isRunning()); 95 | } 96 | 97 | private void enableButton(boolean enable) 98 | { 99 | final Button vpnButton = (Button) findViewById(R.id.vpn); 100 | if (enable) 101 | { 102 | vpnButton.setEnabled(true); 103 | vpnButton.setText(R.string.start_vpn); 104 | } 105 | else 106 | { 107 | vpnButton.setEnabled(false); 108 | vpnButton.setText(R.string.stop_vpn); 109 | } 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /app/src/main/java/xyz/hexene/localvpn/LocalVPNService.java: -------------------------------------------------------------------------------- 1 | /* 2 | ** Copyright 2015, Mohamed Naufal 3 | ** 4 | ** Licensed under the Apache License, Version 2.0 (the "License"); 5 | ** you may not use this file except in compliance with the License. 6 | ** You may obtain a copy of the License at 7 | ** 8 | ** http://www.apache.org/licenses/LICENSE-2.0 9 | ** 10 | ** Unless required by applicable law or agreed to in writing, software 11 | ** distributed under the License is distributed on an "AS IS" BASIS, 12 | ** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | ** See the License for the specific language governing permissions and 14 | ** limitations under the License. 15 | */ 16 | 17 | package xyz.hexene.localvpn; 18 | 19 | import android.app.PendingIntent; 20 | import android.content.Intent; 21 | import android.net.VpnService; 22 | import android.os.ParcelFileDescriptor; 23 | import android.support.v4.content.LocalBroadcastManager; 24 | import android.util.Log; 25 | 26 | import java.io.Closeable; 27 | import java.io.FileDescriptor; 28 | import java.io.FileInputStream; 29 | import java.io.FileOutputStream; 30 | import java.io.IOException; 31 | import java.nio.ByteBuffer; 32 | import java.nio.channels.FileChannel; 33 | import java.nio.channels.Selector; 34 | import java.util.concurrent.ConcurrentLinkedQueue; 35 | import java.util.concurrent.ExecutorService; 36 | import java.util.concurrent.Executors; 37 | 38 | public class LocalVPNService extends VpnService 39 | { 40 | private static final String TAG = LocalVPNService.class.getSimpleName(); 41 | private static final String VPN_ADDRESS = "10.0.0.2"; // Only IPv4 support for now 42 | private static final String VPN_ROUTE = "0.0.0.0"; // Intercept everything 43 | 44 | public static final String BROADCAST_VPN_STATE = "xyz.hexene.localvpn.VPN_STATE"; 45 | 46 | private static boolean isRunning = false; 47 | 48 | private ParcelFileDescriptor vpnInterface = null; 49 | 50 | private PendingIntent pendingIntent; 51 | 52 | private ConcurrentLinkedQueue deviceToNetworkUDPQueue; 53 | private ConcurrentLinkedQueue deviceToNetworkTCPQueue; 54 | private ConcurrentLinkedQueue networkToDeviceQueue; 55 | private ExecutorService executorService; 56 | 57 | private Selector udpSelector; 58 | private Selector tcpSelector; 59 | 60 | @Override 61 | public void onCreate() 62 | { 63 | super.onCreate(); 64 | isRunning = true; 65 | setupVPN(); 66 | try 67 | { 68 | udpSelector = Selector.open(); 69 | tcpSelector = Selector.open(); 70 | deviceToNetworkUDPQueue = new ConcurrentLinkedQueue<>(); 71 | deviceToNetworkTCPQueue = new ConcurrentLinkedQueue<>(); 72 | networkToDeviceQueue = new ConcurrentLinkedQueue<>(); 73 | 74 | executorService = Executors.newFixedThreadPool(5); 75 | executorService.submit(new UDPInput(networkToDeviceQueue, udpSelector)); 76 | executorService.submit(new UDPOutput(deviceToNetworkUDPQueue, udpSelector, this)); 77 | executorService.submit(new TCPInput(networkToDeviceQueue, tcpSelector)); 78 | executorService.submit(new TCPOutput(deviceToNetworkTCPQueue, networkToDeviceQueue, tcpSelector, this)); 79 | executorService.submit(new VPNRunnable(vpnInterface.getFileDescriptor(), 80 | deviceToNetworkUDPQueue, deviceToNetworkTCPQueue, networkToDeviceQueue)); 81 | LocalBroadcastManager.getInstance(this).sendBroadcast(new Intent(BROADCAST_VPN_STATE).putExtra("running", true)); 82 | Log.i(TAG, "Started"); 83 | } 84 | catch (IOException e) 85 | { 86 | // TODO: Here and elsewhere, we should explicitly notify the user of any errors 87 | // and suggest that they stop the service, since we can't do it ourselves 88 | Log.e(TAG, "Error starting service", e); 89 | cleanup(); 90 | } 91 | } 92 | 93 | private void setupVPN() 94 | { 95 | if (vpnInterface == null) 96 | { 97 | Builder builder = new Builder(); 98 | builder.addAddress(VPN_ADDRESS, 32); 99 | builder.addRoute(VPN_ROUTE, 0); 100 | vpnInterface = builder.setSession(getString(R.string.app_name)).setConfigureIntent(pendingIntent).establish(); 101 | } 102 | } 103 | 104 | @Override 105 | public int onStartCommand(Intent intent, int flags, int startId) 106 | { 107 | return START_STICKY; 108 | } 109 | 110 | public static boolean isRunning() 111 | { 112 | return isRunning; 113 | } 114 | 115 | @Override 116 | public void onDestroy() 117 | { 118 | super.onDestroy(); 119 | isRunning = false; 120 | executorService.shutdownNow(); 121 | cleanup(); 122 | Log.i(TAG, "Stopped"); 123 | } 124 | 125 | private void cleanup() 126 | { 127 | deviceToNetworkTCPQueue = null; 128 | deviceToNetworkUDPQueue = null; 129 | networkToDeviceQueue = null; 130 | ByteBufferPool.clear(); 131 | closeResources(udpSelector, tcpSelector, vpnInterface); 132 | } 133 | 134 | // TODO: Move this to a "utils" class for reuse 135 | private static void closeResources(Closeable... resources) 136 | { 137 | for (Closeable resource : resources) 138 | { 139 | try 140 | { 141 | resource.close(); 142 | } 143 | catch (IOException e) 144 | { 145 | // Ignore 146 | } 147 | } 148 | } 149 | 150 | private static class VPNRunnable implements Runnable 151 | { 152 | private static final String TAG = VPNRunnable.class.getSimpleName(); 153 | 154 | private FileDescriptor vpnFileDescriptor; 155 | 156 | private ConcurrentLinkedQueue deviceToNetworkUDPQueue; 157 | private ConcurrentLinkedQueue deviceToNetworkTCPQueue; 158 | private ConcurrentLinkedQueue networkToDeviceQueue; 159 | 160 | public VPNRunnable(FileDescriptor vpnFileDescriptor, 161 | ConcurrentLinkedQueue deviceToNetworkUDPQueue, 162 | ConcurrentLinkedQueue deviceToNetworkTCPQueue, 163 | ConcurrentLinkedQueue networkToDeviceQueue) 164 | { 165 | this.vpnFileDescriptor = vpnFileDescriptor; 166 | this.deviceToNetworkUDPQueue = deviceToNetworkUDPQueue; 167 | this.deviceToNetworkTCPQueue = deviceToNetworkTCPQueue; 168 | this.networkToDeviceQueue = networkToDeviceQueue; 169 | } 170 | 171 | @Override 172 | public void run() 173 | { 174 | Log.i(TAG, "Started"); 175 | 176 | FileChannel vpnInput = new FileInputStream(vpnFileDescriptor).getChannel(); 177 | FileChannel vpnOutput = new FileOutputStream(vpnFileDescriptor).getChannel(); 178 | 179 | try 180 | { 181 | ByteBuffer bufferToNetwork = null; 182 | boolean dataSent = true; 183 | boolean dataReceived; 184 | while (!Thread.interrupted()) 185 | { 186 | if (dataSent) 187 | bufferToNetwork = ByteBufferPool.acquire(); 188 | else 189 | bufferToNetwork.clear(); 190 | 191 | // TODO: Block when not connected 192 | int readBytes = vpnInput.read(bufferToNetwork); 193 | if (readBytes > 0) 194 | { 195 | dataSent = true; 196 | bufferToNetwork.flip(); 197 | Packet packet = new Packet(bufferToNetwork); 198 | if (packet.isUDP()) 199 | { 200 | deviceToNetworkUDPQueue.offer(packet); 201 | } 202 | else if (packet.isTCP()) 203 | { 204 | deviceToNetworkTCPQueue.offer(packet); 205 | } 206 | else 207 | { 208 | Log.w(TAG, "Unknown packet type"); 209 | Log.w(TAG, packet.ip4Header.toString()); 210 | dataSent = false; 211 | } 212 | } 213 | else 214 | { 215 | dataSent = false; 216 | } 217 | 218 | ByteBuffer bufferFromNetwork = networkToDeviceQueue.poll(); 219 | if (bufferFromNetwork != null) 220 | { 221 | bufferFromNetwork.flip(); 222 | while (bufferFromNetwork.hasRemaining()) 223 | vpnOutput.write(bufferFromNetwork); 224 | dataReceived = true; 225 | 226 | ByteBufferPool.release(bufferFromNetwork); 227 | } 228 | else 229 | { 230 | dataReceived = false; 231 | } 232 | 233 | // TODO: Sleep-looping is not very battery-friendly, consider blocking instead 234 | // Confirm if throughput with ConcurrentQueue is really higher compared to BlockingQueue 235 | if (!dataSent && !dataReceived) 236 | Thread.sleep(10); 237 | } 238 | } 239 | catch (InterruptedException e) 240 | { 241 | Log.i(TAG, "Stopping"); 242 | } 243 | catch (IOException e) 244 | { 245 | Log.w(TAG, e.toString(), e); 246 | } 247 | finally 248 | { 249 | closeResources(vpnInput, vpnOutput); 250 | } 251 | } 252 | } 253 | } 254 | -------------------------------------------------------------------------------- /app/src/main/java/xyz/hexene/localvpn/Packet.java: -------------------------------------------------------------------------------- 1 | /* 2 | ** Copyright 2015, Mohamed Naufal 3 | ** 4 | ** Licensed under the Apache License, Version 2.0 (the "License"); 5 | ** you may not use this file except in compliance with the License. 6 | ** You may obtain a copy of the License at 7 | ** 8 | ** http://www.apache.org/licenses/LICENSE-2.0 9 | ** 10 | ** Unless required by applicable law or agreed to in writing, software 11 | ** distributed under the License is distributed on an "AS IS" BASIS, 12 | ** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | ** See the License for the specific language governing permissions and 14 | ** limitations under the License. 15 | */ 16 | 17 | package xyz.hexene.localvpn; 18 | 19 | import java.net.InetAddress; 20 | import java.net.UnknownHostException; 21 | import java.nio.ByteBuffer; 22 | 23 | /** 24 | * Representation of an IP Packet 25 | */ 26 | // TODO: Reduce public mutability 27 | public class Packet 28 | { 29 | public static final int IP4_HEADER_SIZE = 20; 30 | public static final int TCP_HEADER_SIZE = 20; 31 | public static final int UDP_HEADER_SIZE = 8; 32 | 33 | public IP4Header ip4Header; 34 | public TCPHeader tcpHeader; 35 | public UDPHeader udpHeader; 36 | public ByteBuffer backingBuffer; 37 | 38 | private boolean isTCP; 39 | private boolean isUDP; 40 | 41 | public Packet(ByteBuffer buffer) throws UnknownHostException { 42 | this.ip4Header = new IP4Header(buffer); 43 | if (this.ip4Header.protocol == IP4Header.TransportProtocol.TCP) { 44 | this.tcpHeader = new TCPHeader(buffer); 45 | this.isTCP = true; 46 | } else if (ip4Header.protocol == IP4Header.TransportProtocol.UDP) { 47 | this.udpHeader = new UDPHeader(buffer); 48 | this.isUDP = true; 49 | } 50 | this.backingBuffer = buffer; 51 | } 52 | 53 | @Override 54 | public String toString() 55 | { 56 | final StringBuilder sb = new StringBuilder("Packet{"); 57 | sb.append("ip4Header=").append(ip4Header); 58 | if (isTCP) sb.append(", tcpHeader=").append(tcpHeader); 59 | else if (isUDP) sb.append(", udpHeader=").append(udpHeader); 60 | sb.append(", payloadSize=").append(backingBuffer.limit() - backingBuffer.position()); 61 | sb.append('}'); 62 | return sb.toString(); 63 | } 64 | 65 | public boolean isTCP() 66 | { 67 | return isTCP; 68 | } 69 | 70 | public boolean isUDP() 71 | { 72 | return isUDP; 73 | } 74 | 75 | public void swapSourceAndDestination() 76 | { 77 | InetAddress newSourceAddress = ip4Header.destinationAddress; 78 | ip4Header.destinationAddress = ip4Header.sourceAddress; 79 | ip4Header.sourceAddress = newSourceAddress; 80 | 81 | if (isUDP) 82 | { 83 | int newSourcePort = udpHeader.destinationPort; 84 | udpHeader.destinationPort = udpHeader.sourcePort; 85 | udpHeader.sourcePort = newSourcePort; 86 | } 87 | else if (isTCP) 88 | { 89 | int newSourcePort = tcpHeader.destinationPort; 90 | tcpHeader.destinationPort = tcpHeader.sourcePort; 91 | tcpHeader.sourcePort = newSourcePort; 92 | } 93 | } 94 | 95 | public void updateTCPBuffer(ByteBuffer buffer, byte flags, long sequenceNum, long ackNum, int payloadSize) 96 | { 97 | buffer.position(0); 98 | fillHeader(buffer); 99 | backingBuffer = buffer; 100 | 101 | tcpHeader.flags = flags; 102 | backingBuffer.put(IP4_HEADER_SIZE + 13, flags); 103 | 104 | tcpHeader.sequenceNumber = sequenceNum; 105 | backingBuffer.putInt(IP4_HEADER_SIZE + 4, (int) sequenceNum); 106 | 107 | tcpHeader.acknowledgementNumber = ackNum; 108 | backingBuffer.putInt(IP4_HEADER_SIZE + 8, (int) ackNum); 109 | 110 | // Reset header size, since we don't need options 111 | byte dataOffset = (byte) (TCP_HEADER_SIZE << 2); 112 | tcpHeader.dataOffsetAndReserved = dataOffset; 113 | backingBuffer.put(IP4_HEADER_SIZE + 12, dataOffset); 114 | 115 | updateTCPChecksum(payloadSize); 116 | 117 | int ip4TotalLength = IP4_HEADER_SIZE + TCP_HEADER_SIZE + payloadSize; 118 | backingBuffer.putShort(2, (short) ip4TotalLength); 119 | ip4Header.totalLength = ip4TotalLength; 120 | 121 | updateIP4Checksum(); 122 | } 123 | 124 | public void updateUDPBuffer(ByteBuffer buffer, int payloadSize) 125 | { 126 | buffer.position(0); 127 | fillHeader(buffer); 128 | backingBuffer = buffer; 129 | 130 | int udpTotalLength = UDP_HEADER_SIZE + payloadSize; 131 | backingBuffer.putShort(IP4_HEADER_SIZE + 4, (short) udpTotalLength); 132 | udpHeader.length = udpTotalLength; 133 | 134 | // Disable UDP checksum validation 135 | backingBuffer.putShort(IP4_HEADER_SIZE + 6, (short) 0); 136 | udpHeader.checksum = 0; 137 | 138 | int ip4TotalLength = IP4_HEADER_SIZE + udpTotalLength; 139 | backingBuffer.putShort(2, (short) ip4TotalLength); 140 | ip4Header.totalLength = ip4TotalLength; 141 | 142 | updateIP4Checksum(); 143 | } 144 | 145 | private void updateIP4Checksum() 146 | { 147 | ByteBuffer buffer = backingBuffer.duplicate(); 148 | buffer.position(0); 149 | 150 | // Clear previous checksum 151 | buffer.putShort(10, (short) 0); 152 | 153 | int ipLength = ip4Header.headerLength; 154 | int sum = 0; 155 | while (ipLength > 0) 156 | { 157 | sum += BitUtils.getUnsignedShort(buffer.getShort()); 158 | ipLength -= 2; 159 | } 160 | while (sum >> 16 > 0) 161 | sum = (sum & 0xFFFF) + (sum >> 16); 162 | 163 | sum = ~sum; 164 | ip4Header.headerChecksum = sum; 165 | backingBuffer.putShort(10, (short) sum); 166 | } 167 | 168 | private void updateTCPChecksum(int payloadSize) 169 | { 170 | int sum = 0; 171 | int tcpLength = TCP_HEADER_SIZE + payloadSize; 172 | 173 | // Calculate pseudo-header checksum 174 | ByteBuffer buffer = ByteBuffer.wrap(ip4Header.sourceAddress.getAddress()); 175 | sum = BitUtils.getUnsignedShort(buffer.getShort()) + BitUtils.getUnsignedShort(buffer.getShort()); 176 | 177 | buffer = ByteBuffer.wrap(ip4Header.destinationAddress.getAddress()); 178 | sum += BitUtils.getUnsignedShort(buffer.getShort()) + BitUtils.getUnsignedShort(buffer.getShort()); 179 | 180 | sum += IP4Header.TransportProtocol.TCP.getNumber() + tcpLength; 181 | 182 | buffer = backingBuffer.duplicate(); 183 | // Clear previous checksum 184 | buffer.putShort(IP4_HEADER_SIZE + 16, (short) 0); 185 | 186 | // Calculate TCP segment checksum 187 | buffer.position(IP4_HEADER_SIZE); 188 | while (tcpLength > 1) 189 | { 190 | sum += BitUtils.getUnsignedShort(buffer.getShort()); 191 | tcpLength -= 2; 192 | } 193 | if (tcpLength > 0) 194 | sum += BitUtils.getUnsignedByte(buffer.get()) << 8; 195 | 196 | while (sum >> 16 > 0) 197 | sum = (sum & 0xFFFF) + (sum >> 16); 198 | 199 | sum = ~sum; 200 | tcpHeader.checksum = sum; 201 | backingBuffer.putShort(IP4_HEADER_SIZE + 16, (short) sum); 202 | } 203 | 204 | private void fillHeader(ByteBuffer buffer) 205 | { 206 | ip4Header.fillHeader(buffer); 207 | if (isUDP) 208 | udpHeader.fillHeader(buffer); 209 | else if (isTCP) 210 | tcpHeader.fillHeader(buffer); 211 | } 212 | 213 | public static class IP4Header 214 | { 215 | public byte version; 216 | public byte IHL; 217 | public int headerLength; 218 | public short typeOfService; 219 | public int totalLength; 220 | 221 | public int identificationAndFlagsAndFragmentOffset; 222 | 223 | public short TTL; 224 | private short protocolNum; 225 | public TransportProtocol protocol; 226 | public int headerChecksum; 227 | 228 | public InetAddress sourceAddress; 229 | public InetAddress destinationAddress; 230 | 231 | public int optionsAndPadding; 232 | 233 | private enum TransportProtocol 234 | { 235 | TCP(6), 236 | UDP(17), 237 | Other(0xFF); 238 | 239 | private int protocolNumber; 240 | 241 | TransportProtocol(int protocolNumber) 242 | { 243 | this.protocolNumber = protocolNumber; 244 | } 245 | 246 | private static TransportProtocol numberToEnum(int protocolNumber) 247 | { 248 | if (protocolNumber == 6) 249 | return TCP; 250 | else if (protocolNumber == 17) 251 | return UDP; 252 | else 253 | return Other; 254 | } 255 | 256 | public int getNumber() 257 | { 258 | return this.protocolNumber; 259 | } 260 | } 261 | 262 | private IP4Header(ByteBuffer buffer) throws UnknownHostException 263 | { 264 | byte versionAndIHL = buffer.get(); 265 | this.version = (byte) (versionAndIHL >> 4); 266 | this.IHL = (byte) (versionAndIHL & 0x0F); 267 | this.headerLength = this.IHL << 2; 268 | 269 | this.typeOfService = BitUtils.getUnsignedByte(buffer.get()); 270 | this.totalLength = BitUtils.getUnsignedShort(buffer.getShort()); 271 | 272 | this.identificationAndFlagsAndFragmentOffset = buffer.getInt(); 273 | 274 | this.TTL = BitUtils.getUnsignedByte(buffer.get()); 275 | this.protocolNum = BitUtils.getUnsignedByte(buffer.get()); 276 | this.protocol = TransportProtocol.numberToEnum(protocolNum); 277 | this.headerChecksum = BitUtils.getUnsignedShort(buffer.getShort()); 278 | 279 | byte[] addressBytes = new byte[4]; 280 | buffer.get(addressBytes, 0, 4); 281 | this.sourceAddress = InetAddress.getByAddress(addressBytes); 282 | 283 | buffer.get(addressBytes, 0, 4); 284 | this.destinationAddress = InetAddress.getByAddress(addressBytes); 285 | 286 | //this.optionsAndPadding = buffer.getInt(); 287 | } 288 | 289 | public void fillHeader(ByteBuffer buffer) 290 | { 291 | buffer.put((byte) (this.version << 4 | this.IHL)); 292 | buffer.put((byte) this.typeOfService); 293 | buffer.putShort((short) this.totalLength); 294 | 295 | buffer.putInt(this.identificationAndFlagsAndFragmentOffset); 296 | 297 | buffer.put((byte) this.TTL); 298 | buffer.put((byte) this.protocol.getNumber()); 299 | buffer.putShort((short) this.headerChecksum); 300 | 301 | buffer.put(this.sourceAddress.getAddress()); 302 | buffer.put(this.destinationAddress.getAddress()); 303 | } 304 | 305 | @Override 306 | public String toString() 307 | { 308 | final StringBuilder sb = new StringBuilder("IP4Header{"); 309 | sb.append("version=").append(version); 310 | sb.append(", IHL=").append(IHL); 311 | sb.append(", typeOfService=").append(typeOfService); 312 | sb.append(", totalLength=").append(totalLength); 313 | sb.append(", identificationAndFlagsAndFragmentOffset=").append(identificationAndFlagsAndFragmentOffset); 314 | sb.append(", TTL=").append(TTL); 315 | sb.append(", protocol=").append(protocolNum).append(":").append(protocol); 316 | sb.append(", headerChecksum=").append(headerChecksum); 317 | sb.append(", sourceAddress=").append(sourceAddress.getHostAddress()); 318 | sb.append(", destinationAddress=").append(destinationAddress.getHostAddress()); 319 | sb.append('}'); 320 | return sb.toString(); 321 | } 322 | } 323 | 324 | public static class TCPHeader 325 | { 326 | public static final int FIN = 0x01; 327 | public static final int SYN = 0x02; 328 | public static final int RST = 0x04; 329 | public static final int PSH = 0x08; 330 | public static final int ACK = 0x10; 331 | public static final int URG = 0x20; 332 | 333 | public int sourcePort; 334 | public int destinationPort; 335 | 336 | public long sequenceNumber; 337 | public long acknowledgementNumber; 338 | 339 | public byte dataOffsetAndReserved; 340 | public int headerLength; 341 | public byte flags; 342 | public int window; 343 | 344 | public int checksum; 345 | public int urgentPointer; 346 | 347 | public byte[] optionsAndPadding; 348 | 349 | private TCPHeader(ByteBuffer buffer) 350 | { 351 | this.sourcePort = BitUtils.getUnsignedShort(buffer.getShort()); 352 | this.destinationPort = BitUtils.getUnsignedShort(buffer.getShort()); 353 | 354 | this.sequenceNumber = BitUtils.getUnsignedInt(buffer.getInt()); 355 | this.acknowledgementNumber = BitUtils.getUnsignedInt(buffer.getInt()); 356 | 357 | this.dataOffsetAndReserved = buffer.get(); 358 | this.headerLength = (this.dataOffsetAndReserved & 0xF0) >> 2; 359 | this.flags = buffer.get(); 360 | this.window = BitUtils.getUnsignedShort(buffer.getShort()); 361 | 362 | this.checksum = BitUtils.getUnsignedShort(buffer.getShort()); 363 | this.urgentPointer = BitUtils.getUnsignedShort(buffer.getShort()); 364 | 365 | int optionsLength = this.headerLength - TCP_HEADER_SIZE; 366 | if (optionsLength > 0) 367 | { 368 | optionsAndPadding = new byte[optionsLength]; 369 | buffer.get(optionsAndPadding, 0, optionsLength); 370 | } 371 | } 372 | 373 | public boolean isFIN() 374 | { 375 | return (flags & FIN) == FIN; 376 | } 377 | 378 | public boolean isSYN() 379 | { 380 | return (flags & SYN) == SYN; 381 | } 382 | 383 | public boolean isRST() 384 | { 385 | return (flags & RST) == RST; 386 | } 387 | 388 | public boolean isPSH() 389 | { 390 | return (flags & PSH) == PSH; 391 | } 392 | 393 | public boolean isACK() 394 | { 395 | return (flags & ACK) == ACK; 396 | } 397 | 398 | public boolean isURG() 399 | { 400 | return (flags & URG) == URG; 401 | } 402 | 403 | private void fillHeader(ByteBuffer buffer) 404 | { 405 | buffer.putShort((short) sourcePort); 406 | buffer.putShort((short) destinationPort); 407 | 408 | buffer.putInt((int) sequenceNumber); 409 | buffer.putInt((int) acknowledgementNumber); 410 | 411 | buffer.put(dataOffsetAndReserved); 412 | buffer.put(flags); 413 | buffer.putShort((short) window); 414 | 415 | buffer.putShort((short) checksum); 416 | buffer.putShort((short) urgentPointer); 417 | } 418 | 419 | @Override 420 | public String toString() 421 | { 422 | final StringBuilder sb = new StringBuilder("TCPHeader{"); 423 | sb.append("sourcePort=").append(sourcePort); 424 | sb.append(", destinationPort=").append(destinationPort); 425 | sb.append(", sequenceNumber=").append(sequenceNumber); 426 | sb.append(", acknowledgementNumber=").append(acknowledgementNumber); 427 | sb.append(", headerLength=").append(headerLength); 428 | sb.append(", window=").append(window); 429 | sb.append(", checksum=").append(checksum); 430 | sb.append(", flags="); 431 | if (isFIN()) sb.append(" FIN"); 432 | if (isSYN()) sb.append(" SYN"); 433 | if (isRST()) sb.append(" RST"); 434 | if (isPSH()) sb.append(" PSH"); 435 | if (isACK()) sb.append(" ACK"); 436 | if (isURG()) sb.append(" URG"); 437 | sb.append('}'); 438 | return sb.toString(); 439 | } 440 | } 441 | 442 | public static class UDPHeader 443 | { 444 | public int sourcePort; 445 | public int destinationPort; 446 | 447 | public int length; 448 | public int checksum; 449 | 450 | private UDPHeader(ByteBuffer buffer) 451 | { 452 | this.sourcePort = BitUtils.getUnsignedShort(buffer.getShort()); 453 | this.destinationPort = BitUtils.getUnsignedShort(buffer.getShort()); 454 | 455 | this.length = BitUtils.getUnsignedShort(buffer.getShort()); 456 | this.checksum = BitUtils.getUnsignedShort(buffer.getShort()); 457 | } 458 | 459 | private void fillHeader(ByteBuffer buffer) 460 | { 461 | buffer.putShort((short) this.sourcePort); 462 | buffer.putShort((short) this.destinationPort); 463 | 464 | buffer.putShort((short) this.length); 465 | buffer.putShort((short) this.checksum); 466 | } 467 | 468 | @Override 469 | public String toString() 470 | { 471 | final StringBuilder sb = new StringBuilder("UDPHeader{"); 472 | sb.append("sourcePort=").append(sourcePort); 473 | sb.append(", destinationPort=").append(destinationPort); 474 | sb.append(", length=").append(length); 475 | sb.append(", checksum=").append(checksum); 476 | sb.append('}'); 477 | return sb.toString(); 478 | } 479 | } 480 | 481 | private static class BitUtils 482 | { 483 | private static short getUnsignedByte(byte value) 484 | { 485 | return (short)(value & 0xFF); 486 | } 487 | 488 | private static int getUnsignedShort(short value) 489 | { 490 | return value & 0xFFFF; 491 | } 492 | 493 | private static long getUnsignedInt(int value) 494 | { 495 | return value & 0xFFFFFFFFL; 496 | } 497 | } 498 | } 499 | -------------------------------------------------------------------------------- /app/src/main/java/xyz/hexene/localvpn/TCB.java: -------------------------------------------------------------------------------- 1 | /* 2 | ** Copyright 2015, Mohamed Naufal 3 | ** 4 | ** Licensed under the Apache License, Version 2.0 (the "License"); 5 | ** you may not use this file except in compliance with the License. 6 | ** You may obtain a copy of the License at 7 | ** 8 | ** http://www.apache.org/licenses/LICENSE-2.0 9 | ** 10 | ** Unless required by applicable law or agreed to in writing, software 11 | ** distributed under the License is distributed on an "AS IS" BASIS, 12 | ** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | ** See the License for the specific language governing permissions and 14 | ** limitations under the License. 15 | */ 16 | 17 | package xyz.hexene.localvpn; 18 | 19 | import java.io.IOException; 20 | import java.nio.channels.SelectionKey; 21 | import java.nio.channels.SocketChannel; 22 | import java.util.Iterator; 23 | import java.util.Map; 24 | 25 | /** 26 | * Transmission Control Block 27 | */ 28 | public class TCB 29 | { 30 | public String ipAndPort; 31 | 32 | public long mySequenceNum, theirSequenceNum; 33 | public long myAcknowledgementNum, theirAcknowledgementNum; 34 | public TCBStatus status; 35 | 36 | // TCP has more states, but we need only these 37 | public enum TCBStatus 38 | { 39 | SYN_SENT, 40 | SYN_RECEIVED, 41 | ESTABLISHED, 42 | CLOSE_WAIT, 43 | LAST_ACK, 44 | } 45 | 46 | public Packet referencePacket; 47 | 48 | public SocketChannel channel; 49 | public boolean waitingForNetworkData; 50 | public SelectionKey selectionKey; 51 | 52 | private static final int MAX_CACHE_SIZE = 50; // XXX: Is this ideal? 53 | private static LRUCache tcbCache = 54 | new LRUCache<>(MAX_CACHE_SIZE, new LRUCache.CleanupCallback() 55 | { 56 | @Override 57 | public void cleanup(Map.Entry eldest) 58 | { 59 | eldest.getValue().closeChannel(); 60 | } 61 | }); 62 | 63 | public static TCB getTCB(String ipAndPort) 64 | { 65 | synchronized (tcbCache) 66 | { 67 | return tcbCache.get(ipAndPort); 68 | } 69 | } 70 | 71 | public static void putTCB(String ipAndPort, TCB tcb) 72 | { 73 | synchronized (tcbCache) 74 | { 75 | tcbCache.put(ipAndPort, tcb); 76 | } 77 | } 78 | 79 | public TCB(String ipAndPort, long mySequenceNum, long theirSequenceNum, long myAcknowledgementNum, long theirAcknowledgementNum, 80 | SocketChannel channel, Packet referencePacket) 81 | { 82 | this.ipAndPort = ipAndPort; 83 | 84 | this.mySequenceNum = mySequenceNum; 85 | this.theirSequenceNum = theirSequenceNum; 86 | this.myAcknowledgementNum = myAcknowledgementNum; 87 | this.theirAcknowledgementNum = theirAcknowledgementNum; 88 | 89 | this.channel = channel; 90 | this.referencePacket = referencePacket; 91 | } 92 | 93 | public static void closeTCB(TCB tcb) 94 | { 95 | tcb.closeChannel(); 96 | synchronized (tcbCache) 97 | { 98 | tcbCache.remove(tcb.ipAndPort); 99 | } 100 | } 101 | 102 | public static void closeAll() 103 | { 104 | synchronized (tcbCache) 105 | { 106 | Iterator> it = tcbCache.entrySet().iterator(); 107 | while (it.hasNext()) 108 | { 109 | it.next().getValue().closeChannel(); 110 | it.remove(); 111 | } 112 | } 113 | } 114 | 115 | private void closeChannel() 116 | { 117 | try 118 | { 119 | channel.close(); 120 | } 121 | catch (IOException e) 122 | { 123 | // Ignore 124 | } 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /app/src/main/java/xyz/hexene/localvpn/TCPInput.java: -------------------------------------------------------------------------------- 1 | /* 2 | ** Copyright 2015, Mohamed Naufal 3 | ** 4 | ** Licensed under the Apache License, Version 2.0 (the "License"); 5 | ** you may not use this file except in compliance with the License. 6 | ** You may obtain a copy of the License at 7 | ** 8 | ** http://www.apache.org/licenses/LICENSE-2.0 9 | ** 10 | ** Unless required by applicable law or agreed to in writing, software 11 | ** distributed under the License is distributed on an "AS IS" BASIS, 12 | ** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | ** See the License for the specific language governing permissions and 14 | ** limitations under the License. 15 | */ 16 | 17 | package xyz.hexene.localvpn; 18 | 19 | import android.util.Log; 20 | 21 | import java.io.IOException; 22 | import java.nio.ByteBuffer; 23 | import java.nio.channels.SelectionKey; 24 | import java.nio.channels.Selector; 25 | import java.nio.channels.SocketChannel; 26 | import java.util.Iterator; 27 | import java.util.Set; 28 | import java.util.concurrent.ConcurrentLinkedQueue; 29 | 30 | import xyz.hexene.localvpn.TCB.TCBStatus; 31 | 32 | public class TCPInput implements Runnable 33 | { 34 | private static final String TAG = TCPInput.class.getSimpleName(); 35 | private static final int HEADER_SIZE = Packet.IP4_HEADER_SIZE + Packet.TCP_HEADER_SIZE; 36 | 37 | private ConcurrentLinkedQueue outputQueue; 38 | private Selector selector; 39 | 40 | public TCPInput(ConcurrentLinkedQueue outputQueue, Selector selector) 41 | { 42 | this.outputQueue = outputQueue; 43 | this.selector = selector; 44 | } 45 | 46 | @Override 47 | public void run() 48 | { 49 | try 50 | { 51 | Log.d(TAG, "Started"); 52 | while (!Thread.interrupted()) 53 | { 54 | int readyChannels = selector.select(); 55 | 56 | if (readyChannels == 0) { 57 | Thread.sleep(10); 58 | continue; 59 | } 60 | 61 | Set keys = selector.selectedKeys(); 62 | Iterator keyIterator = keys.iterator(); 63 | 64 | while (keyIterator.hasNext() && !Thread.interrupted()) 65 | { 66 | SelectionKey key = keyIterator.next(); 67 | if (key.isValid()) 68 | { 69 | if (key.isConnectable()) 70 | processConnect(key, keyIterator); 71 | else if (key.isReadable()) 72 | processInput(key, keyIterator); 73 | } 74 | } 75 | } 76 | } 77 | catch (InterruptedException e) 78 | { 79 | Log.i(TAG, "Stopping"); 80 | } 81 | catch (IOException e) 82 | { 83 | Log.w(TAG, e.toString(), e); 84 | } 85 | } 86 | 87 | private void processConnect(SelectionKey key, Iterator keyIterator) 88 | { 89 | TCB tcb = (TCB) key.attachment(); 90 | Packet referencePacket = tcb.referencePacket; 91 | try 92 | { 93 | if (tcb.channel.finishConnect()) 94 | { 95 | keyIterator.remove(); 96 | tcb.status = TCBStatus.SYN_RECEIVED; 97 | 98 | // TODO: Set MSS for receiving larger packets from the device 99 | ByteBuffer responseBuffer = ByteBufferPool.acquire(); 100 | referencePacket.updateTCPBuffer(responseBuffer, (byte) (Packet.TCPHeader.SYN | Packet.TCPHeader.ACK), 101 | tcb.mySequenceNum, tcb.myAcknowledgementNum, 0); 102 | outputQueue.offer(responseBuffer); 103 | 104 | tcb.mySequenceNum++; // SYN counts as a byte 105 | key.interestOps(SelectionKey.OP_READ); 106 | } 107 | } 108 | catch (IOException e) 109 | { 110 | Log.e(TAG, "Connection error: " + tcb.ipAndPort, e); 111 | ByteBuffer responseBuffer = ByteBufferPool.acquire(); 112 | referencePacket.updateTCPBuffer(responseBuffer, (byte) Packet.TCPHeader.RST, 0, tcb.myAcknowledgementNum, 0); 113 | outputQueue.offer(responseBuffer); 114 | TCB.closeTCB(tcb); 115 | } 116 | } 117 | 118 | private void processInput(SelectionKey key, Iterator keyIterator) 119 | { 120 | keyIterator.remove(); 121 | ByteBuffer receiveBuffer = ByteBufferPool.acquire(); 122 | // Leave space for the header 123 | receiveBuffer.position(HEADER_SIZE); 124 | 125 | TCB tcb = (TCB) key.attachment(); 126 | synchronized (tcb) 127 | { 128 | Packet referencePacket = tcb.referencePacket; 129 | SocketChannel inputChannel = (SocketChannel) key.channel(); 130 | int readBytes; 131 | try 132 | { 133 | readBytes = inputChannel.read(receiveBuffer); 134 | } 135 | catch (IOException e) 136 | { 137 | Log.e(TAG, "Network read error: " + tcb.ipAndPort, e); 138 | referencePacket.updateTCPBuffer(receiveBuffer, (byte) Packet.TCPHeader.RST, 0, tcb.myAcknowledgementNum, 0); 139 | outputQueue.offer(receiveBuffer); 140 | TCB.closeTCB(tcb); 141 | return; 142 | } 143 | 144 | if (readBytes == -1) 145 | { 146 | // End of stream, stop waiting until we push more data 147 | key.interestOps(0); 148 | tcb.waitingForNetworkData = false; 149 | 150 | if (tcb.status != TCBStatus.CLOSE_WAIT) 151 | { 152 | ByteBufferPool.release(receiveBuffer); 153 | return; 154 | } 155 | 156 | tcb.status = TCBStatus.LAST_ACK; 157 | referencePacket.updateTCPBuffer(receiveBuffer, (byte) Packet.TCPHeader.FIN, tcb.mySequenceNum, tcb.myAcknowledgementNum, 0); 158 | tcb.mySequenceNum++; // FIN counts as a byte 159 | } 160 | else 161 | { 162 | // XXX: We should ideally be splitting segments by MTU/MSS, but this seems to work without 163 | referencePacket.updateTCPBuffer(receiveBuffer, (byte) (Packet.TCPHeader.PSH | Packet.TCPHeader.ACK), 164 | tcb.mySequenceNum, tcb.myAcknowledgementNum, readBytes); 165 | tcb.mySequenceNum += readBytes; // Next sequence number 166 | receiveBuffer.position(HEADER_SIZE + readBytes); 167 | } 168 | } 169 | outputQueue.offer(receiveBuffer); 170 | } 171 | } 172 | -------------------------------------------------------------------------------- /app/src/main/java/xyz/hexene/localvpn/TCPOutput.java: -------------------------------------------------------------------------------- 1 | /* 2 | ** Copyright 2015, Mohamed Naufal 3 | ** 4 | ** Licensed under the Apache License, Version 2.0 (the "License"); 5 | ** you may not use this file except in compliance with the License. 6 | ** You may obtain a copy of the License at 7 | ** 8 | ** http://www.apache.org/licenses/LICENSE-2.0 9 | ** 10 | ** Unless required by applicable law or agreed to in writing, software 11 | ** distributed under the License is distributed on an "AS IS" BASIS, 12 | ** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | ** See the License for the specific language governing permissions and 14 | ** limitations under the License. 15 | */ 16 | 17 | package xyz.hexene.localvpn; 18 | 19 | import android.util.Log; 20 | 21 | import java.io.IOException; 22 | import java.net.InetAddress; 23 | import java.net.InetSocketAddress; 24 | import java.nio.ByteBuffer; 25 | import java.nio.channels.SelectionKey; 26 | import java.nio.channels.Selector; 27 | import java.nio.channels.SocketChannel; 28 | import java.util.Random; 29 | import java.util.concurrent.ConcurrentLinkedQueue; 30 | 31 | import xyz.hexene.localvpn.Packet.TCPHeader; 32 | import xyz.hexene.localvpn.TCB.TCBStatus; 33 | 34 | public class TCPOutput implements Runnable 35 | { 36 | private static final String TAG = TCPOutput.class.getSimpleName(); 37 | 38 | private LocalVPNService vpnService; 39 | private ConcurrentLinkedQueue inputQueue; 40 | private ConcurrentLinkedQueue outputQueue; 41 | private Selector selector; 42 | 43 | private Random random = new Random(); 44 | public TCPOutput(ConcurrentLinkedQueue inputQueue, ConcurrentLinkedQueue outputQueue, 45 | Selector selector, LocalVPNService vpnService) 46 | { 47 | this.inputQueue = inputQueue; 48 | this.outputQueue = outputQueue; 49 | this.selector = selector; 50 | this.vpnService = vpnService; 51 | } 52 | 53 | @Override 54 | public void run() 55 | { 56 | Log.i(TAG, "Started"); 57 | try 58 | { 59 | 60 | Thread currentThread = Thread.currentThread(); 61 | while (true) 62 | { 63 | Packet currentPacket; 64 | // TODO: Block when not connected 65 | do 66 | { 67 | currentPacket = inputQueue.poll(); 68 | if (currentPacket != null) 69 | break; 70 | Thread.sleep(10); 71 | } while (!currentThread.isInterrupted()); 72 | 73 | if (currentThread.isInterrupted()) 74 | break; 75 | 76 | ByteBuffer payloadBuffer = currentPacket.backingBuffer; 77 | currentPacket.backingBuffer = null; 78 | ByteBuffer responseBuffer = ByteBufferPool.acquire(); 79 | 80 | InetAddress destinationAddress = currentPacket.ip4Header.destinationAddress; 81 | 82 | TCPHeader tcpHeader = currentPacket.tcpHeader; 83 | int destinationPort = tcpHeader.destinationPort; 84 | int sourcePort = tcpHeader.sourcePort; 85 | 86 | String ipAndPort = destinationAddress.getHostAddress() + ":" + 87 | destinationPort + ":" + sourcePort; 88 | TCB tcb = TCB.getTCB(ipAndPort); 89 | if (tcb == null) 90 | initializeConnection(ipAndPort, destinationAddress, destinationPort, 91 | currentPacket, tcpHeader, responseBuffer); 92 | else if (tcpHeader.isSYN()) 93 | processDuplicateSYN(tcb, tcpHeader, responseBuffer); 94 | else if (tcpHeader.isRST()) 95 | closeCleanly(tcb, responseBuffer); 96 | else if (tcpHeader.isFIN()) 97 | processFIN(tcb, tcpHeader, responseBuffer); 98 | else if (tcpHeader.isACK()) 99 | processACK(tcb, tcpHeader, payloadBuffer, responseBuffer); 100 | 101 | // XXX: cleanup later 102 | if (responseBuffer.position() == 0) 103 | ByteBufferPool.release(responseBuffer); 104 | ByteBufferPool.release(payloadBuffer); 105 | } 106 | } 107 | catch (InterruptedException e) 108 | { 109 | Log.i(TAG, "Stopping"); 110 | } 111 | catch (IOException e) 112 | { 113 | Log.e(TAG, e.toString(), e); 114 | } 115 | finally 116 | { 117 | TCB.closeAll(); 118 | } 119 | } 120 | 121 | private void initializeConnection(String ipAndPort, InetAddress destinationAddress, int destinationPort, 122 | Packet currentPacket, TCPHeader tcpHeader, ByteBuffer responseBuffer) 123 | throws IOException 124 | { 125 | currentPacket.swapSourceAndDestination(); 126 | if (tcpHeader.isSYN()) 127 | { 128 | SocketChannel outputChannel = SocketChannel.open(); 129 | outputChannel.configureBlocking(false); 130 | vpnService.protect(outputChannel.socket()); 131 | 132 | TCB tcb = new TCB(ipAndPort, random.nextInt(Short.MAX_VALUE + 1), tcpHeader.sequenceNumber, tcpHeader.sequenceNumber + 1, 133 | tcpHeader.acknowledgementNumber, outputChannel, currentPacket); 134 | TCB.putTCB(ipAndPort, tcb); 135 | 136 | try 137 | { 138 | outputChannel.connect(new InetSocketAddress(destinationAddress, destinationPort)); 139 | if (outputChannel.finishConnect()) 140 | { 141 | tcb.status = TCBStatus.SYN_RECEIVED; 142 | // TODO: Set MSS for receiving larger packets from the device 143 | currentPacket.updateTCPBuffer(responseBuffer, (byte) (TCPHeader.SYN | TCPHeader.ACK), 144 | tcb.mySequenceNum, tcb.myAcknowledgementNum, 0); 145 | tcb.mySequenceNum++; // SYN counts as a byte 146 | } 147 | else 148 | { 149 | tcb.status = TCBStatus.SYN_SENT; 150 | selector.wakeup(); 151 | tcb.selectionKey = outputChannel.register(selector, SelectionKey.OP_CONNECT, tcb); 152 | return; 153 | } 154 | } 155 | catch (IOException e) 156 | { 157 | Log.e(TAG, "Connection error: " + ipAndPort, e); 158 | currentPacket.updateTCPBuffer(responseBuffer, (byte) TCPHeader.RST, 0, tcb.myAcknowledgementNum, 0); 159 | TCB.closeTCB(tcb); 160 | } 161 | } 162 | else 163 | { 164 | currentPacket.updateTCPBuffer(responseBuffer, (byte) TCPHeader.RST, 165 | 0, tcpHeader.sequenceNumber + 1, 0); 166 | } 167 | outputQueue.offer(responseBuffer); 168 | } 169 | 170 | private void processDuplicateSYN(TCB tcb, TCPHeader tcpHeader, ByteBuffer responseBuffer) 171 | { 172 | synchronized (tcb) 173 | { 174 | if (tcb.status == TCBStatus.SYN_SENT) 175 | { 176 | tcb.myAcknowledgementNum = tcpHeader.sequenceNumber + 1; 177 | return; 178 | } 179 | } 180 | sendRST(tcb, 1, responseBuffer); 181 | } 182 | 183 | private void processFIN(TCB tcb, TCPHeader tcpHeader, ByteBuffer responseBuffer) 184 | { 185 | synchronized (tcb) 186 | { 187 | Packet referencePacket = tcb.referencePacket; 188 | tcb.myAcknowledgementNum = tcpHeader.sequenceNumber + 1; 189 | tcb.theirAcknowledgementNum = tcpHeader.acknowledgementNumber; 190 | 191 | if (tcb.waitingForNetworkData) 192 | { 193 | tcb.status = TCBStatus.CLOSE_WAIT; 194 | referencePacket.updateTCPBuffer(responseBuffer, (byte) TCPHeader.ACK, 195 | tcb.mySequenceNum, tcb.myAcknowledgementNum, 0); 196 | } 197 | else 198 | { 199 | tcb.status = TCBStatus.LAST_ACK; 200 | referencePacket.updateTCPBuffer(responseBuffer, (byte) (TCPHeader.FIN | TCPHeader.ACK), 201 | tcb.mySequenceNum, tcb.myAcknowledgementNum, 0); 202 | tcb.mySequenceNum++; // FIN counts as a byte 203 | } 204 | } 205 | outputQueue.offer(responseBuffer); 206 | } 207 | 208 | private void processACK(TCB tcb, TCPHeader tcpHeader, ByteBuffer payloadBuffer, ByteBuffer responseBuffer) throws IOException 209 | { 210 | int payloadSize = payloadBuffer.limit() - payloadBuffer.position(); 211 | 212 | synchronized (tcb) 213 | { 214 | SocketChannel outputChannel = tcb.channel; 215 | if (tcb.status == TCBStatus.SYN_RECEIVED) 216 | { 217 | tcb.status = TCBStatus.ESTABLISHED; 218 | 219 | selector.wakeup(); 220 | tcb.selectionKey = outputChannel.register(selector, SelectionKey.OP_READ, tcb); 221 | tcb.waitingForNetworkData = true; 222 | } 223 | else if (tcb.status == TCBStatus.LAST_ACK) 224 | { 225 | closeCleanly(tcb, responseBuffer); 226 | return; 227 | } 228 | 229 | if (payloadSize == 0) return; // Empty ACK, ignore 230 | 231 | if (!tcb.waitingForNetworkData) 232 | { 233 | selector.wakeup(); 234 | tcb.selectionKey.interestOps(SelectionKey.OP_READ); 235 | tcb.waitingForNetworkData = true; 236 | } 237 | 238 | // Forward to remote server 239 | try 240 | { 241 | while (payloadBuffer.hasRemaining()) 242 | outputChannel.write(payloadBuffer); 243 | } 244 | catch (IOException e) 245 | { 246 | Log.e(TAG, "Network write error: " + tcb.ipAndPort, e); 247 | sendRST(tcb, payloadSize, responseBuffer); 248 | return; 249 | } 250 | 251 | // TODO: We don't expect out-of-order packets, but verify 252 | tcb.myAcknowledgementNum = tcpHeader.sequenceNumber + payloadSize; 253 | tcb.theirAcknowledgementNum = tcpHeader.acknowledgementNumber; 254 | Packet referencePacket = tcb.referencePacket; 255 | referencePacket.updateTCPBuffer(responseBuffer, (byte) TCPHeader.ACK, tcb.mySequenceNum, tcb.myAcknowledgementNum, 0); 256 | } 257 | outputQueue.offer(responseBuffer); 258 | } 259 | 260 | private void sendRST(TCB tcb, int prevPayloadSize, ByteBuffer buffer) 261 | { 262 | tcb.referencePacket.updateTCPBuffer(buffer, (byte) TCPHeader.RST, 0, tcb.myAcknowledgementNum + prevPayloadSize, 0); 263 | outputQueue.offer(buffer); 264 | TCB.closeTCB(tcb); 265 | } 266 | 267 | private void closeCleanly(TCB tcb, ByteBuffer buffer) 268 | { 269 | ByteBufferPool.release(buffer); 270 | TCB.closeTCB(tcb); 271 | } 272 | } 273 | -------------------------------------------------------------------------------- /app/src/main/java/xyz/hexene/localvpn/UDPInput.java: -------------------------------------------------------------------------------- 1 | /* 2 | ** Copyright 2015, Mohamed Naufal 3 | ** 4 | ** Licensed under the Apache License, Version 2.0 (the "License"); 5 | ** you may not use this file except in compliance with the License. 6 | ** You may obtain a copy of the License at 7 | ** 8 | ** http://www.apache.org/licenses/LICENSE-2.0 9 | ** 10 | ** Unless required by applicable law or agreed to in writing, software 11 | ** distributed under the License is distributed on an "AS IS" BASIS, 12 | ** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | ** See the License for the specific language governing permissions and 14 | ** limitations under the License. 15 | */ 16 | 17 | package xyz.hexene.localvpn; 18 | 19 | import android.util.Log; 20 | 21 | import java.io.IOException; 22 | import java.nio.ByteBuffer; 23 | import java.nio.channels.DatagramChannel; 24 | import java.nio.channels.SelectionKey; 25 | import java.nio.channels.Selector; 26 | import java.util.Iterator; 27 | import java.util.Set; 28 | import java.util.concurrent.ConcurrentLinkedQueue; 29 | 30 | public class UDPInput implements Runnable 31 | { 32 | private static final String TAG = UDPInput.class.getSimpleName(); 33 | private static final int HEADER_SIZE = Packet.IP4_HEADER_SIZE + Packet.UDP_HEADER_SIZE; 34 | 35 | private Selector selector; 36 | private ConcurrentLinkedQueue outputQueue; 37 | 38 | public UDPInput(ConcurrentLinkedQueue outputQueue, Selector selector) 39 | { 40 | this.outputQueue = outputQueue; 41 | this.selector = selector; 42 | } 43 | 44 | @Override 45 | public void run() 46 | { 47 | try 48 | { 49 | Log.i(TAG, "Started"); 50 | while (!Thread.interrupted()) 51 | { 52 | int readyChannels = selector.select(); 53 | 54 | if (readyChannels == 0) { 55 | Thread.sleep(10); 56 | continue; 57 | } 58 | 59 | Set keys = selector.selectedKeys(); 60 | Iterator keyIterator = keys.iterator(); 61 | 62 | while (keyIterator.hasNext() && !Thread.interrupted()) 63 | { 64 | SelectionKey key = keyIterator.next(); 65 | if (key.isValid() && key.isReadable()) 66 | { 67 | keyIterator.remove(); 68 | 69 | ByteBuffer receiveBuffer = ByteBufferPool.acquire(); 70 | // Leave space for the header 71 | receiveBuffer.position(HEADER_SIZE); 72 | 73 | DatagramChannel inputChannel = (DatagramChannel) key.channel(); 74 | // XXX: We should handle any IOExceptions here immediately, 75 | // but that probably won't happen with UDP 76 | int readBytes = inputChannel.read(receiveBuffer); 77 | 78 | Packet referencePacket = (Packet) key.attachment(); 79 | referencePacket.updateUDPBuffer(receiveBuffer, readBytes); 80 | receiveBuffer.position(HEADER_SIZE + readBytes); 81 | 82 | outputQueue.offer(receiveBuffer); 83 | } 84 | } 85 | } 86 | } 87 | catch (InterruptedException e) 88 | { 89 | Log.i(TAG, "Stopping"); 90 | } 91 | catch (IOException e) 92 | { 93 | Log.w(TAG, e.toString(), e); 94 | } 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /app/src/main/java/xyz/hexene/localvpn/UDPOutput.java: -------------------------------------------------------------------------------- 1 | /* 2 | ** Copyright 2015, Mohamed Naufal 3 | ** 4 | ** Licensed under the Apache License, Version 2.0 (the "License"); 5 | ** you may not use this file except in compliance with the License. 6 | ** You may obtain a copy of the License at 7 | ** 8 | ** http://www.apache.org/licenses/LICENSE-2.0 9 | ** 10 | ** Unless required by applicable law or agreed to in writing, software 11 | ** distributed under the License is distributed on an "AS IS" BASIS, 12 | ** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | ** See the License for the specific language governing permissions and 14 | ** limitations under the License. 15 | */ 16 | 17 | package xyz.hexene.localvpn; 18 | 19 | import android.util.Log; 20 | 21 | import java.io.IOException; 22 | import java.net.InetAddress; 23 | import java.net.InetSocketAddress; 24 | import java.nio.ByteBuffer; 25 | import java.nio.channels.DatagramChannel; 26 | import java.nio.channels.SelectionKey; 27 | import java.nio.channels.Selector; 28 | import java.util.Iterator; 29 | import java.util.Map; 30 | import java.util.concurrent.ConcurrentLinkedQueue; 31 | 32 | public class UDPOutput implements Runnable 33 | { 34 | private static final String TAG = UDPOutput.class.getSimpleName(); 35 | 36 | private LocalVPNService vpnService; 37 | private ConcurrentLinkedQueue inputQueue; 38 | private Selector selector; 39 | 40 | private static final int MAX_CACHE_SIZE = 50; 41 | private LRUCache channelCache = 42 | new LRUCache<>(MAX_CACHE_SIZE, new LRUCache.CleanupCallback() 43 | { 44 | @Override 45 | public void cleanup(Map.Entry eldest) 46 | { 47 | closeChannel(eldest.getValue()); 48 | } 49 | }); 50 | 51 | public UDPOutput(ConcurrentLinkedQueue inputQueue, Selector selector, LocalVPNService vpnService) 52 | { 53 | this.inputQueue = inputQueue; 54 | this.selector = selector; 55 | this.vpnService = vpnService; 56 | } 57 | 58 | @Override 59 | public void run() 60 | { 61 | Log.i(TAG, "Started"); 62 | try 63 | { 64 | 65 | Thread currentThread = Thread.currentThread(); 66 | while (true) 67 | { 68 | Packet currentPacket; 69 | // TODO: Block when not connected 70 | do 71 | { 72 | currentPacket = inputQueue.poll(); 73 | if (currentPacket != null) 74 | break; 75 | Thread.sleep(10); 76 | } while (!currentThread.isInterrupted()); 77 | 78 | if (currentThread.isInterrupted()) 79 | break; 80 | 81 | InetAddress destinationAddress = currentPacket.ip4Header.destinationAddress; 82 | int destinationPort = currentPacket.udpHeader.destinationPort; 83 | int sourcePort = currentPacket.udpHeader.sourcePort; 84 | 85 | String ipAndPort = destinationAddress.getHostAddress() + ":" + destinationPort + ":" + sourcePort; 86 | DatagramChannel outputChannel = channelCache.get(ipAndPort); 87 | if (outputChannel == null) { 88 | outputChannel = DatagramChannel.open(); 89 | vpnService.protect(outputChannel.socket()); 90 | try 91 | { 92 | outputChannel.connect(new InetSocketAddress(destinationAddress, destinationPort)); 93 | } 94 | catch (IOException e) 95 | { 96 | Log.e(TAG, "Connection error: " + ipAndPort, e); 97 | closeChannel(outputChannel); 98 | ByteBufferPool.release(currentPacket.backingBuffer); 99 | continue; 100 | } 101 | outputChannel.configureBlocking(false); 102 | currentPacket.swapSourceAndDestination(); 103 | 104 | selector.wakeup(); 105 | outputChannel.register(selector, SelectionKey.OP_READ, currentPacket); 106 | 107 | channelCache.put(ipAndPort, outputChannel); 108 | } 109 | 110 | try 111 | { 112 | ByteBuffer payloadBuffer = currentPacket.backingBuffer; 113 | while (payloadBuffer.hasRemaining()) 114 | outputChannel.write(payloadBuffer); 115 | } 116 | catch (IOException e) 117 | { 118 | Log.e(TAG, "Network write error: " + ipAndPort, e); 119 | channelCache.remove(ipAndPort); 120 | closeChannel(outputChannel); 121 | } 122 | ByteBufferPool.release(currentPacket.backingBuffer); 123 | } 124 | } 125 | catch (InterruptedException e) 126 | { 127 | Log.i(TAG, "Stopping"); 128 | } 129 | catch (IOException e) 130 | { 131 | Log.i(TAG, e.toString(), e); 132 | } 133 | finally 134 | { 135 | closeAll(); 136 | } 137 | } 138 | 139 | private void closeAll() 140 | { 141 | Iterator> it = channelCache.entrySet().iterator(); 142 | while (it.hasNext()) 143 | { 144 | closeChannel(it.next().getValue()); 145 | it.remove(); 146 | } 147 | } 148 | 149 | private void closeChannel(DatagramChannel channel) 150 | { 151 | try 152 | { 153 | channel.close(); 154 | } 155 | catch (IOException e) 156 | { 157 | // Ignore 158 | } 159 | } 160 | } 161 | -------------------------------------------------------------------------------- /app/src/main/res/drawable-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hexene/LocalVPN/d48572c4ecbc8d31de08bcafd4de633564a23db4/app/src/main/res/drawable-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hexene/LocalVPN/d48572c4ecbc8d31de08bcafd4de633564a23db4/app/src/main/res/drawable-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hexene/LocalVPN/d48572c4ecbc8d31de08bcafd4de633564a23db4/app/src/main/res/drawable-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hexene/LocalVPN/d48572c4ecbc8d31de08bcafd4de633564a23db4/app/src/main/res/drawable-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_local_vpn.xml: -------------------------------------------------------------------------------- 1 | 7 | 8 |