├── .gitignore ├── .idea ├── .gitignore ├── AndroidProjectSystem.xml ├── compiler.xml ├── deploymentTargetDropDown.xml ├── deploymentTargetSelector.xml ├── gradle.xml ├── migrations.xml ├── misc.xml ├── runConfigurations.xml └── vcs.xml ├── README.md ├── app ├── .gitignore ├── build.gradle ├── proguard-rules.pro └── src │ ├── androidTest │ └── java │ │ └── com │ │ └── github │ │ └── zhkl0228 │ │ └── androidvpn │ │ └── ExampleInstrumentedTest.java │ ├── main │ ├── AndroidManifest.xml │ ├── java │ │ └── com │ │ │ └── github │ │ │ └── zhkl0228 │ │ │ └── androidvpn │ │ │ ├── AndroidVPN.java │ │ │ ├── HostPortDiscover.java │ │ │ ├── IPUtil.java │ │ │ ├── InspectorVpnService.java │ │ │ ├── Package.java │ │ │ └── StartVpnActivity.java │ └── res │ │ ├── drawable-v24 │ │ └── ic_launcher_foreground.xml │ │ ├── drawable │ │ └── ic_launcher_background.xml │ │ ├── layout │ │ └── activity_start_vpn.xml │ │ ├── mipmap-hdpi │ │ └── appicon.png │ │ ├── values-land │ │ └── dimens.xml │ │ ├── values-night │ │ └── themes.xml │ │ ├── values-w1240dp │ │ └── dimens.xml │ │ ├── values-w600dp │ │ └── dimens.xml │ │ ├── values │ │ ├── colors.xml │ │ ├── dimens.xml │ │ ├── strings.xml │ │ └── themes.xml │ │ └── xml │ │ ├── backup_rules.xml │ │ └── data_extraction_rules.xml │ └── test │ └── java │ └── com │ └── github │ └── zhkl0228 │ └── androidvpn │ └── ExampleUnitTest.java ├── build.gradle ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat └── settings.gradle /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | /local.properties 4 | /.idea/caches 5 | /.idea/libraries 6 | /.idea/modules.xml 7 | /.idea/workspace.xml 8 | /.idea/navEditor.xml 9 | /.idea/assetWizardSettings.xml 10 | .DS_Store 11 | /build 12 | /captures 13 | .externalNativeBuild 14 | .cxx 15 | local.properties 16 | -------------------------------------------------------------------------------- /.idea/.gitignore: -------------------------------------------------------------------------------- 1 | # Default ignored files 2 | /shelf/ 3 | /workspace.xml 4 | -------------------------------------------------------------------------------- /.idea/AndroidProjectSystem.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | -------------------------------------------------------------------------------- /.idea/compiler.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.idea/deploymentTargetDropDown.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /.idea/deploymentTargetSelector.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /.idea/gradle.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 18 | 19 | -------------------------------------------------------------------------------- /.idea/migrations.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 10 | -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 10 | -------------------------------------------------------------------------------- /.idea/runConfigurations.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 16 | 17 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # AndroidVPN 2 | 3 | - [VPN Server](https://github.com/zhkl0228/libnetguard) 4 | 5 | [frida_multiple_unpinning.js](https://gist.github.com/akabe1/5632cbc1cd49f0237cbd0a93bc8e4452) 6 | -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | /debug/ 3 | /release/ 4 | -------------------------------------------------------------------------------- /app/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'com.android.application' 3 | } 4 | 5 | android { 6 | namespace 'com.github.zhkl0228.androidvpn' 7 | compileSdk 34 8 | 9 | defaultConfig { 10 | applicationId "com.github.zhkl0228.androidvpn" 11 | minSdk 29 12 | targetSdk 34 13 | versionCode 1 14 | versionName "1.0" 15 | 16 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" 17 | } 18 | 19 | buildTypes { 20 | release { 21 | minifyEnabled false 22 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' 23 | } 24 | } 25 | compileOptions { 26 | sourceCompatibility JavaVersion.VERSION_1_8 27 | targetCompatibility JavaVersion.VERSION_1_8 28 | } 29 | buildFeatures { 30 | viewBinding true 31 | } 32 | } 33 | 34 | dependencies { 35 | implementation 'androidx.appcompat:appcompat:1.6.1' 36 | implementation 'com.google.android.material:material:1.11.0' 37 | implementation 'androidx.annotation:annotation:1.7.1' 38 | implementation 'androidx.constraintlayout:constraintlayout:2.1.4' 39 | implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.7.0' 40 | implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.7.0' 41 | testImplementation 'junit:junit:4.13.2' 42 | androidTestImplementation 'androidx.test.ext:junit:1.1.5' 43 | androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1' 44 | } -------------------------------------------------------------------------------- /app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile -------------------------------------------------------------------------------- /app/src/androidTest/java/com/github/zhkl0228/androidvpn/ExampleInstrumentedTest.java: -------------------------------------------------------------------------------- 1 | package com.github.zhkl0228.androidvpn; 2 | 3 | import android.content.Context; 4 | 5 | import androidx.test.platform.app.InstrumentationRegistry; 6 | import androidx.test.ext.junit.runners.AndroidJUnit4; 7 | 8 | import org.junit.Test; 9 | import org.junit.runner.RunWith; 10 | 11 | import static org.junit.Assert.*; 12 | 13 | /** 14 | * Instrumented test, which will execute on an Android device. 15 | * 16 | * @see Testing documentation 17 | */ 18 | @RunWith(AndroidJUnit4.class) 19 | public class ExampleInstrumentedTest { 20 | @Test 21 | public void useAppContext() { 22 | // Context of the app under test. 23 | Context appContext = InstrumentationRegistry.getInstrumentation().getTargetContext(); 24 | assertEquals("com.github.zhkl0228.androidvpn", appContext.getPackageName()); 25 | } 26 | } -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | 10 | 11 | 21 | 25 | 26 | 27 | 28 | 29 | 30 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/zhkl0228/androidvpn/AndroidVPN.java: -------------------------------------------------------------------------------- 1 | package com.github.zhkl0228.androidvpn; 2 | 3 | public interface AndroidVPN { 4 | 5 | String TAG = AndroidVPN.class.getSimpleName(); 6 | 7 | } 8 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/zhkl0228/androidvpn/HostPortDiscover.java: -------------------------------------------------------------------------------- 1 | package com.github.zhkl0228.androidvpn; 2 | 3 | import android.net.wifi.WifiManager; 4 | import android.util.Log; 5 | 6 | import java.io.ByteArrayInputStream; 7 | import java.io.DataInput; 8 | import java.io.DataInputStream; 9 | import java.net.DatagramPacket; 10 | import java.net.DatagramSocket; 11 | import java.net.InetAddress; 12 | import java.net.SocketTimeoutException; 13 | 14 | class HostPortDiscover implements Runnable { 15 | 16 | private static final int UDP_PORT = 20230; 17 | 18 | public interface Listener { 19 | void onDiscover(String host, int port); 20 | } 21 | 22 | private final Listener listener; 23 | private final WifiManager.MulticastLock lock; 24 | 25 | public HostPortDiscover(Listener listener, WifiManager.MulticastLock lock) { 26 | this.listener = listener; 27 | this.lock = lock; 28 | } 29 | 30 | @Override 31 | public void run() { 32 | byte[] buf = new byte[32]; 33 | try (DatagramSocket socket = new DatagramSocket(UDP_PORT)) { 34 | lock.acquire(); 35 | socket.setSoTimeout(3000); 36 | Log.d(AndroidVPN.TAG, "start discover socket=" + socket); 37 | while (!canStop) { 38 | try { 39 | DatagramPacket packet = new DatagramPacket(buf, buf.length); 40 | socket.receive(packet); 41 | byte[] data = packet.getData(); 42 | if (packet.getLength() != 7) { 43 | continue; 44 | } 45 | DataInput dataInput = new DataInputStream(new ByteArrayInputStream(data)); 46 | int magicSize = dataInput.readShort() & 0xffff; 47 | if (magicSize == 3) { 48 | byte[] vpn = new byte[magicSize]; 49 | dataInput.readFully(vpn); 50 | if ("vpn".equals(new String(vpn))) { 51 | InetAddress address = packet.getAddress(); 52 | int port = dataInput.readShort() & 0xffff; 53 | if (listener != null) { 54 | listener.onDiscover(address.getHostAddress(), port); 55 | } 56 | } 57 | } 58 | } catch (SocketTimeoutException ignored) { 59 | } 60 | } 61 | } catch (Exception e) { 62 | Log.w(AndroidVPN.TAG, "run exception", e); 63 | } finally { 64 | lock.release(); 65 | } 66 | } 67 | 68 | private boolean canStop; 69 | 70 | final void start() { 71 | if (canStop) { 72 | throw new IllegalStateException(); 73 | } 74 | Thread thread = new Thread(this, getClass().getSimpleName()); 75 | thread.setDaemon(true); 76 | thread.start(); 77 | } 78 | 79 | final void stop() { 80 | canStop = true; 81 | } 82 | 83 | } 84 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/zhkl0228/androidvpn/IPUtil.java: -------------------------------------------------------------------------------- 1 | package com.github.zhkl0228.androidvpn; 2 | 3 | /* 4 | This file is part of NetGuard. 5 | 6 | NetGuard is free software: you can redistribute it and/or modify 7 | it under the terms of the GNU General Public License as published by 8 | the Free Software Foundation, either version 3 of the License, or 9 | (at your option) any later version. 10 | 11 | NetGuard is distributed in the hope that it will be useful, 12 | but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | GNU General Public License for more details. 15 | 16 | You should have received a copy of the GNU General Public License 17 | along with NetGuard. If not, see . 18 | 19 | Copyright 2015-2018 by Marcel Bokhorst (M66B) 20 | */ 21 | 22 | import android.util.Log; 23 | 24 | import java.net.InetAddress; 25 | import java.net.UnknownHostException; 26 | import java.util.ArrayList; 27 | import java.util.List; 28 | 29 | public class IPUtil { 30 | private static final String TAG = "NetGuard.IPUtil"; 31 | 32 | public static List toCIDR(String start, String end) throws UnknownHostException { 33 | return toCIDR(InetAddress.getByName(start), InetAddress.getByName(end)); 34 | } 35 | 36 | public static List toCIDR(InetAddress start, InetAddress end) throws UnknownHostException { 37 | List listResult = new ArrayList<>(); 38 | 39 | Log.i(TAG, "toCIDR(" + start.getHostAddress() + "," + end.getHostAddress() + ")"); 40 | 41 | long from = inet2long(start); 42 | long to = inet2long(end); 43 | while (to >= from) { 44 | byte prefix = 32; 45 | while (prefix > 0) { 46 | long mask = prefix2mask(prefix - 1); 47 | if ((from & mask) != from) 48 | break; 49 | prefix--; 50 | } 51 | 52 | byte max = (byte) (32 - Math.floor(Math.log(to - from + 1) / Math.log(2))); 53 | if (prefix < max) 54 | prefix = max; 55 | 56 | listResult.add(new CIDR(long2inet(from), prefix)); 57 | 58 | from += Math.pow(2, (32 - prefix)); 59 | } 60 | 61 | for (CIDR cidr : listResult) 62 | Log.i(TAG, cidr.toString()); 63 | 64 | return listResult; 65 | } 66 | 67 | private static long prefix2mask(int bits) { 68 | return (0xFFFFFFFF00000000L >> bits) & 0xFFFFFFFFL; 69 | } 70 | 71 | private static long inet2long(InetAddress addr) { 72 | long result = 0; 73 | if (addr != null) 74 | for (byte b : addr.getAddress()) 75 | result = result << 8 | (b & 0xFF); 76 | return result; 77 | } 78 | 79 | private static InetAddress long2inet(long addr) { 80 | try { 81 | byte[] b = new byte[4]; 82 | for (int i = b.length - 1; i >= 0; i--) { 83 | b[i] = (byte) (addr & 0xFF); 84 | addr = addr >> 8; 85 | } 86 | return InetAddress.getByAddress(b); 87 | } catch (UnknownHostException ignore) { 88 | return null; 89 | } 90 | } 91 | 92 | public static InetAddress minus1(InetAddress addr) { 93 | return long2inet(inet2long(addr) - 1); 94 | } 95 | 96 | public static InetAddress plus1(InetAddress addr) { 97 | return long2inet(inet2long(addr) + 1); 98 | } 99 | 100 | public static class CIDR implements Comparable { 101 | public InetAddress address; 102 | public int prefix; 103 | 104 | public CIDR(InetAddress address, int prefix) { 105 | this.address = address; 106 | this.prefix = prefix; 107 | } 108 | 109 | public CIDR(String ip, int prefix) { 110 | try { 111 | this.address = InetAddress.getByName(ip); 112 | this.prefix = prefix; 113 | } catch (UnknownHostException ex) { 114 | Log.e(TAG, ex.toString() + "\n" + Log.getStackTraceString(ex)); 115 | } 116 | } 117 | 118 | public InetAddress getStart() { 119 | return long2inet(inet2long(this.address) & prefix2mask(this.prefix)); 120 | } 121 | 122 | public InetAddress getEnd() { 123 | return long2inet((inet2long(this.address) & prefix2mask(this.prefix)) + (1L << (32 - this.prefix)) - 1); 124 | } 125 | 126 | @Override 127 | public String toString() { 128 | return address.getHostAddress() + "/" + prefix + "=" + getStart().getHostAddress() + "..." + getEnd().getHostAddress(); 129 | } 130 | 131 | @Override 132 | public int compareTo(CIDR other) { 133 | Long lcidr = IPUtil.inet2long(this.address); 134 | Long lother = IPUtil.inet2long(other.address); 135 | return lcidr.compareTo(lother); 136 | } 137 | } 138 | } 139 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/zhkl0228/androidvpn/InspectorVpnService.java: -------------------------------------------------------------------------------- 1 | package com.github.zhkl0228.androidvpn; 2 | 3 | import android.content.Context; 4 | import android.content.Intent; 5 | import android.content.pm.PackageInfo; 6 | import android.content.pm.PackageManager; 7 | import android.content.res.Configuration; 8 | import android.net.ConnectivityManager; 9 | import android.net.NetworkInfo; 10 | import android.net.VpnService; 11 | import android.os.Bundle; 12 | import android.os.FileUtils; 13 | import android.os.Handler; 14 | import android.os.HandlerThread; 15 | import android.os.Looper; 16 | import android.os.Message; 17 | import android.os.ParcelFileDescriptor; 18 | import android.os.Process; 19 | import android.system.OsConstants; 20 | import android.text.TextUtils; 21 | import android.util.Base64; 22 | import android.util.Log; 23 | 24 | import androidx.annotation.NonNull; 25 | 26 | import org.json.JSONException; 27 | import org.json.JSONObject; 28 | 29 | import java.io.ByteArrayInputStream; 30 | import java.io.ByteArrayOutputStream; 31 | import java.io.DataInput; 32 | import java.io.DataInputStream; 33 | import java.io.DataOutput; 34 | import java.io.DataOutputStream; 35 | import java.io.EOFException; 36 | import java.io.File; 37 | import java.io.FileInputStream; 38 | import java.io.FileOutputStream; 39 | import java.io.IOException; 40 | import java.io.InputStream; 41 | import java.io.OutputStream; 42 | import java.net.DatagramPacket; 43 | import java.net.DatagramSocket; 44 | import java.net.Inet4Address; 45 | import java.net.InetAddress; 46 | import java.net.InetSocketAddress; 47 | import java.net.InterfaceAddress; 48 | import java.net.NetworkInterface; 49 | import java.net.Socket; 50 | import java.net.SocketAddress; 51 | import java.net.SocketException; 52 | import java.net.SocketTimeoutException; 53 | import java.net.UnknownHostException; 54 | import java.util.ArrayList; 55 | import java.util.Arrays; 56 | import java.util.Collections; 57 | import java.util.Enumeration; 58 | import java.util.List; 59 | import java.util.Locale; 60 | import java.util.Objects; 61 | import java.util.Set; 62 | 63 | public class InspectorVpnService extends VpnService { 64 | 65 | private static final String TAG = AndroidVPN.TAG; 66 | 67 | private static final int MSG_SERVICE_INTENT = 0; 68 | 69 | public static final String EXTRA_COMMAND = "Command"; 70 | private static final String EXTRA_REASON = "Reason"; 71 | 72 | private static final int MTU = 10000; 73 | 74 | public enum Command {start, stop} 75 | 76 | private volatile Looper commandLooper; 77 | private volatile CommandHandler commandHandler; 78 | private Thread tunnelThread; 79 | private ParcelFileDescriptor vpn; 80 | 81 | private final class CommandHandler extends Handler { 82 | CommandHandler(Looper looper) { 83 | super(looper); 84 | } 85 | 86 | public void queue(Intent intent) { 87 | Message msg = commandHandler.obtainMessage(); 88 | msg.obj = intent; 89 | msg.what = MSG_SERVICE_INTENT; 90 | commandHandler.sendMessage(msg); 91 | } 92 | 93 | @Override 94 | public void handleMessage(@NonNull Message msg) { 95 | try { 96 | if (msg.what == MSG_SERVICE_INTENT) { 97 | handleIntent((Intent) msg.obj); 98 | } else { 99 | Log.e(TAG, "Unknown command message=" + msg.what); 100 | } 101 | } catch (Throwable ex) { 102 | Log.e(TAG, ex + "\n" + Log.getStackTraceString(ex)); 103 | } 104 | } 105 | 106 | private void handleIntent(Intent intent) { 107 | Command cmd = (Command) intent.getSerializableExtra(EXTRA_COMMAND); 108 | String reason = intent.getStringExtra(EXTRA_REASON); 109 | Bundle bundle = intent.getBundleExtra(Bundle.class.getCanonicalName()); 110 | String vpnHost = bundle == null ? null : bundle.getString(VPN_HOST_KEY); 111 | int vpnPort = bundle == null ? 0 : bundle.getInt(VPN_PORT_KEY); 112 | Log.i(TAG, "Executing intent=" + intent + " command=" + cmd + " reason=" + reason + 113 | " vpn=" + (vpn != null) + " user=" + (Process.myUid() / 100000) + 114 | ", vpnHost=" + vpnHost + ", vpnPort=" + vpnPort); 115 | 116 | try { 117 | if (cmd == null) { 118 | throw new IllegalStateException(); 119 | } 120 | switch (cmd) { 121 | case start: 122 | if (vpnHost != null && vpnPort != 0) { 123 | start(vpnHost, vpnPort); 124 | } 125 | break; 126 | 127 | case stop: 128 | stop(); 129 | break; 130 | 131 | default: 132 | Log.e(TAG, "Unknown command=" + cmd); 133 | } 134 | 135 | } catch (Throwable ex) { 136 | Log.e(TAG, ex + "\n" + Log.getStackTraceString(ex)); 137 | } 138 | } 139 | 140 | private void start(String vpnHost, int vpnPort) { 141 | if (vpn == null) { 142 | Builder builder = getBuilder(vpnHost); 143 | vpn = startVPN(builder); 144 | if (vpn == null) { 145 | throw new IllegalStateException("start vpn failed."); 146 | } 147 | startNative(vpn, vpnHost, vpnPort); 148 | } 149 | } 150 | 151 | private void stop() { 152 | if (vpn != null) { 153 | stopNative(); 154 | stopVPN(vpn); 155 | vpn = null; 156 | } 157 | } 158 | } 159 | 160 | private void stopNative() { 161 | Log.i(TAG, "Stop native"); 162 | 163 | if (vpnServerThread != null) { 164 | Thread thread = vpnServerThread; 165 | thread.interrupt(); 166 | vpnServerThread = null; 167 | Log.i(TAG, "Stopped vpn server thread"); 168 | } 169 | if (tunnelThread != null) { 170 | Log.i(TAG, "Stopping tunnel thread: " + tunnelThread); 171 | 172 | Thread thread = tunnelThread; 173 | if (thread != null) { 174 | try { 175 | thread.join(); 176 | } catch (InterruptedException ignored) { 177 | } 178 | } 179 | tunnelThread = null; 180 | 181 | Log.i(TAG, "Stopped tunnel thread"); 182 | } 183 | } 184 | 185 | private void stopVPN(ParcelFileDescriptor pfd) { 186 | try { 187 | if (pfd != null) { 188 | pfd.close(); 189 | } 190 | } catch (IOException ex) { 191 | Log.e(TAG, ex + "\n" + Log.getStackTraceString(ex)); 192 | } 193 | } 194 | 195 | private static final byte VPN_MAGIC = 0xe; 196 | 197 | private class StreamForward implements Runnable { 198 | private final DataInput dataInput; 199 | private final OutputStream outputStream; 200 | private final int mtu; 201 | public StreamForward(DataInput dataInput, OutputStream outputStream, int mtu) { 202 | this.dataInput = dataInput; 203 | this.outputStream = outputStream; 204 | this.mtu = mtu; 205 | } 206 | @Override 207 | public void run() { 208 | try { 209 | byte[] packet = new byte[mtu]; 210 | while (vpnServerThread != null) { 211 | int length = dataInput.readUnsignedShort(); 212 | if (length > mtu) { 213 | throw new IOException("length=" + length + ", mtu=" + mtu); 214 | } 215 | dataInput.readFully(packet, 0, length); 216 | if (length == -1) { 217 | throw new EOFException(); 218 | } 219 | if (length > 0) { 220 | for (int i = 0; i < length; i++) { 221 | packet[i] ^= VPN_MAGIC; 222 | } 223 | outputStream.write(packet, 0, length); 224 | outputStream.flush(); 225 | } 226 | } 227 | } catch (IOException e) { 228 | Log.w(TAG, "stream forward", e); 229 | } 230 | 231 | if (vpn != null) { 232 | try { vpn.close(); } catch(Exception ignored) {} 233 | vpn = null; 234 | } 235 | } 236 | } 237 | 238 | private Thread vpnServerThread; 239 | 240 | private class ApplicationDiscoverServer implements Runnable { 241 | private final SocketAddress socketAddress; 242 | public ApplicationDiscoverServer(SocketAddress socketAddress) { 243 | this.socketAddress = socketAddress; 244 | } 245 | @Override 246 | public void run() { 247 | byte[] buffer = new byte[1024]; 248 | ConnectivityManager cm = (ConnectivityManager) getSystemService(Context.CONNECTIVITY_SERVICE); 249 | PackageManager pm = getPackageManager(); 250 | try (DatagramSocket udp = new DatagramSocket(socketAddress)) { 251 | protect(udp); 252 | udp.setSoTimeout(2000); 253 | Thread thread; 254 | while ((thread = vpnServerThread) != null && thread.isAlive()) { 255 | try { 256 | DatagramPacket packet = new DatagramPacket(buffer, buffer.length); 257 | udp.receive(packet); 258 | DataInput dataInput = new DataInputStream(new ByteArrayInputStream(buffer)); 259 | int type = dataInput.readUnsignedByte(); 260 | if (type != 0x1) { 261 | throw new IllegalStateException("type=" + type); 262 | } 263 | int protocol = dataInput.readUnsignedByte(); 264 | String saddr = dataInput.readUTF(); 265 | int sport = dataInput.readUnsignedShort(); 266 | String daddr = dataInput.readUTF(); 267 | int dport = dataInput.readUnsignedShort(); 268 | if (protocol != OsConstants.IPPROTO_TCP && protocol != OsConstants.IPPROTO_UDP) { 269 | continue; 270 | } 271 | InetSocketAddress local = new InetSocketAddress(saddr, sport); 272 | InetSocketAddress remote = new InetSocketAddress(daddr, dport); 273 | int uid = cm.getConnectionOwnerUid(protocol, local, remote); 274 | String[] packages = pm.getPackagesForUid(uid); 275 | int hash = Objects.hash(protocol, saddr, sport, daddr, dport); 276 | Log.d(TAG, "allowed protocol=" + protocol + ", uid=" + uid + ", packages=" + Arrays.toString(packages) + " " + local + " => " + remote); 277 | if (packages != null) { 278 | List list = new ArrayList<>(packages.length); 279 | for (String packageName : packages) { 280 | PackageInfo packageInfo = pm.getPackageInfo(packageName, PackageManager.GET_META_DATA); 281 | CharSequence label = pm.getApplicationLabel(packageInfo.applicationInfo); 282 | list.add(new Package(packageName, label, packageInfo.getLongVersionCode())); 283 | } 284 | Log.d(TAG, "allowed list=" + list); 285 | byte[] data = responseForPackages(hash, list); 286 | DatagramPacket forSend = new DatagramPacket(data, data.length, packet.getSocketAddress()); 287 | udp.send(forSend); 288 | } 289 | } catch(SocketTimeoutException ignored) {} 290 | } 291 | } catch (IOException e) { 292 | Log.d(TAG, "run udp server failed.", e); 293 | } catch (Exception e) { 294 | Log.w(TAG, "run udp server failed.", e); 295 | } finally { 296 | Log.d(TAG, "exit udp server."); 297 | } 298 | } 299 | 300 | @NonNull 301 | private byte[] responseForPackages(int hash, List packages) { 302 | if (packages == null) { 303 | throw new IllegalArgumentException(); 304 | } 305 | try (ByteArrayOutputStream baos = new ByteArrayOutputStream()) { 306 | DataOutput dataOutput = new DataOutputStream(baos); 307 | dataOutput.writeByte(0x2); 308 | dataOutput.writeInt(hash); 309 | dataOutput.writeByte(packages.size()); 310 | for (Package pkg : packages) { 311 | pkg.output(dataOutput); 312 | } 313 | return baos.toByteArray(); 314 | } catch (IOException e) { 315 | throw new RuntimeException(e); 316 | } 317 | } 318 | } 319 | 320 | private void startNative(final ParcelFileDescriptor vpn, String vpnHost, int vpnPort) { 321 | Log.d(TAG, "startNative vpnHost=" + vpnHost + ", vpnPort=" + vpnPort + ", vpnServerThread=" + vpnServerThread); 322 | if (vpnServerThread != null) { 323 | return; 324 | } 325 | vpnServerThread = new Thread(() -> { 326 | try (Socket socket = new Socket()) { 327 | protect(socket); 328 | socket.connect(new InetSocketAddress(vpnHost, vpnPort), 15000); 329 | Log.d(TAG, "Connected to vpn server: " + socket); 330 | { 331 | Thread udpServerThread = new Thread(new ApplicationDiscoverServer(socket.getLocalSocketAddress())); 332 | udpServerThread.setDaemon(true); 333 | udpServerThread.start(); 334 | } 335 | 336 | InputStream inputStream = socket.getInputStream(); 337 | OutputStream outputStream = socket.getOutputStream(); 338 | int mtu = MTU; 339 | try (InputStream vpnInput = new FileInputStream(vpn.getFileDescriptor()); 340 | OutputStream vpnOutput = new FileOutputStream(vpn.getFileDescriptor())) { 341 | DataOutput output = new DataOutputStream(outputStream); 342 | int osType = 0x0; 343 | File dir = getExternalFilesDir(null); 344 | // /sdcard/Android/data/com.github.zhkl0228.androidvpn/files/vpn_config.txt 345 | File configFile = dir == null ? null : new File(dir, "vpn_config.txt"); 346 | Log.d(TAG, "vpn config path: " + configFile); 347 | byte[] configData = null; 348 | if (configFile != null && configFile.canRead()) { 349 | try (FileInputStream fileInputStream = new FileInputStream(configFile); 350 | ByteArrayOutputStream baos = new ByteArrayOutputStream()) { 351 | FileUtils.copy(fileInputStream, baos); 352 | configData = baos.toByteArray(); 353 | } catch (Exception e) { 354 | Log.w(TAG, "read vpn config failed: " + configFile, e); 355 | } 356 | } 357 | if (configData != null) { 358 | osType |= 0x80; 359 | } 360 | output.writeByte(osType); 361 | if (configData != null) { 362 | Locale locale = Locale.getDefault(); 363 | try { 364 | JSONObject obj = new JSONObject(); 365 | obj.put("locale", locale.toString()); 366 | obj.put("language", locale.getLanguage()); 367 | obj.put("country", locale.getCountry()); 368 | obj.put("config", Base64.encodeToString(configData, Base64.NO_WRAP)); 369 | String json = obj.toString(); 370 | Log.d(TAG, "vpn config path: " + configFile + ", json=" + json); 371 | output.writeUTF(json); 372 | } catch (JSONException e) { 373 | Log.w(TAG, "write vpn config failed: " + configFile, e); 374 | } 375 | } 376 | Thread thread = new Thread(new StreamForward(new DataInputStream(inputStream), vpnOutput, mtu)); 377 | thread.start(); 378 | byte[] packet = new byte[mtu]; 379 | while (true) { 380 | int length = vpnInput.read(packet); 381 | if (length == -1) { 382 | throw new EOFException(); 383 | } 384 | if (length > 0) { 385 | if (length > mtu) { 386 | throw new IOException("Invalid mtu=" + mtu + ", length=" + length); 387 | } 388 | output.writeShort(length); 389 | for (int i = 0; i < length; i++) { 390 | packet[i] ^= VPN_MAGIC; 391 | } 392 | output.write(packet, 0, length); 393 | outputStream.flush(); 394 | } 395 | } 396 | } 397 | } catch (IOException e) { 398 | Log.d(TAG, "loop vpn server failed", e); 399 | } 400 | 401 | ParcelFileDescriptor fd = this.vpn; 402 | try { 403 | if (fd != null) { 404 | fd.close(); 405 | } 406 | this.vpn = null; 407 | } catch (IOException ignored) { 408 | } 409 | vpnServerThread = null; 410 | }, "Connect vpn server"); 411 | vpnServerThread.setPriority(Thread.MAX_PRIORITY); 412 | vpnServerThread.start(); 413 | } 414 | 415 | private ParcelFileDescriptor startVPN(Builder builder) throws SecurityException { 416 | try { 417 | return builder.establish(); 418 | } catch (SecurityException ex) { 419 | throw ex; 420 | } catch (Throwable ex) { 421 | Log.e(TAG, ex + "\n" + Log.getStackTraceString(ex)); 422 | return null; 423 | } 424 | } 425 | 426 | private Builder getBuilder(String vpnHost) { 427 | // Build VPN service 428 | Builder builder = new Builder(); 429 | builder.setSession("Inspector"); 430 | 431 | // VPN address 432 | String vpn4 = "10.1.10.1"; 433 | Log.i(TAG, "vpn4=" + vpn4); 434 | builder.addAddress(vpn4, 32); 435 | 436 | // String vpn6 = "fd00:1:fd00:1:fd00:1:fd00:1"; 437 | // Log.i(TAG, "vpn6=" + vpn6); 438 | // builder.addAddress(vpn6, 128); 439 | 440 | // Exclude IP ranges 441 | List listExclude = new ArrayList<>(); 442 | 443 | try { 444 | InetAddress address = InetAddress.getByName(vpnHost); 445 | IPUtil.CIDR local = new IPUtil.CIDR(address, address.getAddress().length * 8); 446 | Log.i(TAG, "Excluding " + vpnHost + " " + local); 447 | listExclude.add(local); 448 | } catch (IOException ex) { 449 | Log.e(TAG, ex + "\n" + Log.getStackTraceString(ex)); 450 | } 451 | 452 | // DNS address 453 | for (InetAddress dns : getDns()) { 454 | if (dns instanceof Inet4Address) { 455 | Log.i(TAG, "dns=" + dns + ", address=" + dns.getHostAddress()); 456 | builder.addDnsServer(dns); 457 | listExclude.add(new IPUtil.CIDR(dns.getHostAddress(), 24)); 458 | } 459 | } 460 | 461 | listExclude.add(new IPUtil.CIDR("127.0.0.0", 8)); // localhost 462 | 463 | // USB tethering 192.168.42.x 464 | // Wi-Fi tethering 192.168.43.x 465 | listExclude.add(new IPUtil.CIDR("192.168.42.0", 23)); 466 | // Wi-Fi direct 192.168.49.x 467 | listExclude.add(new IPUtil.CIDR("192.168.49.0", 24)); 468 | 469 | try { 470 | Enumeration nis = NetworkInterface.getNetworkInterfaces(); 471 | while (nis.hasMoreElements()) { 472 | NetworkInterface ni = nis.nextElement(); 473 | if (ni != null && ni.isUp() && !ni.isLoopback() && 474 | ni.getName() != null && !ni.getName().startsWith("tun")) 475 | for (InterfaceAddress ia : ni.getInterfaceAddresses()) 476 | if (ia.getAddress() instanceof Inet4Address) { 477 | IPUtil.CIDR local = new IPUtil.CIDR(ia.getAddress(), ia.getNetworkPrefixLength()); 478 | Log.i(TAG, "Excluding " + ni.getName() + " " + local); 479 | listExclude.add(local); 480 | } 481 | } 482 | } catch (SocketException ex) { 483 | Log.e(TAG, ex + "\n" + Log.getStackTraceString(ex)); 484 | } 485 | 486 | // https://en.wikipedia.org/wiki/Mobile_country_code 487 | Configuration config = getResources().getConfiguration(); 488 | 489 | // T-Mobile Wi-Fi calling 490 | if (config.mcc == 310 && (config.mnc == 160 || 491 | config.mnc == 200 || 492 | config.mnc == 210 || 493 | config.mnc == 220 || 494 | config.mnc == 230 || 495 | config.mnc == 240 || 496 | config.mnc == 250 || 497 | config.mnc == 260 || 498 | config.mnc == 270 || 499 | config.mnc == 310 || 500 | config.mnc == 490 || 501 | config.mnc == 660 || 502 | config.mnc == 800)) { 503 | listExclude.add(new IPUtil.CIDR("66.94.2.0", 24)); 504 | listExclude.add(new IPUtil.CIDR("66.94.6.0", 23)); 505 | listExclude.add(new IPUtil.CIDR("66.94.8.0", 22)); 506 | listExclude.add(new IPUtil.CIDR("208.54.0.0", 16)); 507 | } 508 | 509 | // Verizon wireless calling 510 | if ((config.mcc == 310 && 511 | (config.mnc == 4 || 512 | config.mnc == 5 || 513 | config.mnc == 6 || 514 | config.mnc == 10 || 515 | config.mnc == 12 || 516 | config.mnc == 13 || 517 | config.mnc == 350 || 518 | config.mnc == 590 || 519 | config.mnc == 820 || 520 | config.mnc == 890 || 521 | config.mnc == 910)) || 522 | (config.mcc == 311 && (config.mnc == 12 || 523 | config.mnc == 110 || 524 | (config.mnc >= 270 && config.mnc <= 289) || 525 | config.mnc == 390 || 526 | (config.mnc >= 480 && config.mnc <= 489) || 527 | config.mnc == 590)) || 528 | (config.mcc == 312 && (config.mnc == 770))) { 529 | listExclude.add(new IPUtil.CIDR("66.174.0.0", 16)); // 66.174.0.0 - 66.174.255.255 530 | listExclude.add(new IPUtil.CIDR("66.82.0.0", 15)); // 69.82.0.0 - 69.83.255.255 531 | listExclude.add(new IPUtil.CIDR("69.96.0.0", 13)); // 69.96.0.0 - 69.103.255.255 532 | listExclude.add(new IPUtil.CIDR("70.192.0.0", 11)); // 70.192.0.0 - 70.223.255.255 533 | listExclude.add(new IPUtil.CIDR("97.128.0.0", 9)); // 97.128.0.0 - 97.255.255.255 534 | listExclude.add(new IPUtil.CIDR("174.192.0.0", 9)); // 174.192.0.0 - 174.255.255.255 535 | listExclude.add(new IPUtil.CIDR("72.96.0.0", 9)); // 72.96.0.0 - 72.127.255.255 536 | listExclude.add(new IPUtil.CIDR("75.192.0.0", 9)); // 75.192.0.0 - 75.255.255.255 537 | listExclude.add(new IPUtil.CIDR("97.0.0.0", 10)); // 97.0.0.0 - 97.63.255.255 538 | } 539 | 540 | // Broadcast 541 | listExclude.add(new IPUtil.CIDR("224.0.0.0", 3)); 542 | 543 | Collections.sort(listExclude); 544 | 545 | try { 546 | InetAddress start = InetAddress.getByName("0.0.0.0"); 547 | for (IPUtil.CIDR exclude : listExclude) { 548 | Log.i(TAG, "Exclude " + exclude.getStart().getHostAddress() + "..." + exclude.getEnd().getHostAddress()); 549 | for (IPUtil.CIDR include : IPUtil.toCIDR(start, IPUtil.minus1(exclude.getStart()))) { 550 | try { 551 | builder.addRoute(include.address, include.prefix); 552 | } catch (Throwable ex) { 553 | Log.e(TAG, ex + "\n" + Log.getStackTraceString(ex)); 554 | } 555 | } 556 | start = IPUtil.plus1(exclude.getEnd()); 557 | } 558 | for (IPUtil.CIDR include : IPUtil.toCIDR("224.0.0.0", "255.255.255.255")) 559 | try { 560 | builder.addRoute(include.address, include.prefix); 561 | } catch (Throwable ex) { 562 | Log.e(TAG, ex + "\n" + Log.getStackTraceString(ex)); 563 | } 564 | } catch (UnknownHostException ex) { 565 | Log.e(TAG, ex + "\n" + Log.getStackTraceString(ex)); 566 | } 567 | 568 | // builder.addRoute("0:0:0:0:0:0:0:0", 0); 569 | builder.setMtu(MTU); 570 | builder.setBlocking(true); 571 | 572 | return builder; 573 | } 574 | 575 | @SuppressWarnings("deprecation") 576 | private class Builder extends VpnService.Builder { 577 | private final NetworkInfo networkInfo; 578 | private int mtu; 579 | private final List listAddress = new ArrayList<>(); 580 | private final List listRoute = new ArrayList<>(); 581 | private final List listDns = new ArrayList<>(); 582 | 583 | private Builder() { 584 | super(); 585 | ConnectivityManager cm = (ConnectivityManager) getSystemService(Context.CONNECTIVITY_SERVICE); 586 | networkInfo = cm == null ? null : cm.getActiveNetworkInfo(); 587 | } 588 | 589 | @NonNull 590 | @Override 591 | public VpnService.Builder setMtu(int mtu) { 592 | this.mtu = mtu; 593 | super.setMtu(mtu); 594 | return this; 595 | } 596 | 597 | @NonNull 598 | @Override 599 | public Builder addAddress(@NonNull String address, int prefixLength) { 600 | listAddress.add(address + "/" + prefixLength); 601 | super.addAddress(address, prefixLength); 602 | return this; 603 | } 604 | 605 | @NonNull 606 | @Override 607 | public Builder addRoute(@NonNull String address, int prefixLength) { 608 | listRoute.add(address + "/" + prefixLength); 609 | super.addRoute(address, prefixLength); 610 | return this; 611 | } 612 | 613 | @NonNull 614 | @Override 615 | public Builder addDnsServer(@NonNull InetAddress address) { 616 | listDns.add(address); 617 | super.addDnsServer(address); 618 | return this; 619 | } 620 | 621 | @Override 622 | public boolean equals(Object obj) { 623 | if (!(obj instanceof Builder)) { 624 | return false; 625 | } 626 | 627 | Builder other = (Builder) obj; 628 | 629 | if (this.networkInfo == null || other.networkInfo == null || 630 | this.networkInfo.getType() != other.networkInfo.getType()) 631 | return false; 632 | 633 | if (this.mtu != other.mtu) 634 | return false; 635 | 636 | if (this.listAddress.size() != other.listAddress.size()) 637 | return false; 638 | 639 | if (this.listRoute.size() != other.listRoute.size()) 640 | return false; 641 | 642 | if (this.listDns.size() != other.listDns.size()) 643 | return false; 644 | 645 | for (String address : this.listAddress) 646 | if (!other.listAddress.contains(address)) 647 | return false; 648 | 649 | for (String route : this.listRoute) 650 | if (!other.listRoute.contains(route)) 651 | return false; 652 | 653 | for (InetAddress dns : this.listDns) 654 | if (!other.listDns.contains(dns)) 655 | return false; 656 | 657 | return true; 658 | } 659 | } 660 | 661 | private static List getDns() { 662 | List listDns = new ArrayList<>(); 663 | List sysDns = Arrays.asList("8.8.8.8", "8.8.4.4"); 664 | 665 | // Get custom DNS servers 666 | Log.i(TAG, "DNS system=" + TextUtils.join(",", sysDns)); 667 | 668 | // Use system DNS servers only when no two custom DNS servers specified 669 | for (String def_dns : sysDns) { 670 | try { 671 | InetAddress ddns = InetAddress.getByName(def_dns); 672 | if (!listDns.contains(ddns) && 673 | !(ddns.isLoopbackAddress() || ddns.isAnyLocalAddress()) && 674 | ddns instanceof Inet4Address) 675 | listDns.add(ddns); 676 | } catch (Throwable ex) { 677 | Log.e(TAG, ex + "\n" + Log.getStackTraceString(ex)); 678 | } 679 | } 680 | 681 | return listDns; 682 | } 683 | 684 | public static void logExtras(Intent intent) { 685 | if (intent != null) 686 | logBundle(intent.getExtras()); 687 | } 688 | 689 | public static void logBundle(Bundle data) { 690 | if (data != null) { 691 | Set keys = data.keySet(); 692 | StringBuilder stringBuilder = new StringBuilder(); 693 | for (String key : keys) { 694 | Object value = data.get(key); 695 | stringBuilder.append(key) 696 | .append("=") 697 | .append(value) 698 | .append(value == null ? "" : " (" + value.getClass().getSimpleName() + ")") 699 | .append("\r\n"); 700 | } 701 | Log.d(TAG, stringBuilder.toString()); 702 | } 703 | } 704 | 705 | @Override 706 | public void onCreate() { 707 | Log.d(TAG, "onCreate obj=" + this); 708 | 709 | super.onCreate(); 710 | 711 | HandlerThread commandThread = new HandlerThread("NetGuard command", Process.THREAD_PRIORITY_FOREGROUND); 712 | commandThread.start(); 713 | 714 | commandLooper = commandThread.getLooper(); 715 | commandHandler = new CommandHandler(commandLooper); 716 | } 717 | 718 | public static final String VPN_HOST_KEY = "vpnHost"; 719 | public static final String VPN_PORT_KEY = "vpnPort"; 720 | 721 | @Override 722 | public int onStartCommand(Intent intent, int flags, int startId) { 723 | Log.i(TAG, "Received " + intent); 724 | logExtras(intent); 725 | 726 | // Handle service restart 727 | if (intent == null) { 728 | Log.i(TAG, "Restart"); 729 | 730 | // Recreate intent 731 | intent = new Intent(this, InspectorVpnService.class); 732 | intent.putExtra(EXTRA_COMMAND, Command.start); 733 | } 734 | 735 | if (vpn == null) { 736 | intent.putExtra(EXTRA_COMMAND, Command.start); 737 | } else { 738 | intent.putExtra(EXTRA_COMMAND, Command.stop); 739 | } 740 | String reason = intent.getStringExtra(EXTRA_REASON); 741 | Log.i(TAG, "Start intent=" + intent + " reason=" + reason + " vpn=" + (vpn != null)); 742 | 743 | commandHandler.queue(intent); 744 | return START_STICKY; 745 | } 746 | 747 | @Override 748 | public void onDestroy() { 749 | Log.i(TAG, "Destroy"); 750 | 751 | commandLooper.quit(); 752 | 753 | try { 754 | if (vpn != null) { 755 | stopNative(); 756 | stopVPN(vpn); 757 | vpn = null; 758 | } 759 | } catch (Throwable ex) { 760 | Log.e(TAG, ex + "\n" + Log.getStackTraceString(ex)); 761 | } 762 | 763 | super.onDestroy(); 764 | } 765 | 766 | } 767 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/zhkl0228/androidvpn/Package.java: -------------------------------------------------------------------------------- 1 | package com.github.zhkl0228.androidvpn; 2 | 3 | import androidx.annotation.NonNull; 4 | 5 | import java.io.DataOutput; 6 | import java.io.IOException; 7 | 8 | class Package { 9 | 10 | final String packageName; 11 | final CharSequence label; 12 | final long versionCode; 13 | 14 | public Package(String packageName, CharSequence label, long versionCode) { 15 | this.packageName = packageName; 16 | this.label = label; 17 | this.versionCode = versionCode; 18 | } 19 | 20 | @NonNull 21 | @Override 22 | public String toString() { 23 | return label + "(" + packageName + ")"; 24 | } 25 | 26 | void output(DataOutput dataOutput) throws IOException { 27 | dataOutput.writeUTF(packageName); 28 | dataOutput.writeUTF(String.valueOf(label)); 29 | dataOutput.writeLong(versionCode); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/zhkl0228/androidvpn/StartVpnActivity.java: -------------------------------------------------------------------------------- 1 | package com.github.zhkl0228.androidvpn; 2 | 3 | import android.app.Activity; 4 | import android.content.Context; 5 | import android.content.Intent; 6 | import android.content.SharedPreferences; 7 | import android.net.VpnService; 8 | import android.net.wifi.WifiManager; 9 | import android.os.Bundle; 10 | import android.text.method.LinkMovementMethod; 11 | import android.util.Log; 12 | import android.view.KeyEvent; 13 | import android.view.inputmethod.EditorInfo; 14 | import android.widget.Button; 15 | import android.widget.EditText; 16 | import android.widget.TextView; 17 | 18 | import androidx.appcompat.app.AppCompatActivity; 19 | 20 | import com.github.zhkl0228.androidvpn.databinding.ActivityStartVpnBinding; 21 | 22 | public class StartVpnActivity extends AppCompatActivity implements HostPortDiscover.Listener { 23 | 24 | private ActivityStartVpnBinding binding; 25 | private HostPortDiscover hostPortDiscover; 26 | private SharedPreferences preference; 27 | 28 | @Override 29 | public void onDiscover(String host, int port) { 30 | Log.d(AndroidVPN.TAG, "onDiscover host=" + host + ", port=" + port); 31 | 32 | runOnUiThread(() -> { 33 | String ps = String.valueOf(port); 34 | EditText hostEditText = binding.host; 35 | EditText portEditText = binding.port; 36 | if (ps.equals(portEditText.getText().toString())) { 37 | return; 38 | } 39 | Button startVpnButton = binding.startVpn; 40 | hostEditText.setText(host); 41 | portEditText.setText(ps); 42 | startVpnButton.setEnabled(true); 43 | }); 44 | } 45 | 46 | public static final int VPN_REQUEST_CODE = 0x7b; 47 | 48 | @SuppressWarnings("deprecation") 49 | @Override 50 | public void onCreate(Bundle savedInstanceState) { 51 | super.onCreate(savedInstanceState); 52 | preference = getPreferences(Context.MODE_PRIVATE); 53 | 54 | binding = ActivityStartVpnBinding.inflate(getLayoutInflater()); 55 | setContentView(binding.getRoot()); 56 | binding.ca.setMovementMethod(LinkMovementMethod.getInstance()); 57 | 58 | final Button startVpnButton = binding.startVpn; 59 | 60 | startVpnButton.setOnClickListener(v -> { 61 | Intent vpnIntent = VpnService.prepare(this); 62 | if (vpnIntent != null) { 63 | startActivityForResult(vpnIntent, VPN_REQUEST_CODE); 64 | } else { 65 | onActivityResult(VPN_REQUEST_CODE, Activity.RESULT_OK, null); 66 | } 67 | }); 68 | 69 | EditText hostEditText = binding.host; 70 | EditText portEditText = binding.port; 71 | hostEditText.setEnabled(true); 72 | portEditText.setEnabled(true); 73 | TextView.OnEditorActionListener listener = (v, actionId, event) -> { 74 | if (actionId == EditorInfo.IME_ACTION_SEARCH || 75 | actionId == EditorInfo.IME_ACTION_DONE || 76 | event != null && 77 | event.getAction() == KeyEvent.ACTION_DOWN && 78 | event.getKeyCode() == KeyEvent.KEYCODE_ENTER) { 79 | if (event == null || !event.isShiftPressed()) { 80 | if (!hostEditText.getText().toString().isEmpty() && 81 | !portEditText.getText().toString().isEmpty()) { 82 | startVpnButton.setEnabled(true); 83 | } 84 | return true; 85 | } 86 | } 87 | return false; 88 | }; 89 | hostEditText.setOnEditorActionListener(listener); 90 | portEditText.setOnEditorActionListener(listener); 91 | 92 | String host = preference.getString("host", null); 93 | int port = preference.getInt("port", 0); 94 | if (host != null && port != 0) { 95 | hostEditText.setText(host); 96 | portEditText.setText(String.valueOf(port)); 97 | startVpnButton.setEnabled(true); 98 | } 99 | 100 | WifiManager wifiManager = (WifiManager) getSystemService(Context.WIFI_SERVICE); 101 | WifiManager.MulticastLock lock = wifiManager.createMulticastLock(AndroidVPN.TAG); 102 | hostPortDiscover = new HostPortDiscover(this, lock); 103 | hostPortDiscover.start(); 104 | } 105 | 106 | @Override 107 | public void onActivityResult(int requestCode, int resultCode, Intent data) { 108 | super.onActivityResult(requestCode, resultCode, data); 109 | 110 | if (requestCode == VPN_REQUEST_CODE && resultCode == Activity.RESULT_OK) { 111 | EditText hostEditText = binding.host; 112 | EditText portEditText = binding.port; 113 | String host = hostEditText.getText().toString().trim(); 114 | int port = Integer.parseInt(portEditText.getText().toString().trim()); 115 | Log.d(AndroidVPN.TAG, "onActivityResult host=" + host + ", port=" + port); 116 | preference.edit().putString("host", host).putInt("port", port).apply(); 117 | 118 | Intent intent = new Intent(this, InspectorVpnService.class); 119 | Bundle bundle = new Bundle(); 120 | bundle.putString(InspectorVpnService.VPN_HOST_KEY, host); 121 | bundle.putInt(InspectorVpnService.VPN_PORT_KEY, port); 122 | intent.putExtra(Bundle.class.getCanonicalName(), bundle); 123 | startService(intent); 124 | } 125 | } 126 | 127 | @Override 128 | protected void onDestroy() { 129 | super.onDestroy(); 130 | hostPortDiscover.stop(); 131 | } 132 | 133 | } -------------------------------------------------------------------------------- /app/src/main/res/drawable-v24/ic_launcher_foreground.xml: -------------------------------------------------------------------------------- 1 | 7 | 8 | 9 | 15 | 18 | 21 | 22 | 23 | 24 | 30 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_launcher_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 10 | 15 | 20 | 25 | 30 | 35 | 40 | 45 | 50 | 55 | 60 | 65 | 70 | 75 | 80 | 85 | 90 | 95 | 100 | 105 | 110 | 115 | 120 | 125 | 130 | 135 | 140 | 145 | 150 | 155 | 160 | 165 | 170 | 171 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_start_vpn.xml: -------------------------------------------------------------------------------- 1 | 2 | 13 | 14 | 25 | 26 | 38 | 39 | 50 | 51 |