diff --git a/.gitignore b/.gitignore
index a307a61d..598ae792 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,37 +1 @@
-# Built application files
-/*/build/
-
-# Crashlytics configuations
-com_crashlytics_export_strings.xml
-
-# Local configuration file (sdk path, etc)
-local.properties
-
-# Gradle generated files
-.gradle/
-
-# Signing files
-.signing/
-
-# User-specific configurations
-.idea/libraries/
-.idea/workspace.xml
-.idea/tasks.xml
-.idea/.name
-.idea/compiler.xml
-.idea/copyright/profiles_settings.xml
-.idea/encodings.xml
-.idea/misc.xml
-.idea/modules.xml
-.idea/scopes/scope_settings.xml
-.idea/vcs.xml
-*.iml
-
-# OS-specific files
-.DS_Store
-.DS_Store?
-._*
-.Spotlight-V100
-.Trashes
-ehthumbs.db
-Thumbs.db
\ No newline at end of file
+/.vscode
diff --git a/Readme-ja.md b/Readme-ja.md
new file mode 100644
index 00000000..06a9add1
--- /dev/null
+++ b/Readme-ja.md
@@ -0,0 +1,197 @@
+Android 通信 Proxy 設定ツール
+=============
+
+Language/[English](https://github.com/raise-isayan/TunProxy/blob/master/Readme.md)
+
+このツールは、Android の VPNService 機能を利用した Proxy 設定ツールです。
+指定したアプリからの通信のみを取得することが可能です。
+
+## 使用方法
+
+ユーザ証明書領域に信頼させたい Root CA がない場合はインストールします。
+
+TunProxyアプリを起動すると以下の画面が起動します。
+
+
+
+* Profile 選択
+ * 選択したプロファイルを適用します。
+ * Manual
+ * 手動で設定します。
+
+* Proxy address (host:port)
+ * 接続先のプロキシサーバを ** host:port ** の形式で指定します。
+ IPアドレスの場合はIPv4形式で記載する必要があります。
+
+* Proxy Type
+ * HTTP
+ HTTPプロキシとして接続します。
+ * SOCKS5
+ SOCKS5プロキシとして接続します。
+
+* [Start] ボタン
+ * 接続を開始します。
+* [Stop] ボタン
+ * 接続を停止します。
+
+## メニュー
+
+画面上部のメニューアイコン()からアプリケーションの設定ができます。
+
+## Settings
+
+VPNの接続設定を行います。
+
+ ⇒ 
+
+Disallowed Application と Allowed Application の2つのモードがありますが、同時に指定することはできません。
+このためどちらのモードで動作させたいかを選択する必要があります。
+デフォルトは **Disallowed Application** が選択された状態です。
+
+## VPN connection setting
+
+* Disallowed Application
+ * VPN通信から除外したいアプリを選択する。
+ 選択したアプリはVPN通信を経由されなくなり、VPNを利用しない場合と同じ挙動となります。
+
+* Allowed Application
+ * VPN通信を行いたいアプリを選択する。
+ 選択したアプリはVPN通信を経由するようになります。
+ 選択されていないアプリは、VPNを利用しない場合と同じ挙動になります。
+ なお、一つも選択されていない場合は、すべてのアプリの通信がVPNを経由します。
+
+* Clear all selection
+ * Allowed / Disallowed Application のすべての選択をクリアします。
+
+### Settings 検索
+
+ / 
+
+画面上部の検索アイコン()から、アプリケーションを絞り込めます。
+アプリケーション名または、パッケージ名に指定したキーワードを含むアプリケーションのみが表示されます。
+
+プリケーションリストは、画面上部のメニューアイコン()からソートできます。
+
+### Settings Menu
+
+アプリ一覧の表示方法を変更します。
+
+* show system app
+ * システムアプリを表示します。
+
+#### sort by
+
+* app name
+ * アプリケーション名でアプリケーションリストを並べ替えます。
+
+* package name
+ * パッケージ名でアプリケーションリストを並べ替えます。
+
+#### order by
+
+* ascending
+ * 昇順にソートします
+
+* descending
+ * 降順にソートします
+
+#### filter by
+
+* app name
+ * アプリケーション名に指定したキーワードを含むものを検索します。
+
+* package name
+ * パッケージ名に指定したキーワードを含むものを検索します。
+
+## Proxy setting
+ 有効な場合、開始する前にプロキシの接続状態を確認します。
+
+## DNS setting
+
+DNSサーバの設定を行います。
+
+* Use Custom DNS
+ * 有効な場合カスタムDNSサーバを指定します。
+
+* Primary DNS
+ * プライマリDNSを指定します。
+
+* Secondary DNS
+ * セカンダリDNSを指定します。
+
+## Profile Settings
+
+### Profile Lis
+
+プロファイルの一覧が表示されます。
+
+
+
+### Profile New
+
+プロファイルを新規作成します。
+
+
+
+### Profile Edit
+
+プロファイルを編集します。
+
+
+
+### Profile Delete
+
+プロファイルを削除します。
+
+
+
+## About
+
+アプリケーションバージョンを表示します。
+
+## MITM (SSL 復号化)
+
+TunProxyはSSL復号化を実行しません。TunProxyは透過プロキシのように機能します。
+
+SSL復号化を実行するには、Burp suite や Fiddler などのSSLを復号化可能なローカルプロキシツールのIPをTunProxyのIPに設定します
+
+SSLを復号化可能なローカルプロキシツールとしては次に記載するものがあります。
+
+* Burp suite
+ * https://portswigger.net/burp
+
+* Fiddler
+ * https://www.telerik.com/fiddler
+
+* ZAP Proxy
+ * https://www.zaproxy.org/
+
+
+SSLを復号化するには、ローカルプロキシツールのRoot証明書をAndroid端末のユーザ証明書にインストールしてください。
+ただし、Android 7.0 以降において、デフォルトではユーザ証明書を信頼しなくなっています。
+
+* https://android-developers.googleblog.com/2016/07/changes-to-trusted-certificate.html
+
+解決策として次のWebサイトを参照してください。
+
+* Android 7 Nougatおよび認証局
+ * https://blog.jeroenhd.nl/article/android-7-nougat-and-certificate-authorities
+
+## 動作環境
+
+* Android 6.0 (API Level 23) 以降
+
+### ビルド
+ gradlew build
+
+## 謝辞
+
+アプリ作成にあたりコードの大部分は以下のアプリをベースとして作成しました。
+
+* forked from MengAndy/tun2http
+ * https://github.com/MengAndy/tun2http/
+
+## 開発環境
+
+* JRE(JDK) 1.8以上(Open JDK)
+* AndroidStudio 2026.1.1 (https://developer.android.com/studio/index.html)
diff --git a/Readme.md b/Readme.md
new file mode 100644
index 00000000..62dd8719
--- /dev/null
+++ b/Readme.md
@@ -0,0 +1,187 @@
+Android HTTP traffic Proxy setting tool
+=============
+
+Language/[Japanese](https://github.com/raise-isayan/TunProxy/blob/master/Readme-ja.md)
+
+This tool is a proxy configuration tool that takes advantage of Android VPNService feature.
+Only the communication from the specified application can be acquired.
+
+## How to use
+
+When you start the TunProxy application, the following screen will be launched.
+
+
+
+* Select Profile
+ * Apply the selected profile.
+ * Manual
+ * Configure it manually.
+
+* Proxy address (host:port)
+ * Specify the destination proxy server in the format ** host:port r**.
+ When you specify an IP address, use the IPv4 format.
+
+* [Start] button
+ * Start the VPN service.
+* [Stop] button
+ * Stop the VPN service.
+
+## Menu
+
+Application settings can be made from the menu icon () at the top of the screen.
+
+## Settings
+
+Configure VPN service settings.
+
+ ⇒ 
+
+## VPN connection setting
+
+There are two modes, Disallowed Application and Allowed Application, but you can not specify them at the same time.
+Because of this you will have to choose whether you want to run in either mode.
+The default is **Disallowed Application** selected.
+
+* Disallowed Application
+ * Select the application you want to exclude from VPN service.
+ The selected application will no longer go through VPN service and behave the same as if you do not use VPN.
+
+* Allowed Application
+ * Select the application for which you want to perform VPN service.
+ The selected application will now go through VPN service.
+ Applications that are not selected behave the same as when not using VPN.
+ In addition, if none of them are selected, communication of all applications will go through VPN.
+
+* Clear all selection
+ * Clear all selections of Allowed / Disallowed application list.
+
+### Settings Search
+
+ / 
+
+You can narrow down the applications from the search icon.()
+Only applications that contain the keyword specified in the application name or package name will be displayed.
+
+The application list can be sorted from the menu icon () at the top of the screen.
+
+### Settings Menu
+
+Changed the way the application list is displayed.
+
+* show system app
+ * show system application
+
+### sort by
+
+* app name
+ * Sort application list by application name
+
+* package name
+ * Sort application list by package name
+
+### order by
+
+* ascending
+ * Sorting in ascending order
+
+* descending
+ * Sorting in descending order
+
+### filter by
+
+* app name
+ * Search for the application name that contains the keyword you specified.
+
+* package name
+ * Search for the package name that contains the keyword you specified.
+
+## Proxy setting
+ Check proxy connectivity before starting
+
+## DNS setting
+
+Configure the DNS server settings.
+
+* Use Custom DNS
+ * Enable to specify custom DNS servers
+
+* Primary DNS
+ * Specify the primary DNS server.
+
+* Secondary DNS
+ * Specify the secondary DNS server.
+
+## Profile Settings
+
+### Profile List
+
+List view of profiles.
+
+
+
+### Profile New
+
+Create a new profile.
+
+
+
+### Profile Edit
+
+Edit Profile
+
+
+
+### Profile Delete
+
+Delete Profile
+
+
+
+## About
+
+Display application version
+
+## MITM (SSL decrypt)
+
+TunProxy does not perform SSL decryption. TunProxy acts like a transparent proxy.
+To perform SSL decryption, set the IP of an SSL decryptable proxy such as Burp suite or Fiddler to the IP of TunProxy
+
+The following are local proxy tools that can decrypt SSL.
+
+* Burp suite
+ * https://portswigger.net/burp
+
+* Fiddler
+ * https://www.telerik.com/fiddler
+
+* ZAP Proxy
+ * https://www.zaproxy.org/
+
+To decrypt SSL, install the local proxy tool Root certificate in the Android device user certificate.
+However, in Android 7.0 and later, the application no longer trusts user certificates by default.
+
+* https://android-developers.googleblog.com/2016/07/changes-to-trusted-certificate.html
+
+Please refer to the following web site as a solution
+
+* Android 7 Nougat and certificate authorities
+ * https://blog.jeroenhd.nl/article/android-7-nougat-and-certificate-authorities
+
+## Operating environment
+
+* Android 6.0 (API Level 23) or later
+
+### Build
+ gradlew build
+
+## Base application
+
+Most of the code was created based on the following applications for creating applications.
+
+* forked from MengAndy/tun2http
+ * https://github.com/MengAndy/tun2http/
+
+## Development environment
+
+* JRE(JDK) 1.8 or later(Open JDK)
+* AndroidStudio 2026.1.1 (https://developer.android.com/studio/index.html)
diff --git a/android_app/.aiexlude b/android_app/.aiexlude
new file mode 100644
index 00000000..eaeb0f24
--- /dev/null
+++ b/android_app/.aiexlude
@@ -0,0 +1,8 @@
+*.iml
+*.jks
+.gradle
+.idea
+.DS_Store
+local.properties
+/build
+*.zip
diff --git a/android_app/.gitignore b/android_app/.gitignore
new file mode 100644
index 00000000..cbc9ec9f
--- /dev/null
+++ b/android_app/.gitignore
@@ -0,0 +1,7 @@
+*.iml
+.gradle
+.idea
+.DS_Store
+local.properties
+/build
+*.zip
diff --git a/android_app/app/.gitignore b/android_app/app/.gitignore
index 796b96d1..ef3d3031 100644
--- a/android_app/app/.gitignore
+++ b/android_app/app/.gitignore
@@ -1 +1,4 @@
/build
+.cxx
+.externalNativeBuild
+local.properties
diff --git a/android_app/app/CMakeLists.txt b/android_app/app/CMakeLists.txt
index 8f126c2e..dd6a6aaf 100644
--- a/android_app/app/CMakeLists.txt
+++ b/android_app/app/CMakeLists.txt
@@ -1,35 +1,35 @@
-cmake_minimum_required(VERSION 3.4.1)
+cmake_minimum_required(VERSION 3.10.2)
include_directories(${CMAKE_CURRENT_SOURCE_DIR}/src/main/cpp)
-set(EXECUTABLE_OUTPUT_PATH "${CMAKE_CURRENT_SOURCE_DIR}/src/main/assets/${ANDROID_ABI}")
+set(EXECUTABLE_OUTPUT_PATH "${CMAKE_CURRENT_SOURCE_DIR}/src/main/assets/${ANDROID_ABI}")
add_library( # Sets the name of the library.
- tun2http
+ tun2http
- # Sets the library as a shared library.
- SHARED
+ # Sets the library as a shared library.
+ SHARED
- src/main/cpp/dhcp.c
- src/main/cpp/dns.c
- src/main/cpp/icmp.c
- src/main/cpp/ip.c
- src/main/cpp/http.c
- src/main/cpp/tun2http.c
- src/main/cpp/session.c
- src/main/cpp/tcp.c
- src/main/cpp/tls.c
- src/main/cpp/udp.c
- src/main/cpp/util.c
-)
+ src/main/cpp/dhcp.c
+ src/main/cpp/dns.c
+ src/main/cpp/icmp.c
+ src/main/cpp/ip.c
+ src/main/cpp/http.c
+ src/main/cpp/tun2http.c
+ src/main/cpp/session.c
+ src/main/cpp/tcp.c
+ src/main/cpp/tls.c
+ src/main/cpp/udp.c
+ src/main/cpp/util.c
+ )
find_library( # Sets the name of the path variable.
- log-lib
+ log-lib
- # Specifies the name of the NDK library that
- # you want CMake to locate.
- log )
+ # Specifies the name of the NDK library that
+ # you want CMake to locate.
+ log)
target_link_libraries( # Specifies the target library.
- tun2http
- ${log-lib}
- )
\ No newline at end of file
+ tun2http
+ ${log-lib}
+ )
\ No newline at end of file
diff --git a/android_app/app/build.gradle b/android_app/app/build.gradle
index 46bd3d72..68fb3769 100644
--- a/android_app/app/build.gradle
+++ b/android_app/app/build.gradle
@@ -1,38 +1,90 @@
apply plugin: 'com.android.application'
android {
- compileSdkVersion 26
+ namespace = 'tun.proxy'
+
+ compileSdkVersion 37
+ buildToolsVersion "37.0.0"
+
defaultConfig {
- applicationId "com.tun2http.app"
- minSdkVersion 19
- targetSdkVersion 26
- versionCode 2
- versionName '1.01'
+ applicationId "tun.proxy"
+ minSdk 23
+ targetSdk 37
+ versionCode 100510
+ versionName VERSION_NAME
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
externalNativeBuild {
cmake {
- cppFlags "-std=c++11 -fvisibility=hidden "
- abiFilters 'armeabi-v7a'
+ cppFlags "-std=c++17 -fvisibility=hidden "
+ abiFilters 'armeabi-v7a','arm64-v8a', 'x86'
arguments "-DCMAKE_VERBOSE_MAKEFILE=1 -DANDROID_FUNCTION_LEVEL_LINKING=ON"
}
}
+ testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
+ }
+ signingConfigs {
+ debug {
+ }
+ release {
+ // @See gradle.properties
+ storeFile file(productKeyStore)
+ keyAlias productKeyAlias
+ storePassword productKeyStorePassword
+ keyPassword productKeyAliasPassword
+ }
}
buildTypes {
release {
minifyEnabled false
- proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
+ proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
+ signingConfig = signingConfigs.release
}
}
externalNativeBuild {
cmake {
- path "CMakeLists.txt"
+ path file('CMakeLists.txt')
}
}
- productFlavors {
+
+ ndkVersion = '25.1.8937393'
+ lint {
+ abortOnError = false
+ disable 'QueryPermissionsNeeded'
+ }
+
+ testOptions {
+ unitTests.returnDefaultValues = true
}
+
+ androidComponents {
+ // すべてのビルドバリアントに対して処理を行う
+ onVariants(selector().all(), { variant ->
+ if (variant.buildType == "release") {
+ variant.outputs.each { output ->
+ def versionName = android.defaultConfig.versionName
+ output.outputFileName.set("${APP_NAME}_v${versionName}.apk")
+ }
+ }
+ })
+ }
+
}
dependencies {
- implementation fileTree(include: ['*.jar'], dir: 'libs')
- implementation 'com.android.support:appcompat-v7:26.1.0'
+ implementation fileTree(dir: 'libs', include: ['*.jar'])
+
+ implementation 'androidx.preference:preference:1.2.1'
+ implementation 'androidx.appcompat:appcompat:1.7.1'
+ implementation 'androidx.constraintlayout:constraintlayout:2.2.1'
+ implementation 'androidx.vectordrawable:vectordrawable:1.2.0'
+ implementation 'androidx.legacy:legacy-support-v13:1.0.0'
+ implementation 'androidx.legacy:legacy-support-v4:1.0.0'
+ implementation 'com.google.android.material:material:1.14.0'
+ implementation 'androidx.constraintlayout:constraintlayout:2.2.1'
+ implementation 'androidx.navigation:navigation-fragment:2.9.8'
+ implementation 'androidx.navigation:navigation-ui:2.9.8'
+ testImplementation 'junit:junit:4.13.2'
+ androidTestImplementation 'androidx.test.ext:junit:1.3.0'
+ androidTestImplementation 'androidx.test.espresso:espresso-core:3.7.0'
+ testImplementation 'org.json:json:20260522'
}
diff --git a/android_app/app/src/androidTest/java/com/tun2http/app/ExampleInstrumentedTest.java b/android_app/app/src/androidTest/java/com/tun2http/app/ExampleInstrumentedTest.java
deleted file mode 100644
index 9de396e5..00000000
--- a/android_app/app/src/androidTest/java/com/tun2http/app/ExampleInstrumentedTest.java
+++ /dev/null
@@ -1,26 +0,0 @@
-package com.tun2http.app;
-
-import android.content.Context;
-import android.support.test.InstrumentationRegistry;
-import android.support.test.runner.AndroidJUnit4;
-
-import org.junit.Test;
-import org.junit.runner.RunWith;
-
-import static org.junit.Assert.*;
-
-/**
- * Instrumented test, which will execute on an Android device.
- *
- * @see Testing documentation
- */
-@RunWith(AndroidJUnit4.class)
-public class ExampleInstrumentedTest {
- @Test
- public void useAppContext() throws Exception {
- // Context of the app under test.
- Context appContext = InstrumentationRegistry.getTargetContext();
-
- assertEquals("com.tun2http.app", appContext.getPackageName());
- }
-}
diff --git a/android_app/app/src/androidTest/java/tun/proxy/AppInstrumentedTest.java b/android_app/app/src/androidTest/java/tun/proxy/AppInstrumentedTest.java
new file mode 100644
index 00000000..baef2c43
--- /dev/null
+++ b/android_app/app/src/androidTest/java/tun/proxy/AppInstrumentedTest.java
@@ -0,0 +1,50 @@
+package tun.proxy;
+
+import android.content.Context;
+
+import androidx.test.platform.app.InstrumentationRegistry;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.List;
+
+import tun.utils.NetUtil;
+
+import static org.junit.Assert.*;
+
+/**
+ * Instrumented test, which will execute on an Android device.
+ *
+ * @see Testing documentation
+ */
+@RunWith(AndroidJUnit4.class)
+public class AppInstrumentedTest {
+
+ @Before
+ public void setUp() {
+ System.loadLibrary("tun2http");
+ }
+
+ @After
+ public void tearDown() {
+
+ }
+
+ @Test
+ public void useAppContext() {
+ // Context of the app under test.
+ Context appContext = InstrumentationRegistry.getInstrumentation().getTargetContext();
+ assertEquals("tun.proxy", appContext.getPackageName());
+
+ List dnsList = NetUtil.getDefaultDNS(appContext);
+ System.out.println("dnsList:" + dnsList.size());
+ for (String dns : dnsList) {
+ System.out.println("dns:" + dns);
+ }
+
+ }
+}
diff --git a/android_app/app/src/androidTest/java/tun/proxy/MyApplicationTest.java b/android_app/app/src/androidTest/java/tun/proxy/MyApplicationTest.java
new file mode 100644
index 00000000..34d82703
--- /dev/null
+++ b/android_app/app/src/androidTest/java/tun/proxy/MyApplicationTest.java
@@ -0,0 +1,76 @@
+package tun.proxy;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+
+import android.content.Context;
+import android.content.pm.PackageInfo;
+import android.util.Log;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.platform.app.InstrumentationRegistry;
+
+import org.junit.After;
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.List;
+
+import tun.utils.ProgressTask;
+
+@RunWith(AndroidJUnit4.class)
+public class MyApplicationTest {
+ private static final String TAG = "MyApplicationTest";
+
+ @Before
+ public void setUp() {
+
+ }
+
+ @After
+ public void tearDown() {
+
+ }
+
+ @Test
+ public void testMyApplication() {
+ MyApplication app = MyApplication.getInstance();
+ assertNotNull(app);
+
+ /* VPN setting */
+
+ app.storeVPNMode(MyApplication.VPNMode.DISALLOW);
+ assertEquals(MyApplication.VPNMode.DISALLOW, app.loadVPNMode());
+ app.storeVPNMode(MyApplication.VPNMode.ALLOW);
+ assertEquals(MyApplication.VPNMode.ALLOW, app.loadVPNMode());
+
+ /* Proxy setting */
+
+ app.storeProxyConnectivityCheck(false);
+ assertTrue(app.loadProxyConnectivityCheck(true));
+ app.storeProxyConnectivityCheck(true);
+ assertTrue(app.loadProxyConnectivityCheck(false));
+
+ /* DNS setting */
+
+ app.storeUseDnsCustom(true);
+ assertTrue(app.loadUseDnsCustom());
+ app.storeUseDnsCustom(false);
+ assertFalse(app.loadUseDnsCustom());
+
+ app.storePrimaryDns("1.1.1.1");
+ assertEquals("1.1.1.1", app.loadPrimaryDns(""));
+ app.storeSecondaryDns("1.1.1.2");
+ assertEquals("1.1.1.2", app.loadSecondaryDns(""));
+ }
+
+ @Test
+ public void testProxyType() {
+ assertEquals(0, MyApplication.ProxyType.HTTP.ordinal());
+ assertEquals(1, MyApplication.ProxyType.SOCKS5.ordinal());
+ }
+}
diff --git a/android_app/app/src/androidTest/java/tun/proxy/ProgressTaskTest.java b/android_app/app/src/androidTest/java/tun/proxy/ProgressTaskTest.java
new file mode 100644
index 00000000..4d57051e
--- /dev/null
+++ b/android_app/app/src/androidTest/java/tun/proxy/ProgressTaskTest.java
@@ -0,0 +1,60 @@
+package tun.proxy;
+
+import android.content.pm.PackageInfo;
+import android.util.Log;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+
+import org.junit.After;
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.List;
+
+import tun.utils.ProgressTask;
+
+import static org.junit.Assert.assertEquals;
+
+@RunWith(AndroidJUnit4.class)
+public class ProgressTaskTest {
+ private static final String TAG = "ProgressTaskTest";
+
+ @Before
+ public void setUp() {
+
+ }
+
+ @After
+ public void tearDown() {
+
+ }
+
+ @Test
+ public void progressTask() {
+ Log.w(TAG, "progressTask: start");
+
+ ProgressTask> task = new ProgressTask<>() {
+
+ @Override
+ protected List doInBackground(String... var1) {
+ for (int i = 0; i < 100; i++) {
+ try {
+ Thread.sleep(10);
+ Log.d(TAG, "Progress:" + i);
+ } catch (InterruptedException e) {
+ Log.e(TAG, e.getMessage(), e);
+ }
+ }
+ return null;
+ }
+
+ };
+ Assert.assertEquals(ProgressTask.Status.PENDING, task.getStatus());
+ Log.w(TAG, "progressTask: execute");
+ task.execute();
+ Assert.assertEquals(ProgressTask.Status.RUNNING, task.getStatus());
+ Log.w(TAG, "progressTask: end");
+ }
+}
diff --git a/android_app/app/src/main/AndroidManifest.xml b/android_app/app/src/main/AndroidManifest.xml
index dbef8e4e..e570eb23 100644
--- a/android_app/app/src/main/AndroidManifest.xml
+++ b/android_app/app/src/main/AndroidManifest.xml
@@ -1,40 +1,75 @@
+ xmlns:tools="http://schemas.android.com/tools">
-
-
-
+
+
+
+
+
+
+
+
+
-
-
+ android:theme="@style/AppTheme"
+ tools:targetApi="24">
+
-
-
-
+
+
+
+
+
+
+
+
+
+
+ android:exported="true"
+ android:permission="android.permission.BIND_VPN_SERVICE"
+ tools:ignore="VpnServicePolicy">
-
+
+
+
-
\ No newline at end of file
+
diff --git a/android_app/app/src/main/cpp/dns.c b/android_app/app/src/main/cpp/dns.c
index 2e6c3e0d..65bcf19e 100644
--- a/android_app/app/src/main/cpp/dns.c
+++ b/android_app/app/src/main/cpp/dns.c
@@ -17,17 +17,16 @@ int32_t get_qname(const uint8_t *data, const size_t datalen, uint16_t off, char
c = 1;
off += 2;
}
- }
- else if (ptr + 1 + len <= datalen && noff + len <= DNS_QNAME_MAX) {
+ } else if (ptr + 1 + len <= datalen && noff + len <= DNS_QNAME_MAX) {
memcpy(qname + noff, data + ptr + 1, len);
*(qname + noff + len) = '.';
noff += (len + 1);
ptr += (len + 1);
len = *(data + ptr);
- }
- else
+ } else {
break;
+ }
}
ptr++;
@@ -56,8 +55,9 @@ void parse_dns_response(const struct arguments *args, const struct udp_session *
int acount = ntohs(dns->ans_count);
if (dns->qr == 1 && dns->opcode == 0 && qcount > 0 && acount > 0) {
log_android(ANDROID_LOG_DEBUG, "DNS response qcount %d acount %d", qcount, acount);
- if (qcount > 1)
+ if (qcount > 1) {
log_android(ANDROID_LOG_WARN, "DNS response qcount %d acount %d", qcount, acount);
+ }
// http://tools.ietf.org/html/rfc1035
char name[DNS_QNAME_MAX + 1];
@@ -80,8 +80,7 @@ void parse_dns_response(const struct arguments *args, const struct udp_session *
q, qtype, qclass, qname);
}
off += 4;
- }
- else {
+ } else {
log_android(ANDROID_LOG_WARN,
"DNS response Q invalid off %d datalen %d", off, *datalen);
return;
@@ -103,33 +102,30 @@ void parse_dns_response(const struct arguments *args, const struct udp_session *
(qtype == DNS_QTYPE_A || qtype == DNS_QTYPE_AAAA)) {
char rd[INET6_ADDRSTRLEN + 1];
- if (qtype == DNS_QTYPE_A)
+ if (qtype == DNS_QTYPE_A) {
inet_ntop(AF_INET, data + off, rd, sizeof(rd));
- else if (qclass == DNS_QCLASS_IN && qtype == DNS_QTYPE_AAAA)
+ } else if (qclass == DNS_QCLASS_IN && qtype == DNS_QTYPE_AAAA) {
inet_ntop(AF_INET6, data + off, rd, sizeof(rd));
- }
- else
+ }
+ } else {
log_android(ANDROID_LOG_DEBUG,
"DNS answer %d qname %s qclass %d qtype %d ttl %d length %d",
a, name, qclass, qtype, ttl, rdlength);
-
+ }
off += rdlength;
- }
- else {
+ } else {
log_android(ANDROID_LOG_WARN,
"DNS response A invalid off %d rdlength %d datalen %d",
off, rdlength, *datalen);
return;
}
- }
- else {
+ } else {
log_android(ANDROID_LOG_WARN,
"DNS response A invalid off %d datalen %d", off, *datalen);
return;
}
}
- }
- else if (acount > 0)
+ } else if (acount > 0)
log_android(ANDROID_LOG_WARN,
"DNS response qr %d opcode %d qcount %d acount %d",
dns->qr, dns->opcode, qcount, acount);
@@ -157,9 +153,9 @@ int get_dns_query(const struct arguments *args, const struct udp_session *u,
*qtype = ntohs(*((uint16_t *) (data + off)));
*qclass = ntohs(*((uint16_t *) (data + off + 2)));
return 0;
- }
- else
+ } else {
log_android(ANDROID_LOG_WARN, "DNS query invalid off %d datalen %d", off, datalen);
+ }
}
return -1;
diff --git a/android_app/app/src/main/cpp/http.c b/android_app/app/src/main/cpp/http.c
index 08111d77..a2ac792b 100644
--- a/android_app/app/src/main/cpp/http.c
+++ b/android_app/app/src/main/cpp/http.c
@@ -13,9 +13,9 @@
static const char http_503[] =
"HTTP/1.1 503 Service Temporarily Unavailable\r\n"
- "Content-Type: text/html\r\n"
- "Connection: close\r\n\r\n"
- "Backend not available";
+ "Content-Type: text/html\r\n"
+ "Connection: close\r\n\r\n"
+ "Backend not available";
/*
@@ -34,8 +34,7 @@ static const char http_503[] =
#include "tun2http.h"
-int
-get_header(const char *header, const char *data, size_t data_len, char *value) {
+int get_header(const char *header, const char *data, size_t data_len, char *value) {
int len, header_len;
header_len = strlen(header);
@@ -64,8 +63,7 @@ get_header(const char *header, const char *data, size_t data_len, char *value) {
return -2;
}
-int
-next_header(const char **data, size_t *len) {
+int next_header(const char **data, size_t *len) {
int header_len;
/* perhaps we can optimize this to reuse the value of header_len, rather
@@ -84,8 +82,9 @@ next_header(const char **data, size_t *len) {
header_len = 0;
while (*len > header_len + 1
&& (*data)[header_len] != '\r'
- && (*data)[header_len + 1] != '\n')
+ && (*data)[header_len + 1] != '\n') {
header_len++;
+ }
return header_len;
}
@@ -114,12 +113,12 @@ uint8_t *find_data(uint8_t *data, size_t data_len, char *value) {
return 0;
}
-uint_t patch_buffer[2*MTU];
+uint8_t patch_buffer[2 * MTU];
uint8_t *patch_http_url(uint8_t *data, size_t *data_len) {
__android_log_print(ANDROID_LOG_VERBOSE, LOG_TAG, "patch_http_url start");
- char hostname[1024];
+ char hostname[512];
uint8_t *host = find_data(data, *data_len, "Host: ");
size_t length = 0;
if (host) {
@@ -136,38 +135,29 @@ uint8_t *patch_http_url(uint8_t *data, size_t *data_len) {
__android_log_print(ANDROID_LOG_VERBOSE, LOG_TAG, "patch_http_url find word");
- //GET POST PUT DELETE HEAD OPTIONS PATCH
- char *word;
- uint8_t *pos = 0;
- if (pos = find_data(data, *data_len, "GET ")) {
- word = "GET ";
- } else if (pos = find_data(data, *data_len, "POST ")) {
- word = "POST ";
- } else if (pos = find_data(data, *data_len, "PUT ")) {
- word = "PUT ";
- } else if (pos = find_data(data, *data_len, "DELETE ")) {
- word = "DELETE ";
- } else if (pos = find_data(data, *data_len, "HEAD ")) {
- word = "HEAD ";
- } else if (pos = find_data(data, *data_len, "OPTIONS ")) {
- word = "OPTIONS ";
- } else if (pos = find_data(data, *data_len, "PATCH ")) {
- word = "PATCH ";
+ size_t word_len = 0;
+ while (word_len < *data_len && data[word_len] != ' ' && data[word_len] != '\r' &&
+ data[word_len] != '\n') {
+ word_len++;
}
- if (!pos) {
- LOG("patch_http_url no word");
+ if (word_len == 0 || word_len >= *data_len || data[word_len] != ' ') {
+ LOG("patch_http_url no method space");
return 0;
}
+ if (word_len == strlen("CONNECT") && memcmp(data, "CONNECT", 7) == 0) {
+ LOG("patch_http_url skip CONNECT");
+ return 0;
+ }
- size_t http_len = strlen("http://");
- size_t word_len = strlen(word);
- size_t pos1 = pos - data + word_len;
+ size_t pos1 = word_len + 1;
LOG("patch_http_url word found");
- if (data[pos1] == 'h' &&
+ size_t http_len = strlen("http://");
+ if (pos1 + http_len <= *data_len &&
+ data[pos1] == 'h' &&
data[pos1 + 1] == 't' &&
data[pos1 + 2] == 't' &&
data[pos1 + 3] == 'p' &&
@@ -179,8 +169,8 @@ uint8_t *patch_http_url(uint8_t *data, size_t *data_len) {
uint8_t *new_data = &patch_buffer[0];
LOG("patch_http_url start patch");
- memcpy(new_data, data, pos1);
+ memcpy(new_data, data, pos1);
memcpy(new_data + pos1, "http://", http_len);
memcpy(new_data + pos1 + http_len, hostname, length);
memcpy(new_data + pos1 + http_len + length, data + pos1, *data_len - pos1);
diff --git a/android_app/app/src/main/cpp/http.h b/android_app/app/src/main/cpp/http.h
index 94789ca4..9390cfcd 100644
--- a/android_app/app/src/main/cpp/http.h
+++ b/android_app/app/src/main/cpp/http.h
@@ -4,6 +4,12 @@
#include
+int get_header(const char *header, const char *data, size_t data_len, char *value);
+
+int next_header(const char **data, size_t *len);
+
+uint8_t *find_data(uint8_t *data, size_t data_len, char *value);
+
uint8_t *patch_http_url(uint8_t *data, size_t *data_len);
#endif //TUN2HTTP_TLS_H
diff --git a/android_app/app/src/main/cpp/icmp.c b/android_app/app/src/main/cpp/icmp.c
index 0f5efbdb..6faba2d5 100644
--- a/android_app/app/src/main/cpp/icmp.c
+++ b/android_app/app/src/main/cpp/icmp.c
@@ -24,8 +24,7 @@ int check_icmp_session(const struct arguments *args, struct ng_session *s,
if (s->icmp.version == 4) {
inet_ntop(AF_INET, &s->icmp.saddr.ip4, source, sizeof(source));
inet_ntop(AF_INET, &s->icmp.daddr.ip4, dest, sizeof(dest));
- }
- else {
+ } else {
inet_ntop(AF_INET6, &s->icmp.saddr.ip6, source, sizeof(source));
inet_ntop(AF_INET6, &s->icmp.daddr.ip6, dest, sizeof(dest));
}
@@ -53,16 +52,16 @@ void check_icmp_socket(const struct arguments *args, const struct epoll_event *e
int serr = 0;
socklen_t optlen = sizeof(int);
int err = getsockopt(s->socket, SOL_SOCKET, SO_ERROR, &serr, &optlen);
- if (err < 0)
+ if (err < 0) {
log_android(ANDROID_LOG_ERROR, "ICMP getsockopt error %d: %s",
errno, strerror(errno));
- else if (serr)
+ } else if (serr) {
log_android(ANDROID_LOG_ERROR, "ICMP SO_ERROR %d: %s",
serr, strerror(serr));
+ }
s->icmp.stop = 1;
- }
- else {
+ } else {
// Check socket read
if (ev->events & EPOLLIN) {
s->icmp.time = time(NULL);
@@ -75,21 +74,21 @@ void check_icmp_socket(const struct arguments *args, const struct epoll_event *e
log_android(ANDROID_LOG_WARN, "ICMP recv error %d: %s",
errno, strerror(errno));
- if (errno != EINTR && errno != EAGAIN)
+ if (errno != EINTR && errno != EAGAIN) {
s->icmp.stop = 1;
- }
- else if (bytes == 0) {
+ }
+ } else if (bytes == 0) {
log_android(ANDROID_LOG_WARN, "ICMP recv eof");
s->icmp.stop = 1;
- }
- else {
+ } else {
// Socket read data
char dest[INET6_ADDRSTRLEN + 1];
- if (s->icmp.version == 4)
+ if (s->icmp.version == 4) {
inet_ntop(AF_INET, &s->icmp.daddr.ip4, dest, sizeof(dest));
- else
+ } else {
inet_ntop(AF_INET6, &s->icmp.daddr.ip6, dest, sizeof(dest));
+ }
// cur->id should be equal to icmp->icmp_id
// but for some unexplained reason this is not the case
@@ -120,8 +119,9 @@ void check_icmp_socket(const struct arguments *args, const struct epoll_event *e
icmp->icmp_cksum = ~calc_checksum(csum, buffer, (size_t) bytes);
// Forward to tun
- if (write_icmp(args, &s->icmp, buffer, (size_t) bytes) < 0)
+ if (write_icmp(args, &s->icmp, buffer, (size_t) bytes) < 0) {
s->icmp.stop = 1;
+ }
}
free(buffer);
}
@@ -145,8 +145,7 @@ jboolean handle_icmp(const struct arguments *args,
if (version == 4) {
inet_ntop(AF_INET, &ip4->saddr, source, sizeof(source));
inet_ntop(AF_INET, &ip4->daddr, dest, sizeof(dest));
- }
- else {
+ } else {
inet_ntop(AF_INET6, &ip6->ip6_src, source, sizeof(source));
inet_ntop(AF_INET6, &ip6->ip6_dst, dest, sizeof(dest));
}
@@ -183,8 +182,7 @@ jboolean handle_icmp(const struct arguments *args,
if (version == 4) {
s->icmp.saddr.ip4 = (__be32) ip4->saddr;
s->icmp.daddr.ip4 = (__be32) ip4->daddr;
- }
- else {
+ } else {
memcpy(&s->icmp.saddr.ip6, &ip6->ip6_src, 16);
memcpy(&s->icmp.daddr.ip6, &ip6->ip6_dst, 16);
}
@@ -207,8 +205,9 @@ jboolean handle_icmp(const struct arguments *args,
memset(&s->ev, 0, sizeof(struct epoll_event));
s->ev.events = EPOLLIN | EPOLLERR;
s->ev.data.ptr = s;
- if (epoll_ctl(epoll_fd, EPOLL_CTL_ADD, s->socket, &s->ev))
+ if (epoll_ctl(epoll_fd, EPOLL_CTL_ADD, s->socket, &s->ev)) {
log_android(ANDROID_LOG_ERROR, "epoll add icmp error %d: %s", errno, strerror(errno));
+ }
s->next = ng_session;
ng_session = s;
@@ -246,8 +245,7 @@ jboolean handle_icmp(const struct arguments *args,
server4.sin_family = AF_INET;
server4.sin_addr.s_addr = (__be32) ip4->daddr;
server4.sin_port = 0;
- }
- else {
+ } else {
server6.sin6_family = AF_INET6;
memcpy(&server6.sin6_addr, &ip6->ip6_dst, 16);
server6.sin6_port = 0;
@@ -278,8 +276,9 @@ int open_icmp_socket(const struct arguments *args, const struct icmp_session *cu
}
// Protect socket
- if (protect_socket(args, sock) < 0)
+ if (protect_socket(args, sock) < 0) {
return -1;
+ }
return sock;
}
@@ -312,8 +311,7 @@ ssize_t write_icmp(const struct arguments *args, const struct icmp_session *cur,
// Calculate IP4 checksum
ip4->check = ~calc_checksum(0, (uint8_t *) ip4, sizeof(struct iphdr));
- }
- else {
+ } else {
len = sizeof(struct ip6_hdr) + datalen;
buffer = malloc(len);
struct ip6_hdr *ip6 = (struct ip6_hdr *) buffer;
diff --git a/android_app/app/src/main/cpp/ip.c b/android_app/app/src/main/cpp/ip.c
index 7b65fc57..2d60fa95 100644
--- a/android_app/app/src/main/cpp/ip.c
+++ b/android_app/app/src/main/cpp/ip.c
@@ -10,10 +10,12 @@ uint16_t get_mtu() {
}
uint16_t get_default_mss(int version) {
- if (version == 4)
+ if (version == 4) {
return (uint16_t) (get_mtu() - sizeof(struct iphdr) - sizeof(struct tcphdr));
- else
+ }
+ else {
return (uint16_t) (get_mtu() - sizeof(struct ip6_hdr) - sizeof(struct tcphdr));
+ }
}
int check_tun(const struct arguments *args,
@@ -27,17 +29,17 @@ int check_tun(const struct arguments *args,
// Check tun read
if (ev->events & EPOLLIN) {
- uint8_t *buffer = malloc(get_mtu()*2);
+ uint8_t *buffer = malloc(get_mtu() * 2);
ssize_t length = read(args->tun, buffer, get_mtu());
if (length < 0) {
free(buffer);
log_android(ANDROID_LOG_ERROR, "tun %d read error %d: %s",
args->tun, errno, strerror(errno));
- if (errno == EINTR || errno == EAGAIN)
+ if (errno == EINTR || errno == EAGAIN) {
// Retry later
return 0;
- else {
+ } else {
return -1;
}
} else if (length > 0) {
@@ -227,8 +229,9 @@ void handle_ip(const struct arguments *args,
flags[flen++] = 'R';
// TODO checksum
- } else if (protocol != IPPROTO_HOPOPTS && protocol != IPPROTO_IGMP && protocol != IPPROTO_ESP)
+ } else if (protocol != IPPROTO_HOPOPTS && protocol != IPPROTO_IGMP && protocol != IPPROTO_ESP) {
log_android(ANDROID_LOG_WARN, "Unknown protocol %d", protocol);
+ }
flags[flen] = 0;
@@ -248,18 +251,19 @@ void handle_ip(const struct arguments *args,
jint uid = -1;
if (protocol == IPPROTO_ICMP || protocol == IPPROTO_ICMPV6 ||
(protocol == IPPROTO_UDP && !has_udp_session(args, pkt, payload)) ||
- (protocol == IPPROTO_TCP && syn))
+ (protocol == IPPROTO_TCP && syn)) {
uid = get_uid(version, protocol, saddr, sport, daddr, dport);
+ }
log_android(ANDROID_LOG_DEBUG,
"Packet v%d %s/%u > %s/%u proto %d flags %s uid %d",
version, source, sport, dest, dport, protocol, flags, uid);
- if (protocol == IPPROTO_ICMP || protocol == IPPROTO_ICMPV6)
+ if (protocol == IPPROTO_ICMP || protocol == IPPROTO_ICMPV6) {
handle_icmp(args, pkt, length, payload, uid, epoll_fd);
- else if (protocol == IPPROTO_UDP)
+ } else if (protocol == IPPROTO_UDP) {
handle_udp(args, pkt, length, payload, uid, epoll_fd);
- else if (protocol == IPPROTO_TCP) {
+ } else if (protocol == IPPROTO_TCP) {
handle_tcp(args, pkt, length, payload, uid, epoll_fd);
}
}
@@ -284,12 +288,14 @@ jint get_uid(const int version, const int protocol,
uid = get_uid_sub(6, protocol, saddr, sport, daddr128, dport);
}
- if (uid < 0)
+ if (uid < 0) {
uid = get_uid_sub(version, protocol, saddr, sport, daddr, dport);
+ }
- if (uid < 0)
+ if (uid < 0) {
log_android(ANDROID_LOG_ERROR, "uid v%d p%d %u > %s/%u not found",
version, protocol, sport, dest, dport);
+ }
return uid;
}
@@ -318,16 +324,17 @@ jint get_uid_sub(const int version, const int protocol,
// Get proc file name
char *fn = NULL;
- if (protocol == IPPROTO_ICMP && version == 4)
+ if (protocol == IPPROTO_ICMP && version == 4) {
fn = "/proc/net/icmp";
- else if (protocol == IPPROTO_ICMPV6 && version == 6)
+ } else if (protocol == IPPROTO_ICMPV6 && version == 6) {
fn = "/proc/net/icmp6";
- else if (protocol == IPPROTO_TCP)
+ } else if (protocol == IPPROTO_TCP) {
fn = (version == 4 ? "/proc/net/tcp" : "/proc/net/tcp6");
- else if (protocol == IPPROTO_UDP)
+ } else if (protocol == IPPROTO_UDP) {
fn = (version == 4 ? "/proc/net/udp" : "/proc/net/udp6");
- else
+ } else {
return uid;
+ }
// Open proc file
FILE *fd = fopen(fn, "r");
@@ -346,26 +353,28 @@ jint get_uid_sub(const int version, const int protocol,
_sport = -1;
_dport = -1;
u = -1;
- if (version == 4)
+ if (version == 4) {
fields = sscanf(
line,
"%*d: %*X:%X %8s:%X %*X %*lX:%*lX %*X:%*X %*X %d %*d %*ld",
&_sport, hex, &_dport, &u);
- else
+ } else {
fields = sscanf(
line,
"%*d: %*X:%X %32s:%X %*X %*lX:%*lX %*X:%*X %*X %d %*d %*ld",
&_sport, hex, &_dport, &u);
+ }
if (fields == 4 && (version == 4 ? strlen(hex) == 8 : strlen(hex) == 32)) {
if (_sport > 0 && u >= 0) {
hex2bytes(hex, version == 4 ? _daddr4 : _daddr6);
- if (version == 4)
+ if (version == 4) {
((uint32_t *) _daddr4)[0] = htonl(((uint32_t *) _daddr4)[0]);
- else
- for (int w = 0; w < 4; w++)
+ } else {
+ for (int w = 0; w < 4; w++) {
((uint32_t *) _daddr6)[w] = htonl(((uint32_t *) _daddr6)[w]);
-
+ }
+ }
if (_sport == sport) {
uid = u;
if (_dport == dport &&
@@ -374,13 +383,15 @@ jint get_uid_sub(const int version, const int protocol,
break;
}
}
- } else
+ } else {
log_android(ANDROID_LOG_ERROR, "Invalid field #%d: %s", fields, line);
+ }
}
}
- if (fclose(fd))
+ if (fclose(fd)) {
log_android(ANDROID_LOG_ERROR, "fclose %s error %d: %s", fn, errno, strerror(errno));
+ }
#ifdef PROFILE_UID
gettimeofday(&end, NULL);
diff --git a/android_app/app/src/main/cpp/session.c b/android_app/app/src/main/cpp/session.c
index 0f5088fa..eccb2b74 100644
--- a/android_app/app/src/main/cpp/session.c
+++ b/android_app/app/src/main/cpp/session.c
@@ -15,11 +15,13 @@ void init(const struct arguments *args) {
void clear() {
struct ng_session *s = ng_session;
while (s != NULL) {
- if (s->socket >= 0 && close(s->socket))
+ if (s->socket >= 0 && close(s->socket)) {
log_android(ANDROID_LOG_ERROR, "close %d error %d: %s",
s->socket, errno, strerror(errno));
- if (s->protocol == IPPROTO_TCP)
+ }
+ if (s->protocol == IPPROTO_TCP) {
clear_tcp_data(&s->tcp);
+ }
struct ng_session *p = s;
s = s->next;
free(p);
@@ -43,14 +45,16 @@ void *handle_events(void *a) {
// Get max sessions
int maxsessions = 1024;
struct rlimit rlim;
- if (getrlimit(RLIMIT_NOFILE, &rlim))
+ if (getrlimit(RLIMIT_NOFILE, &rlim)) {
log_android(ANDROID_LOG_WARN, "getrlimit error %d: %s", errno, strerror(errno));
- else
+ } else {
log_android(ANDROID_LOG_WARN, "getrlimit soft %d hard %d max sessions %d",
rlim.rlim_cur, rlim.rlim_max, maxsessions);
+ }
maxsessions = (int) (rlim.rlim_cur * SESSION_LIMIT / 100);
- if (maxsessions > 1000)
+ if (maxsessions > 1000) {
maxsessions = 1000;
+ }
// Terminate existing sessions not allowed anymore
check_allowed(args);
@@ -99,16 +103,20 @@ void *handle_events(void *a) {
struct ng_session *s = ng_session;
while (s != NULL) {
if (s->protocol == IPPROTO_ICMP || s->protocol == IPPROTO_ICMPV6) {
- if (!s->icmp.stop)
+ if (!s->icmp.stop) {
isessions++;
+ }
} else if (s->protocol == IPPROTO_UDP) {
- if (s->udp.state == UDP_ACTIVE)
+ if (s->udp.state == UDP_ACTIVE) {
usessions++;
+ }
} else if (s->protocol == IPPROTO_TCP) {
- if (s->tcp.state != TCP_CLOSING && s->tcp.state != TCP_CLOSE)
+ if (s->tcp.state != TCP_CLOSING && s->tcp.state != TCP_CLOSE) {
tsessions++;
- if (s->socket >= 0)
+ }
+ if (s->socket >= 0) {
recheck = recheck | monitor_tcp_session(args, s, epoll_fd);
+ }
}
s = s->next;
}
@@ -129,37 +137,43 @@ void *handle_events(void *a) {
if (!s->icmp.stop && !del) {
int stimeout = s->icmp.time +
get_icmp_timeout(&s->icmp, sessions, maxsessions) - now + 1;
- if (stimeout > 0 && stimeout < timeout)
+ if (stimeout > 0 && stimeout < timeout) {
timeout = stimeout;
+ }
}
} else if (s->protocol == IPPROTO_UDP) {
del = check_udp_session(args, s, sessions, maxsessions);
if (s->udp.state == UDP_ACTIVE && !del) {
int stimeout = s->udp.time +
get_udp_timeout(&s->udp, sessions, maxsessions) - now + 1;
- if (stimeout > 0 && stimeout < timeout)
+ if (stimeout > 0 && stimeout < timeout) {
timeout = stimeout;
+ }
}
} else if (s->protocol == IPPROTO_TCP) {
del = check_tcp_session(args, s, sessions, maxsessions);
if (s->tcp.state != TCP_CLOSING && s->tcp.state != TCP_CLOSE && !del) {
int stimeout = s->tcp.time +
get_tcp_timeout(&s->tcp, sessions, maxsessions) - now + 1;
- if (stimeout > 0 && stimeout < timeout)
+ if (stimeout > 0 && stimeout < timeout) {
timeout = stimeout;
+ }
}
}
if (del) {
- if (sl == NULL)
+ if (sl == NULL) {
ng_session = s->next;
- else
+ }
+ else {
sl->next = s->next;
+ }
struct ng_session *c = s;
s = s->next;
- if (c->protocol == IPPROTO_TCP)
+ if (c->protocol == IPPROTO_TCP) {
clear_tcp_data(&c->tcp);
+ }
free(c);
} else {
sl = s;
@@ -193,12 +207,14 @@ void *handle_events(void *a) {
}
}
- if (ready == 0)
+ if (ready == 0) {
log_android(ANDROID_LOG_DEBUG, "epoll timeout");
+ }
else {
- if (pthread_mutex_lock(&lock))
+ if (pthread_mutex_lock(&lock)) {
log_android(ANDROID_LOG_ERROR, "pthread_mutex_lock failed");
+ }
int error = 0;
@@ -207,11 +223,13 @@ void *handle_events(void *a) {
// Check pipe
stopping = 1;
uint8_t buffer[1];
- if (read(pipefds[0], buffer, 1) < 0)
+ if (read(pipefds[0], buffer, 1) < 0) {
log_android(ANDROID_LOG_WARN, "Read pipe error %d: %s",
errno, strerror(errno));
- else
+ }
+ else {
log_android(ANDROID_LOG_WARN, "Read pipe");
+ }
break;
} else if (ev[i].data.ptr == NULL) {
@@ -223,9 +241,11 @@ void *handle_events(void *a) {
(ev[i].events & EPOLLERR) != 0,
(ev[i].events & EPOLLHUP) != 0);
- while (!error && is_readable(args->tun))
- if (check_tun(args, &ev[i], epoll_fd, sessions, maxsessions) < 0)
+ while (!error && is_readable(args->tun)) {
+ if (check_tun(args, &ev[i], epoll_fd, sessions, maxsessions) < 0) {
error = 1;
+ }
+ }
} else {
// Check downstream
@@ -241,22 +261,26 @@ void *handle_events(void *a) {
struct ng_session *session = (struct ng_session *) ev[i].data.ptr;
if (session->protocol == IPPROTO_ICMP ||
- session->protocol == IPPROTO_ICMPV6)
+ session->protocol == IPPROTO_ICMPV6) {
check_icmp_socket(args, &ev[i]);
+ }
else if (session->protocol == IPPROTO_UDP) {
while (!(ev[i].events & EPOLLERR) && (ev[i].events & EPOLLIN) &&
- is_readable(session->socket))
+ is_readable(session->socket)) {
check_udp_socket(args, &ev[i]);
- } else if (session->protocol == IPPROTO_TCP)
+ }
+ } else if (session->protocol == IPPROTO_TCP) {
check_tcp_socket(args, &ev[i], epoll_fd);
+ }
}
if (error)
break;
}
- if (pthread_mutex_unlock(&lock))
+ if (pthread_mutex_unlock(&lock)) {
log_android(ANDROID_LOG_ERROR, "pthread_mutex_unlock failed");
+ }
if (error)
break;
@@ -264,16 +288,18 @@ void *handle_events(void *a) {
}
// Close epoll file
- if (epoll_fd >= 0 && close(epoll_fd))
+ if (epoll_fd >= 0 && close(epoll_fd)) {
log_android(ANDROID_LOG_ERROR,
"epoll close error %d: %s", errno, strerror(errno));
+ }
(*env)->DeleteGlobalRef(env, args->instance);
// Detach from Java
rs = (*jvm)->DetachCurrentThread(jvm);
- if (rs != JNI_OK)
+ if (rs != JNI_OK) {
log_android(ANDROID_LOG_ERROR, "DetachCurrentThread failed");
+ }
// Cleanup
free(args);
@@ -300,7 +326,6 @@ void check_allowed(const struct arguments *args) {
inet_ntop(AF_INET6, &s->icmp.daddr.ip6, dest, sizeof(dest));
}
-
s->icmp.stop = 1;
log_android(ANDROID_LOG_WARN, "ICMP terminate %d uid %d",
s->socket, s->icmp.uid);
@@ -322,10 +347,11 @@ void check_allowed(const struct arguments *args) {
} else if (s->udp.state == UDP_BLOCKED) {
log_android(ANDROID_LOG_WARN, "UDP remove blocked session uid %d", s->udp.uid);
- if (l == NULL)
+ if (l == NULL) {
ng_session = s->next;
- else
+ } else {
l->next = s->next;
+ }
struct ng_session *c = s;
s = s->next;
diff --git a/android_app/app/src/main/cpp/tcp.c b/android_app/app/src/main/cpp/tcp.c
index c7b78379..358f8278 100644
--- a/android_app/app/src/main/cpp/tcp.c
+++ b/android_app/app/src/main/cpp/tcp.c
@@ -4,6 +4,27 @@
extern struct ng_session *ng_session;
+static int should_redirect(const struct arguments *args, int port) {
+ if (port == args->proxyPort) return 0;
+ if (port == 53 && !args->fwd53) return 0;
+ return 1;
+}
+
+static int is_http_payload(const uint8_t *data, size_t len) {
+ if (len < 10) return 0;
+
+ if (data[0] < 'A' || data[0] > 'Z') return 0;
+ if (len >= strlen("CONNECT ") && memcmp(data, "CONNECT ", 8) == 0) return 0;
+
+ for (size_t i = 3; i < len - 8; i++) {
+ if (data[i] == '\r' || data[i] == '\n') break;
+ if (memcmp(data + i, " HTTP/1.", 8) == 0) {
+ return 1;
+ }
+ }
+ return 0;
+}
+
void clear_tcp_data(struct tcp_session *cur) {
struct segment *s = cur->forward;
while (s != NULL) {
@@ -16,12 +37,13 @@ void clear_tcp_data(struct tcp_session *cur) {
int get_tcp_timeout(const struct tcp_session *t, int sessions, int maxsessions) {
int timeout;
- if (t->state == TCP_LISTEN || t->state == TCP_SYN_RECV)
+ if (t->state == TCP_LISTEN || t->state == TCP_SYN_RECV) {
timeout = TCP_INIT_TIMEOUT;
- else if (t->state == TCP_ESTABLISHED)
+ } else if (t->state == TCP_ESTABLISHED) {
timeout = TCP_IDLE_TIMEOUT;
- else
+ } else {
timeout = TCP_CLOSE_TIMEOUT;
+ }
int scale = 100 - sessions * 100 / maxsessions;
timeout = timeout * scale / 100;
@@ -53,21 +75,23 @@ int check_tcp_session(const struct arguments *args, struct ng_session *s,
// Check session timeout
if (s->tcp.state != TCP_CLOSING && s->tcp.state != TCP_CLOSE &&
s->tcp.time + timeout < now) {
- if (s->tcp.state == TCP_LISTEN)
+ if (s->tcp.state == TCP_LISTEN) {
s->tcp.state = TCP_CLOSING;
- else
+ } else {
write_rst(args, &s->tcp);
+ }
}
// Check closing sessions
if (s->tcp.state == TCP_CLOSING) {
// eof closes socket
if (s->socket >= 0) {
- if (close(s->socket))
+ if (close(s->socket)) {
log_android(ANDROID_LOG_ERROR, "%s close error %d: %s",
session, errno, strerror(errno));
- else
+ } else {
log_android(ANDROID_LOG_WARN, "%s close", session);
+ }
s->socket = -1;
}
@@ -96,16 +120,17 @@ int monitor_tcp_session(const struct arguments *args, struct ng_session *s, int
int rport = htons(s->tcp.dest);
// Check for connected = writable
- if (s->tcp.connect_sent == TCP_CONNECT_SENT && rport == 443)
+ if (s->tcp.connect_sent == TCP_CONNECT_SENT && rport != 80) {
events = events | EPOLLIN;
- else
+ } else {
events = events | EPOLLOUT;
+ }
} else if (s->tcp.state == TCP_ESTABLISHED || s->tcp.state == TCP_CLOSE_WAIT) {
// Check for incoming data
- if (get_send_window(&s->tcp) > 0)
+ if (get_send_window(&s->tcp) > 0) {
events = events | EPOLLIN;
- else {
+ } else {
recheck = 1;
long long ms = get_ms();
@@ -122,10 +147,11 @@ int monitor_tcp_session(const struct arguments *args, struct ng_session *s, int
if (s->tcp.forward != NULL) {
uint32_t buffer_size = (uint32_t) get_receive_buffer(s);
if (s->tcp.forward->seq + s->tcp.forward->sent == s->tcp.remote_seq &&
- s->tcp.forward->len - s->tcp.forward->sent < buffer_size)
+ s->tcp.forward->len - s->tcp.forward->sent < buffer_size) {
events = events | EPOLLOUT;
- else
+ } else {
recheck = 1;
+ }
}
}
@@ -134,9 +160,10 @@ int monitor_tcp_session(const struct arguments *args, struct ng_session *s, int
if (epoll_ctl(epoll_fd, EPOLL_CTL_MOD, s->socket, &s->ev)) {
s->tcp.state = TCP_CLOSING;
log_android(ANDROID_LOG_ERROR, "epoll mod tcp error %d: %s", errno, strerror(errno));
- } else
+ } else {
log_android(ANDROID_LOG_DEBUG, "epoll mod tcp socket %d in %d out %d",
s->socket, (events & EPOLLIN) != 0, (events & EPOLLOUT) != 0);
+ }
}
return recheck;
@@ -150,23 +177,27 @@ uint32_t get_send_window(const struct tcp_session *cur) {
}
int get_receive_buffer(const struct ng_session *cur) {
- if (cur->socket < 0)
+ if (cur->socket < 0) {
return 0;
+ }
// Get send buffer size
// /proc/sys/net/core/wmem_default
int sendbuf = 0;
int sendbufsize = sizeof(sendbuf);
- if (getsockopt(cur->socket, SOL_SOCKET, SO_SNDBUF, &sendbuf, &sendbufsize) < 0)
+ if (getsockopt(cur->socket, SOL_SOCKET, SO_SNDBUF, &sendbuf, &sendbufsize) < 0) {
log_android(ANDROID_LOG_WARN, "getsockopt SO_RCVBUF %d: %s", errno, strerror(errno));
+ }
- if (sendbuf == 0)
+ if (sendbuf == 0) {
sendbuf = 16384; // Safe default
+ }
// Get unsent data size
int unsent = 0;
- if (ioctl(cur->socket, SIOCOUTQ, &unsent))
+ if (ioctl(cur->socket, SIOCOUTQ, &unsent)) {
log_android(ANDROID_LOG_WARN, "ioctl SIOCOUTQ %d: %s", errno, strerror(errno));
+ }
return (unsent < sendbuf / 2 ? sendbuf / 2 - unsent : 0);
}
@@ -183,12 +214,14 @@ uint32_t get_receive_window(const struct ng_session *cur) {
uint32_t window = (uint32_t) get_receive_buffer(cur);
uint32_t max = ((uint32_t) 0xFFFF) << cur->tcp.recv_scale;
- if (window > max)
+ if (window > max) {
window = max;
+ }
window = (toforward < window ? window - toforward : 0);
- if ((window >> cur->tcp.recv_scale) == 0)
+ if ((window >> cur->tcp.recv_scale) == 0) {
window = 0;
+ }
return window;
}
@@ -225,25 +258,27 @@ void check_tcp_socket(const struct arguments *args,
int serr = 0;
socklen_t optlen = sizeof(int);
int err = getsockopt(s->socket, SOL_SOCKET, SO_ERROR, &serr, &optlen);
- if (err < 0)
+ if (err < 0) {
log_android(ANDROID_LOG_ERROR, "%s getsockopt error %d: %s",
session, errno, strerror(errno));
- else if (serr)
+ } else if (serr) {
log_android(ANDROID_LOG_ERROR, "%s SO_ERROR %d: %s",
session, serr, strerror(serr));
+ }
write_rst(args, &s->tcp);
// Connection refused
- if (0)
+ if (0) {
if (err >= 0 && (serr == ECONNREFUSED || serr == EHOSTUNREACH)) {
struct icmp icmp;
memset(&icmp, 0, sizeof(struct icmp));
icmp.icmp_type = ICMP_UNREACH;
- if (serr == ECONNREFUSED)
+ if (serr == ECONNREFUSED) {
icmp.icmp_code = ICMP_UNREACH_PORT;
- else
+ } else {
icmp.icmp_code = ICMP_UNREACH_HOST;
+ }
icmp.icmp_cksum = 0;
icmp.icmp_cksum = ~calc_checksum(0, &icmp, 4);
@@ -257,30 +292,77 @@ void check_tcp_socket(const struct arguments *args,
memcpy(&sicmp.saddr.ip6, &s->tcp.saddr.ip6, 16);
memcpy(&sicmp.daddr.ip6, &s->tcp.daddr.ip6, 16);
}
-
write_icmp(args, &sicmp, &icmp, 8);
}
+ }
} else {
// Assume socket okay
if (s->tcp.state == TCP_LISTEN) {
// Check socket connect
if (ev->events & EPOLLIN) {
uint8_t buffer[512];
- ssize_t bytes = recv(s->socket, buffer, 12, 0);
- if (bytes < 0) {
- log_android(ANDROID_LOG_ERROR, "%s recv SOCKS5 error %d: %s",
+ ssize_t bytes = recv(s->socket, buffer, sizeof(buffer), 0);
+ if (bytes <= 0) {
+ log_android(ANDROID_LOG_ERROR, "%s recv error %d: %s",
session, errno, strerror(errno));
write_rst(args, &s->tcp);
+ } else if (args->isSocks5) {
+ if (s->tcp.socks5_state == SOCKS5_STATE_GREETING) {
+ if (bytes >= 2 && buffer[0] == 0x05 && buffer[1] == 0x00) {
+ uint8_t request[1024];
+ int reqlen = 0;
+ request[reqlen++] = 0x05; // VER
+ request[reqlen++] = 0x01; // CMD
+ request[reqlen++] = 0x00; // RSV
+ if (s->tcp.hostname[0] != 0) {
+ request[reqlen++] = 0x03; // ATYP: DOMAINNAME
+ uint8_t hostlen = (uint8_t) strlen(s->tcp.hostname);
+ request[reqlen++] = hostlen;
+ memcpy(request + reqlen, s->tcp.hostname, hostlen);
+ reqlen += hostlen;
+ } else {
+ request[reqlen++] = 0x01; // ATYP: IPv4
+ memcpy(request + reqlen, &s->tcp.daddr.ip4, 4);
+ reqlen += 4;
+ }
+ memcpy(request + reqlen, &s->tcp.dest, 2);
+ reqlen += 2;
+
+ if (send(s->socket, request, (size_t) reqlen, MSG_NOSIGNAL) != reqlen) {
+ log_android(ANDROID_LOG_ERROR,
+ "%s SOCKS5 connect send error %d: %s",
+ session, errno, strerror(errno));
+ write_rst(args, &s->tcp);
+ } else {
+ s->tcp.socks5_state = SOCKS5_STATE_CONNECT;
+ }
+ } else {
+ log_android(ANDROID_LOG_ERROR, "%s SOCKS5 greeting failed", session);
+ write_rst(args, &s->tcp);
+ }
+ } else if (s->tcp.socks5_state == SOCKS5_STATE_CONNECT) {
+ if (bytes >= 10 && buffer[0] == 0x05 && buffer[1] == 0x00) {
+ s->tcp.connect_sent = TCP_CONNECT_ESTABLISHED;
+ s->tcp.state = TCP_SYN_RECV;
+ } else {
+ log_android(ANDROID_LOG_ERROR, "%s SOCKS5 connect failed", session);
+ write_rst(args, &s->tcp);
+ }
+ }
} else {
if (s->tcp.connect_sent == TCP_CONNECT_SENT) {
buffer[bytes] = '\0';
- if (strcmp(buffer, "HTTP/1.0 200") == 0 || strcmp(buffer, "HTTP/1.1 200") == 0) {
+ if (strstr((char *) buffer, "HTTP/1.0 200") != NULL ||
+ strstr((char *) buffer, "HTTP/1.1 200") != NULL) {
s->tcp.connect_sent = TCP_CONNECT_ESTABLISHED;
- while (recv(s->socket, buffer, sizeof(buffer), 0) > 0) {}
s->tcp.state = TCP_SYN_RECV;
} else {
+ log_android(ANDROID_LOG_ERROR, "%s HTTP CONNECT failed: %s", session,
+ buffer);
write_rst(args, &s->tcp);
}
+ } else {
+ write_rst(args, &s->tcp);
}
}
} else {
@@ -310,7 +392,7 @@ void check_tcp_socket(const struct arguments *args,
size_t len = s->tcp.forward->len - s->tcp.forward->sent;
size_t newlen = len;
uint8_t *new_data = 0;
- if (htons(s->tcp.dest) == 80) {
+ if (!args->isSocks5 && s->tcp.is_http) {
new_data = patch_http_url(data, &newlen);
if (new_data) {
data = new_data;
@@ -373,9 +455,10 @@ void check_tcp_socket(const struct arguments *args,
uint32_t window = get_receive_window(s);
uint32_t prev = s->tcp.recv_window;
s->tcp.recv_window = window;
- if ((prev == 0 && window > 0) || (prev > 0 && window == 0))
+ if ((prev == 0 && window > 0) || (prev > 0 && window == 0)) {
log_android(ANDROID_LOG_WARN, "%s recv window %u > %u",
session, prev, window);
+ }
// Acknowledge forwarded data
if (fwd || (prev == 0 && window > 0)) {
@@ -383,8 +466,9 @@ void check_tcp_socket(const struct arguments *args,
log_android(ANDROID_LOG_WARN, "%s confirm FIN", session);
s->tcp.remote_seq++; // remote FIN
}
- if (write_ack(args, &s->tcp) >= 0)
+ if (write_ack(args, &s->tcp) >= 0) {
s->tcp.time = time(NULL);
+ }
}
if (s->tcp.state == TCP_ESTABLISHED || s->tcp.state == TCP_CLOSE_WAIT) {
@@ -404,8 +488,9 @@ void check_tcp_socket(const struct arguments *args,
log_android(ANDROID_LOG_ERROR, "%s recv error %d: %s",
session, errno, strerror(errno));
- if (errno != EINTR && errno != EAGAIN)
+ if (errno != EINTR && errno != EAGAIN) {
write_rst(args, &s->tcp);
+ }
} else if (bytes == 0) {
log_android(ANDROID_LOG_WARN, "%s recv eof", session);
@@ -415,21 +500,23 @@ void check_tcp_socket(const struct arguments *args,
s->tcp.local_seq++; // local FIN
}
- if (s->tcp.state == TCP_ESTABLISHED)
+ if (s->tcp.state == TCP_ESTABLISHED) {
s->tcp.state = TCP_FIN_WAIT1;
- else if (s->tcp.state == TCP_CLOSE_WAIT)
+ } else if (s->tcp.state == TCP_CLOSE_WAIT) {
s->tcp.state = TCP_LAST_ACK;
- else
+ } else {
log_android(ANDROID_LOG_ERROR, "%s invalid close", session);
+ }
} else {
// There was still data to send
log_android(ANDROID_LOG_ERROR, "%s close with queue", session);
write_rst(args, &s->tcp);
}
- if (close(s->socket))
+ if (close(s->socket)) {
log_android(ANDROID_LOG_ERROR, "%s close error %d: %s",
session, errno, strerror(errno));
+ }
s->socket = -1;
} else {
@@ -438,8 +525,9 @@ void check_tcp_socket(const struct arguments *args,
s->tcp.received += bytes;
// Forward to tun
- if (write_data(args, &s->tcp, buffer, (size_t) bytes) >= 0)
+ if (write_data(args, &s->tcp, buffer, (size_t) bytes) >= 0) {
s->tcp.local_seq += bytes;
+ }
}
free(buffer);
}
@@ -461,28 +549,20 @@ static void lookup_hostname(struct sockaddr_in *addr, char *hostname, int hostle
if (needed)
{
if ((host = gethostbyaddr((char *)&addr->sin_addr,
- sizeof(addr->sin_addr), AF_INET)) != NULL)
- {
+ sizeof(addr->sin_addr), AF_INET)) != NULL) {
strncpy(hostname, host->h_name, hostlen);
hostname[hostlen - 1] = '\0';
- }
- else
- {
+ } else {
strncpy(hostname, inet_ntoa(addr->sin_addr), hostlen);
hostname[hostlen - 1] = '\0';
}
- }
- else
- {
+ } else{
# ifdef USELESS_DNS_LOOKUPS
if ((host = gethostbyaddr((char *)&addr->sin_addr,
- sizeof(addr->sin_addr), AF_INET)) != NULL)
- {
+ sizeof(addr->sin_addr), AF_INET)) != NULL) {
strncpy(hostname, host->h_name, hostlen);
hostname[hostlen - 1] = '\0';
- }
- else
- {
+ } else {
strncpy(hostname, inet_ntoa(addr->sin_addr), hostlen);
hostname[hostlen - 1] = '\0';
}
@@ -504,8 +584,6 @@ jboolean handle_tcp(const struct arguments *args,
int uid,
const int epoll_fd) {
-
-
// Get headers
const uint8_t version = (*pkt) >> 4;
const struct iphdr *ip4 = (struct iphdr *) pkt;
@@ -536,8 +614,9 @@ jboolean handle_tcp(const struct arguments *args,
(version == 4 ? cur->tcp.saddr.ip4 == ip4->saddr &&
cur->tcp.daddr.ip4 == ip4->daddr
: memcmp(&cur->tcp.saddr.ip6, &ip6->ip6_src, 16) == 0 &&
- memcmp(&cur->tcp.daddr.ip6, &ip6->ip6_dst, 16) == 0)))
+ memcmp(&cur->tcp.daddr.ip6, &ip6->ip6_dst, 16) == 0))) {
cur = cur->next;
+ }
// Prepare logging
@@ -591,18 +670,18 @@ jboolean handle_tcp(const struct arguments *args,
uint16_t mss = get_default_mss(version);
uint8_t ws = 0;
int optlen = tcpoptlen;
- uint8_t *options = tcpoptions;
+ const uint8_t *options = tcpoptions;
while (optlen > 0) {
uint8_t kind = *options;
uint8_t len = *(options + 1);
- if (kind == 0) // End of options list
+ if (kind == 0) { // End of options list
break;
-
- if (kind == 2 && len == 4)
+ }
+ if (kind == 2 && len == 4) {
mss = ntohs(*((uint16_t *) (options + 2)));
-
- else if (kind == 3 && len == 3)
+ } else if (kind == 3 && len == 3) {
ws = *(options + 2);
+ }
if (kind == 1) {
optlen--;
@@ -636,9 +715,8 @@ jboolean handle_tcp(const struct arguments *args,
s->tcp.sent = 0;
s->tcp.received = 0;
s->tcp.connect_sent = TCP_CONNECT_NOT_SENT;
- if (rport == 80) {
- s->tcp.connect_sent = TCP_CONNECT_ESTABLISHED;
- }
+ s->tcp.is_http = 0;
+ s->tcp.is_tls = 0;
if (version == 4) {
s->tcp.saddr.ip4 = (__be32) ip4->saddr;
@@ -715,10 +793,16 @@ jboolean handle_tcp(const struct arguments *args,
goto free;
}
} else {
- if (rport == 443) {
+ int rport = htons(cur->tcp.dest);
+ int is_tls = (len > 0) || (datalen > 0 && data[0] == 0x16);
+ int is_http = is_http_payload(data, datalen);
+ cur->tcp.is_http = is_http;
+ cur->tcp.is_tls = is_tls;
+
+ if (should_redirect(args, rport)) {
if (len > 0) {
strcpy(cur->tcp.hostname, hostname);
- } else {
+ } else if (cur->tcp.hostname[0] == 0) {
struct sockaddr_in addr4;
addr4.sin_family = AF_INET;
addr4.sin_addr.s_addr = (__be32) cur->tcp.daddr.ip4;
@@ -729,22 +813,39 @@ jboolean handle_tcp(const struct arguments *args,
strcpy(cur->tcp.hostname, hostname);
}
}
- if (cur->tcp.connect_sent == TCP_CONNECT_NOT_SENT) {
- if (len > 0) {
- uint8_t buffer[512];
- sprintf(buffer, "CONNECT %s:443 HTTP/1.0\r\n\r\n", cur->tcp.hostname);
- ssize_t sent = send(cur->socket, buffer, strlen(buffer), MSG_NOSIGNAL);
+ if (cur->tcp.connect_sent == TCP_CONNECT_NOT_SENT && datalen > 0) {
+ if (args->isSocks5) {
+ uint8_t greeting[] = {0x05, 0x01, 0x00};
+ ssize_t sent = send(cur->socket, greeting, sizeof(greeting), MSG_NOSIGNAL);
if (sent < 0) {
+ log_android(ANDROID_LOG_ERROR, "%s SOCKS5 greeting send error %d: %s",
+ packet, errno, strerror(errno));
write_rst(args, &cur->tcp);
} else {
cur->tcp.connect_sent = TCP_CONNECT_SENT;
+ cur->tcp.socks5_state = SOCKS5_STATE_GREETING;
cur->tcp.state = TCP_LISTEN;
}
+ } else if (is_tls || rport == 443 || (!is_http && rport != 80)) {
+ if (cur->tcp.hostname[0] != 0) {
+ char buffer[512];
+ sprintf(buffer, "CONNECT %s:%d HTTP/1.0\r\n\r\n", cur->tcp.hostname, rport);
+
+ ssize_t sent = send(cur->socket, buffer, strlen(buffer), MSG_NOSIGNAL);
+ if (sent < 0) {
+ write_rst(args, &cur->tcp);
+ } else {
+ cur->tcp.connect_sent = TCP_CONNECT_SENT;
+ cur->tcp.state = TCP_LISTEN;
+ }
+ }
+ } else {
+ cur->tcp.connect_sent = TCP_CONNECT_ESTABLISHED;
}
}
}
- if (rport == 443 && cur->tcp.connect_sent != TCP_CONNECT_ESTABLISHED) {
+ if (should_redirect(args, rport) && cur->tcp.connect_sent != TCP_CONNECT_ESTABLISHED) {
char session[250];
sprintf(session,
"%s %s loc %u rem %u acked %u",
@@ -818,8 +919,9 @@ jboolean handle_tcp(const struct arguments *args,
cur->tcp.remote_seq++; // remote FIN
if (write_ack(args, &cur->tcp) >= 0)
cur->tcp.state = TCP_CLOSE_WAIT;
- } else
+ } else {
cur->tcp.state = TCP_CLOSE_WAIT;
+ }
} else if (cur->tcp.state == TCP_CLOSE_WAIT) {
log_android(ANDROID_LOG_WARN, "%s repeated FIN", session);
// The socket is probably not closed yet
@@ -836,15 +938,13 @@ jboolean handle_tcp(const struct arguments *args,
} else if (tcphdr->ack) {
cur->tcp.acked = ntohl(tcphdr->ack_seq);
- if (cur->tcp.state == TCP_SYN_RECV)
+ if (cur->tcp.state == TCP_SYN_RECV) {
cur->tcp.state = TCP_ESTABLISHED;
-
- else if (cur->tcp.state == TCP_ESTABLISHED) {
+ } else if (cur->tcp.state == TCP_ESTABLISHED) {
// Do nothing
- } else if (cur->tcp.state == TCP_LAST_ACK)
+ } else if (cur->tcp.state == TCP_LAST_ACK) {
cur->tcp.state = TCP_CLOSING;
-
- else if (cur->tcp.state == TCP_CLOSE_WAIT) {
+ } else if (cur->tcp.state == TCP_CLOSE_WAIT) {
// ACK after FIN/ACK
} else if (cur->tcp.state == TCP_FIN_WAIT1) {
// Do nothing
@@ -862,24 +962,27 @@ jboolean handle_tcp(const struct arguments *args,
// Keep alive
if (cur->tcp.state == TCP_ESTABLISHED) {
int on = 1;
- if (setsockopt(cur->socket, SOL_SOCKET, SO_KEEPALIVE, &on, sizeof(on)))
+ if (setsockopt(cur->socket, SOL_SOCKET, SO_KEEPALIVE, &on,
+ sizeof(on))) {
log_android(ANDROID_LOG_ERROR,
"%s setsockopt SO_KEEPALIVE error %d: %s",
session, errno, strerror(errno));
- else
+ } else {
log_android(ANDROID_LOG_WARN, "%s enabled keep alive", session);
- } else
+ }
+ } else {
log_android(ANDROID_LOG_WARN, "%s keep alive", session);
+ }
} else if (compare_u32(ack, cur->tcp.local_seq) < 0) {
- if (compare_u32(ack, cur->tcp.acked) <= 0)
+ if (compare_u32(ack, cur->tcp.acked) <= 0) {
log_android(
ack == cur->tcp.acked ? ANDROID_LOG_WARN : ANDROID_LOG_ERROR,
"%s repeated ACK %u/%u",
session,
ack - cur->tcp.local_start,
cur->tcp.acked - cur->tcp.local_start);
- else {
+ } else {
log_android(ANDROID_LOG_WARN, "%s previous ACK %u",
session, ack - cur->tcp.local_seq);
cur->tcp.acked = ack;
@@ -896,12 +999,13 @@ jboolean handle_tcp(const struct arguments *args,
if (cur->tcp.state != oldstate ||
cur->tcp.local_seq != oldlocal ||
- cur->tcp.remote_seq != oldremote)
+ cur->tcp.remote_seq != oldremote) {
log_android(ANDROID_LOG_INFO, "%s > %s loc %u rem %u",
session,
strstate(cur->tcp.state),
cur->tcp.local_seq - cur->tcp.local_start,
cur->tcp.remote_seq - cur->tcp.remote_start);
+ }
}
}
free:
@@ -939,16 +1043,17 @@ void queue_tcp(const struct arguments *args,
memcpy(n->data, data, datalen);
n->next = s;
- if (p == NULL)
+ if (p == NULL) {
cur->forward = n;
- else
+ } else {
p->next = n;
+ }
} else if (s != NULL && s->seq == seq) {
- if (s->len == datalen)
+ if (s->len == datalen) {
log_android(ANDROID_LOG_WARN, "%s segment already queued %u..%u",
session,
s->seq - cur->remote_start, s->seq + s->len - cur->remote_start);
- else if (s->len < datalen) {
+ } else if (s->len < datalen) {
log_android(ANDROID_LOG_WARN, "%s segment smaller %u..%u > %u",
session,
s->seq - cur->remote_start, s->seq + s->len - cur->remote_start,
@@ -956,11 +1061,13 @@ void queue_tcp(const struct arguments *args,
free(s->data);
s->data = malloc(datalen);
memcpy(s->data, data, datalen);
- } else
+ s->len = datalen;
+ } else {
log_android(ANDROID_LOG_ERROR, "%s segment larger %u..%u < %u",
session,
s->seq - cur->remote_start, s->seq + s->len - cur->remote_start,
s->seq + datalen - cur->remote_start);
+ }
}
}
}
@@ -971,14 +1078,15 @@ int open_tcp_socket(const struct arguments *args,
int version;
int rport = htons(cur->dest);
- if (rport != 80 && rport != 443) {
+ if (!should_redirect(args, rport)) {
redirect = NULL;
}
if (redirect == NULL) {
version = cur->version;
- } else
+ } else {
version = (strstr(redirect->raddr, ":") == NULL ? 4 : 6);
+ }
// Get TCP socket
if ((sock = socket(version == 4 ? PF_INET : PF_INET6, SOCK_STREAM, 0)) < 0) {
@@ -1081,8 +1189,9 @@ void write_rst(const struct arguments *args, struct tcp_session *cur) {
cur->remote_seq++; // SYN
}
write_tcp(args, cur, NULL, 0, 0, ack, 0, 1);
- if (cur->state != TCP_CLOSE)
+ if (cur->state != TCP_CLOSE) {
cur->state = TCP_CLOSING;
+ }
}
ssize_t write_tcp(const struct arguments *args, const struct tcp_session *cur,
@@ -1104,8 +1213,9 @@ ssize_t write_tcp(const struct arguments *args, const struct tcp_session *cur,
struct iphdr *ip4 = (struct iphdr *) buffer;
tcp = (struct tcphdr *) (buffer + sizeof(struct iphdr));
options = buffer + sizeof(struct iphdr) + sizeof(struct tcphdr);
- if (datalen)
+ if (datalen) {
memcpy(buffer + sizeof(struct iphdr) + sizeof(struct tcphdr) + optlen, data, datalen);
+ }
// Build IP4 header
memset(ip4, 0, sizeof(struct iphdr));
@@ -1135,8 +1245,9 @@ ssize_t write_tcp(const struct arguments *args, const struct tcp_session *cur,
struct ip6_hdr *ip6 = (struct ip6_hdr *) buffer;
tcp = (struct tcphdr *) (buffer + sizeof(struct ip6_hdr));
options = buffer + sizeof(struct ip6_hdr) + sizeof(struct tcphdr);
- if (datalen)
+ if (datalen) {
memcpy(buffer + sizeof(struct ip6_hdr) + sizeof(struct tcphdr) + optlen, data, datalen);
+ }
// Build IP6 header
memset(ip6, 0, sizeof(struct ip6_hdr));
@@ -1172,8 +1283,9 @@ ssize_t write_tcp(const struct arguments *args, const struct tcp_session *cur,
tcp->rst = (__u16) rst;
tcp->window = htons(cur->recv_window >> cur->recv_scale);
- if (!tcp->ack)
+ if (!tcp->ack) {
tcp->ack_seq = 0;
+ }
// TCP options
if (syn) {
diff --git a/android_app/app/src/main/cpp/tls.c b/android_app/app/src/main/cpp/tls.c
index 83a45dff..c0127551 100644
--- a/android_app/app/src/main/cpp/tls.c
+++ b/android_app/app/src/main/cpp/tls.c
@@ -38,8 +38,9 @@ static void parse_server_name_extension(const char *data, size_t data_len,
size_t len = ((unsigned char)data[pos + 1] << 8) +
(unsigned char)data[pos + 2];
- if (pos + 3 + len > data_len)
+ if (pos + 3 + len > data_len) {
return;
+ }
switch (data[pos]) { /* name type */
case 0x00: /* host_name */
@@ -69,8 +70,9 @@ static void parse_extensions(const char *data, size_t data_len, char *hostname)
* so we break our state and move p to beinnging
* of the extension here
*/
- if (pos + 4 + len > data_len)
+ if (pos + 4 + len > data_len) {
return;
+ }
parse_server_name_extension(data + pos + 4, len,
hostname);
return;
@@ -96,8 +98,9 @@ void parse_tls_header(const char *data, size_t data_len, char *hostname)
* Check that our TCP payload is at least large enough for a
* TLS header
*/
- if (data_len < TLS_HEADER_LEN)
+ if (data_len < TLS_HEADER_LEN) {
return;
+ }
/*
* SSL 2.0 compatible Client Hello
@@ -127,12 +130,14 @@ void parse_tls_header(const char *data, size_t data_len, char *hostname)
data_len = MIN(data_len, len);
/* Check we received entire TLS record length */
- if (data_len < len)
+ if (data_len < len && data_len < 512) {
return;
+ }
/* Handshake */
- if (pos + 1 > data_len)
+ if (pos + 1 > data_len) {
return;
+ }
if (data[pos] != TLS_HANDSHAKE_TYPE_CLIENT_HELLO) {
return;
}
@@ -148,20 +153,23 @@ void parse_tls_header(const char *data, size_t data_len, char *hostname)
pos += 38;
/* Session ID */
- if (pos + 1 > data_len)
+ if (pos + 1 > data_len) {
return;
+ }
len = (unsigned char)data[pos];
pos += 1 + len;
/* Cipher Suites */
- if (pos + 2 > data_len)
+ if (pos + 2 > data_len) {
return;
+ }
len = ((unsigned char)data[pos] << 8) + (unsigned char)data[pos + 1];
pos += 2 + len;
/* Compression Methods */
- if (pos + 1 > data_len)
+ if (pos + 1 > data_len) {
return;
+ }
len = (unsigned char)data[pos];
pos += 1 + len;
@@ -171,13 +179,15 @@ void parse_tls_header(const char *data, size_t data_len, char *hostname)
}
/* Extensions */
- if (pos + 2 > data_len)
+ if (pos + 2 > data_len) {
return;
+ }
len = ((unsigned char)data[pos] << 8) + (unsigned char)data[pos + 1];
pos += 2;
- if (pos + len > data_len)
+ if (pos + len > data_len) {
return;
+ }
- parse_extensions(data + pos, len, hostname);
+ parse_extensions(data + pos, MIN(data_len - pos, len), hostname);
}
\ No newline at end of file
diff --git a/android_app/app/src/main/cpp/tun2http.c b/android_app/app/src/main/cpp/tun2http.c
index 9c50d5ef..b1ba492a 100644
--- a/android_app/app/src/main/cpp/tun2http.c
+++ b/android_app/app/src/main/cpp/tun2http.c
@@ -23,15 +23,16 @@ jint JNI_OnLoad(JavaVM *vm, void *reserved) {
// Raise file number limit to maximum
struct rlimit rlim;
- if (getrlimit(RLIMIT_NOFILE, &rlim))
+ if (getrlimit(RLIMIT_NOFILE, &rlim)) {
log_android(ANDROID_LOG_WARN, "getrlimit error %d: %s", errno, strerror(errno));
- else {
+ } else {
rlim_t soft = rlim.rlim_cur;
rlim.rlim_cur = rlim.rlim_max;
- if (setrlimit(RLIMIT_NOFILE, &rlim))
+ if (setrlimit(RLIMIT_NOFILE, &rlim)) {
log_android(ANDROID_LOG_WARN, "setrlimit error %d: %s", errno, strerror(errno));
- else
+ } else {
log_android(ANDROID_LOG_WARN, "raised file limit from %d to %d", soft, rlim.rlim_cur);
+ }
}
return JNI_VERSION_1_6;
@@ -41,14 +42,15 @@ void JNI_OnUnload(JavaVM *vm, void *reserved) {
log_android(ANDROID_LOG_INFO, "JNI unload");
JNIEnv *env;
- if ((*vm)->GetEnv(vm, (void **) &env, JNI_VERSION_1_6) != JNI_OK)
+ if ((*vm)->GetEnv(vm, (void **) &env, JNI_VERSION_1_6) != JNI_OK) {
log_android(ANDROID_LOG_INFO, "JNI load GetEnv failed");
+ }
}
// JNI ServiceSinkhole
JNIEXPORT void JNICALL
-Java_com_tun2http_app_service_Tun2HttpVpnService_jni_1init(JNIEnv *env, jobject instance) {
+Java_tun_proxy_service_Tun2HttpVpnService_jni_1init(JNIEnv *env, jobject instance) {
loglevel = ANDROID_LOG_WARN;
struct arguments args;
@@ -56,24 +58,28 @@ Java_com_tun2http_app_service_Tun2HttpVpnService_jni_1init(JNIEnv *env, jobject
args.instance = instance;
init(&args);
- if (pthread_mutex_init(&lock, NULL))
+ if (pthread_mutex_init(&lock, NULL)) {
log_android(ANDROID_LOG_ERROR, "pthread_mutex_init failed");
+ }
// Create signal pipe
- if (pipe(pipefds))
+ if (pipe(pipefds)) {
log_android(ANDROID_LOG_ERROR, "Create pipe error %d: %s", errno, strerror(errno));
- else
+ } else {
for (int i = 0; i < 2; i++) {
int flags = fcntl(pipefds[i], F_GETFL, 0);
- if (flags < 0 || fcntl(pipefds[i], F_SETFL, flags | O_NONBLOCK) < 0)
+ if (flags < 0 || fcntl(pipefds[i], F_SETFL, flags | O_NONBLOCK) < 0) {
log_android(ANDROID_LOG_ERROR, "fcntl pipefds[%d] O_NONBLOCK error %d: %s",
i, errno, strerror(errno));
+ }
}
+ }
}
JNIEXPORT void JNICALL
-Java_com_tun2http_app_service_Tun2HttpVpnService_jni_1start(
- JNIEnv *env, jobject instance, jint tun, jboolean fwd53, jint rcode, jstring proxyIp, jint proxyPort) {
+Java_tun_proxy_service_Tun2HttpVpnService_jni_1start(
+ JNIEnv *env, jobject instance, jint tun, jboolean fwd53, jint rcode, jstring proxyIp,
+ jint proxyPort, jboolean isSocks5) {
const char *proxy_ip = (*env)->GetStringUTFChars(env, proxyIp, 0);
@@ -81,16 +87,18 @@ Java_com_tun2http_app_service_Tun2HttpVpnService_jni_1start(
// Set blocking
int flags = fcntl(tun, F_GETFL, 0);
- if (flags < 0 || fcntl(tun, F_SETFL, flags & ~O_NONBLOCK) < 0)
+ if (flags < 0 || fcntl(tun, F_SETFL, flags & ~O_NONBLOCK) < 0) {
log_android(ANDROID_LOG_ERROR, "fcntl tun ~O_NONBLOCK error %d: %s",
errno, strerror(errno));
+ }
- if (thread_id && pthread_kill(thread_id, 0) == 0)
+ if (thread_id && pthread_kill(thread_id, 0) == 0) {
log_android(ANDROID_LOG_ERROR, "Already running thread %x", thread_id);
- else {
+ } else {
jint rs = (*env)->GetJavaVM(env, &jvm);
- if (rs != JNI_OK)
+ if (rs != JNI_OK) {
log_android(ANDROID_LOG_ERROR, "GetJavaVM failed");
+ }
// Get arguments
struct arguments *args = malloc(sizeof(struct arguments));
@@ -101,15 +109,16 @@ Java_com_tun2http_app_service_Tun2HttpVpnService_jni_1start(
args->rcode = rcode;
strcpy(args->proxyIp, proxy_ip);
args->proxyPort = proxyPort;
+ args->isSocks5 = isSocks5;
// Start native thread
int err = pthread_create(&thread_id, NULL, handle_events, (void *) args);
- if (err == 0)
+ if (err == 0) {
log_android(ANDROID_LOG_WARN, "Started thread %x", thread_id);
- else
+ } else {
log_android(ANDROID_LOG_ERROR, "pthread_create error %d: %s", err, strerror(err));
-
+ }
}
@@ -117,52 +126,56 @@ Java_com_tun2http_app_service_Tun2HttpVpnService_jni_1start(
}
JNIEXPORT void JNICALL
-Java_com_tun2http_app_service_Tun2HttpVpnService_jni_1stop(
+Java_tun_proxy_service_Tun2HttpVpnService_jni_1stop(
JNIEnv *env, jobject instance, jint tun) {
pthread_t t = thread_id;
log_android(ANDROID_LOG_WARN, "Stop tun %d thread %x", tun, t);
if (t && pthread_kill(t, 0) == 0) {
log_android(ANDROID_LOG_WARN, "Write pipe thread %x", t);
- if (write(pipefds[1], "x", 1) < 0)
+ if (write(pipefds[1], "x", 1) < 0) {
log_android(ANDROID_LOG_WARN, "Write pipe error %d: %s", errno, strerror(errno));
- else {
+ } else {
log_android(ANDROID_LOG_WARN, "Join thread %x", t);
int err = pthread_join(t, NULL);
- if (err != 0)
+ if (err != 0) {
log_android(ANDROID_LOG_WARN, "pthread_join error %d: %s", err, strerror(err));
+ }
}
clear();
log_android(ANDROID_LOG_WARN, "Stopped thread %x", t);
- } else
+ } else {
log_android(ANDROID_LOG_WARN, "Not running thread %x", t);
+ }
}
JNIEXPORT jint JNICALL
-Java_com_tun2http_app_service_Tun2HttpVpnService_jni_1get_1mtu(JNIEnv *env, jobject instance) {
+Java_tun_proxy_service_Tun2HttpVpnService_jni_1get_1mtu(JNIEnv *env, jobject instance) {
return get_mtu();
}
JNIEXPORT void JNICALL
-Java_com_tun2http_app_service_Tun2HttpVpnService_jni_1done(JNIEnv *env, jobject instance) {
+Java_tun_proxy_service_Tun2HttpVpnService_jni_1done(JNIEnv *env, jobject instance) {
log_android(ANDROID_LOG_INFO, "Done");
clear();
- if (pthread_mutex_destroy(&lock))
+ if (pthread_mutex_destroy(&lock)) {
log_android(ANDROID_LOG_ERROR, "pthread_mutex_destroy failed");
+ }
- for (int i = 0; i < 2; i++)
- if (close(pipefds[i]))
+ for (int i = 0; i < 2; i++) {
+ if (close(pipefds[i])) {
log_android(ANDROID_LOG_ERROR, "Close pipe error %d: %s", errno, strerror(errno));
+ }
+ }
}
-// JNI Util
-
+// JNI NetUtil
JNIEXPORT jstring JNICALL
-Java_com_tun2http_app_utils_Util_jni_1getprop(JNIEnv *env, jclass type, jstring name_) {
+Java_tun_utils_NetUtil_jni_1getprop(JNIEnv *env, jclass type, jstring name_) {
const char *name = (*env)->GetStringUTFChars(env, name_, 0);
char value[PROP_VALUE_MAX + 1] = "";
@@ -178,8 +191,9 @@ static jmethodID midProtect = NULL;
int protect_socket(const struct arguments *args, int socket) {
jclass cls = (*args->env)->GetObjectClass(args->env, args->instance);
- if (midProtect == NULL)
+ if (midProtect == NULL) {
midProtect = jniGetMethodID(args->env, cls, "protect", "(I)Z");
+ }
jboolean isProtected = (*args->env)->CallBooleanMethod(
args->env, args->instance, midProtect, socket);
@@ -198,17 +212,19 @@ int protect_socket(const struct arguments *args, int socket) {
jobject jniGlobalRef(JNIEnv *env, jobject cls) {
jobject gcls = (*env)->NewGlobalRef(env, cls);
- if (gcls == NULL)
+ if (gcls == NULL) {
log_android(ANDROID_LOG_ERROR, "Global ref failed (out of memory?)");
+ }
return gcls;
}
jclass jniFindClass(JNIEnv *env, const char *name) {
jclass cls = (*env)->FindClass(env, name);
- if (cls == NULL)
+ if (cls == NULL) {
log_android(ANDROID_LOG_ERROR, "Class %s not found", name);
- else
+ } else {
jniCheckException(env);
+ }
return cls;
}
diff --git a/android_app/app/src/main/cpp/tun2http.h b/android_app/app/src/main/cpp/tun2http.h
index 129b12e6..00a44950 100644
--- a/android_app/app/src/main/cpp/tun2http.h
+++ b/android_app/app/src/main/cpp/tun2http.h
@@ -1,6 +1,7 @@
#include
#include
#include
+#include
#include
#include
#include
@@ -55,6 +56,8 @@
#define TCP_CONNECT_NOT_SENT -1
#define TCP_CONNECT_SENT 0
#define TCP_CONNECT_ESTABLISHED 1
+#define SOCKS5_STATE_GREETING 2
+#define SOCKS5_STATE_CONNECT 3
#define MTU 10000
@@ -66,6 +69,7 @@ struct arguments {
jint rcode;
char proxyIp[128];
int proxyPort;
+ jboolean isSocks5;
};
struct allowed {
@@ -169,6 +173,9 @@ struct tcp_session {
char hostname[512];
int connect_sent;
+ uint8_t socks5_state;
+ uint8_t is_http;
+ uint8_t is_tls;
};
struct ng_session {
@@ -207,16 +214,16 @@ struct ip6_hdr_pseudo {
struct dns_header {
uint16_t id; // identification number
# if __BYTE_ORDER == __LITTLE_ENDIAN
- uint16_t rd :1; // recursion desired
- uint16_t tc :1; // truncated message
- uint16_t aa :1; // authoritive answer
- uint16_t opcode :4; // purpose of message
- uint16_t qr :1; // query/response flag
- uint16_t rcode :4; // response code
- uint16_t cd :1; // checking disabled
- uint16_t ad :1; // authenticated data
- uint16_t z :1; // its z! reserved
- uint16_t ra :1; // recursion available
+ uint16_t rd: 1; // recursion desired
+ uint16_t tc: 1; // truncated message
+ uint16_t aa: 1; // authoritive answer
+ uint16_t opcode: 4; // purpose of message
+ uint16_t qr: 1; // query/response flag
+ uint16_t rcode: 4; // response code
+ uint16_t cd: 1; // checking disabled
+ uint16_t ad: 1; // authenticated data
+ uint16_t z: 1; // its z! reserved
+ uint16_t ra: 1; // recursion available
#elif __BYTE_ORDER == __BIG_ENDIAN
uint16_t qr :1; // query/response flag
uint16_t opcode :4; // purpose of message
diff --git a/android_app/app/src/main/cpp/udp.c b/android_app/app/src/main/cpp/udp.c
index b7a078fb..5b484f9f 100644
--- a/android_app/app/src/main/cpp/udp.c
+++ b/android_app/app/src/main/cpp/udp.c
@@ -41,9 +41,10 @@ int check_udp_session(const struct arguments *args, struct ng_session *s,
log_android(ANDROID_LOG_INFO, "UDP close from %s/%u to %s/%u socket %d",
source, ntohs(s->udp.source), dest, ntohs(s->udp.dest), s->socket);
- if (close(s->socket))
+ if (close(s->socket)) {
log_android(ANDROID_LOG_ERROR, "UDP close %d error %d: %s",
s->socket, errno, strerror(errno));
+ }
s->socket = -1;
s->udp.time = time(NULL);
@@ -57,8 +58,9 @@ int check_udp_session(const struct arguments *args, struct ng_session *s,
// Cleanup lingering sessions
if ((s->udp.state == UDP_CLOSED || s->udp.state == UDP_BLOCKED) &&
- s->udp.time + UDP_KEEP_TIMEOUT < now)
+ s->udp.time + UDP_KEEP_TIMEOUT < now) {
return 1;
+ }
return 0;
}
@@ -73,11 +75,12 @@ void check_udp_socket(const struct arguments *args, const struct epoll_event *ev
int serr = 0;
socklen_t optlen = sizeof(int);
int err = getsockopt(s->socket, SOL_SOCKET, SO_ERROR, &serr, &optlen);
- if (err < 0)
+ if (err < 0) {
log_android(ANDROID_LOG_ERROR, "UDP getsockopt error %d: %s",
errno, strerror(errno));
- else if (serr)
+ } else if (serr) {
log_android(ANDROID_LOG_ERROR, "UDP SO_ERROR %d: %s", serr, strerror(serr));
+ }
s->udp.state = UDP_FINISHING;
} else {
@@ -101,26 +104,29 @@ void check_udp_socket(const struct arguments *args, const struct epoll_event *ev
} else {
// Socket read data
char dest[INET6_ADDRSTRLEN + 1];
- if (s->udp.version == 4)
+ if (s->udp.version == 4) {
inet_ntop(AF_INET, &s->udp.daddr.ip4, dest, sizeof(dest));
- else
+ } else {
inet_ntop(AF_INET6, &s->udp.daddr.ip6, dest, sizeof(dest));
+ }
log_android(ANDROID_LOG_INFO, "UDP recv bytes %d from %s/%u for tun",
bytes, dest, ntohs(s->udp.dest));
s->udp.received += bytes;
// Process DNS response
- if (ntohs(s->udp.dest) == 53)
+ if (ntohs(s->udp.dest) == 53) {
parse_dns_response(args, &s->udp, buffer, (size_t *) &bytes);
+ }
// Forward to tun
- if (write_udp(args, &s->udp, buffer, (size_t) bytes) < 0)
+ if (write_udp(args, &s->udp, buffer, (size_t) bytes) < 0) {
s->udp.state = UDP_FINISHING;
- else {
+ } else {
// Prevent too many open files
- if (ntohs(s->udp.dest) == 53)
+ if (ntohs(s->udp.dest) == 53) {
s->udp.state = UDP_FINISHING;
+ }
}
}
free(buffer);
@@ -135,8 +141,9 @@ int has_udp_session(const struct arguments *args, const uint8_t *pkt, const uint
const struct ip6_hdr *ip6 = (struct ip6_hdr *) pkt;
const struct udphdr *udphdr = (struct udphdr *) payload;
- if (ntohs(udphdr->dest) == 53 && !args->fwd53)
+ if (ntohs(udphdr->dest) == 53 && !args->fwd53) {
return 1;
+ }
// Search session
struct ng_session *cur = ng_session;
@@ -147,9 +154,9 @@ int has_udp_session(const struct arguments *args, const uint8_t *pkt, const uint
(version == 4 ? cur->udp.saddr.ip4 == ip4->saddr &&
cur->udp.daddr.ip4 == ip4->daddr
: memcmp(&cur->udp.saddr.ip6, &ip6->ip6_src, 16) == 0 &&
- memcmp(&cur->udp.daddr.ip6, &ip6->ip6_dst, 16) == 0)))
+ memcmp(&cur->udp.daddr.ip6, &ip6->ip6_dst, 16) == 0))) {
cur = cur->next;
-
+ }
return (cur != NULL);
}
@@ -288,8 +295,9 @@ jboolean handle_udp(const struct arguments *args,
memset(&s->ev, 0, sizeof(struct epoll_event));
s->ev.events = EPOLLIN | EPOLLERR;
s->ev.data.ptr = s;
- if (epoll_ctl(epoll_fd, EPOLL_CTL_ADD, s->socket, &s->ev))
+ if (epoll_ctl(epoll_fd, EPOLL_CTL_ADD, s->socket, &s->ev)) {
log_android(ANDROID_LOG_ERROR, "epoll add udp error %d: %s", errno, strerror(errno));
+ }
s->next = ng_session;
ng_session = s;
@@ -307,7 +315,7 @@ jboolean handle_udp(const struct arguments *args,
"DNS query qtype %d qclass %d name %s",
qtype, qclass, qname);
- if (0)
+ if (0) {
if (check_domain(args, &cur->udp, data, datalen, qclass, qtype, qname)) {
// Log qname
char name[DNS_QNAME_MAX + 40 + 1];
@@ -322,13 +330,15 @@ jboolean handle_udp(const struct arguments *args,
cur->udp.state = UDP_FINISHING;
return 0;
}
+ }
}
}
// Check for DHCP (tethering)
if (ntohs(udphdr->source) == 68 || ntohs(udphdr->dest) == 67) {
- if (check_dhcp(args, &cur->udp, data, datalen) >= 0)
+ if (check_dhcp(args, &cur->udp, data, datalen) >= 0) {
return 1;
+ }
}
log_android(ANDROID_LOG_INFO, "UDP forward from tun %s/%u to %s/%u data %d",
@@ -359,8 +369,9 @@ jboolean handle_udp(const struct arguments *args,
cur->udp.state = UDP_FINISHING;
return 0;
}
- } else
+ } else {
cur->udp.sent += datalen;
+ }
return 1;
}
@@ -369,10 +380,11 @@ int open_udp_socket(const struct arguments *args,
const struct udp_session *cur, const struct allowed *redirect) {
int sock;
int version;
- if (redirect == NULL)
+ if (redirect == NULL) {
version = cur->version;
- else
+ } else {
version = (strstr(redirect->raddr, ":") == NULL ? 4 : 6);
+ }
// Get UDP socket
sock = socket(version == 4 ? PF_INET : PF_INET6, SOCK_DGRAM, IPPROTO_UDP);
@@ -382,8 +394,9 @@ int open_udp_socket(const struct arguments *args,
}
// Protect socket
- if (protect_socket(args, sock) < 0)
+ if (protect_socket(args, sock) < 0) {
return -1;
+ }
// Check for broadcast/multicast
if (cur->version == 4) {
@@ -391,9 +404,10 @@ int open_udp_socket(const struct arguments *args,
if (memcmp(&cur->daddr.ip4, &broadcast4, sizeof(broadcast4)) == 0) {
log_android(ANDROID_LOG_WARN, "UDP4 broadcast");
int on = 1;
- if (setsockopt(sock, SOL_SOCKET, SO_BROADCAST, &on, sizeof(on)))
+ if (setsockopt(sock, SOL_SOCKET, SO_BROADCAST, &on, sizeof(on))) {
log_android(ANDROID_LOG_ERROR, "UDP setsockopt SO_BROADCAST error %d: %s",
errno, strerror(errno));
+ }
}
} else {
// http://man7.org/linux/man-pages/man7/ipv6.7.html
@@ -401,24 +415,27 @@ int open_udp_socket(const struct arguments *args,
log_android(ANDROID_LOG_WARN, "UDP6 broadcast");
int loop = 1; // true
- if (setsockopt(sock, IPPROTO_IPV6, IPV6_MULTICAST_LOOP, &loop, sizeof(loop)))
+ if (setsockopt(sock, IPPROTO_IPV6, IPV6_MULTICAST_LOOP, &loop, sizeof(loop))) {
log_android(ANDROID_LOG_ERROR,
"UDP setsockopt IPV6_MULTICAST_LOOP error %d: %s",
errno, strerror(errno));
+ }
int ttl = -1; // route default
- if (setsockopt(sock, IPPROTO_IPV6, IPV6_MULTICAST_HOPS, &ttl, sizeof(ttl)))
+ if (setsockopt(sock, IPPROTO_IPV6, IPV6_MULTICAST_HOPS, &ttl, sizeof(ttl))) {
log_android(ANDROID_LOG_ERROR,
"UDP setsockopt IPV6_MULTICAST_HOPS error %d: %s",
errno, strerror(errno));
+ }
struct ipv6_mreq mreq6;
memcpy(&mreq6.ipv6mr_multiaddr, &cur->daddr.ip6, sizeof(struct in6_addr));
mreq6.ipv6mr_interface = INADDR_ANY;
- if (setsockopt(sock, IPPROTO_IPV6, IPV6_ADD_MEMBERSHIP, &mreq6, sizeof(mreq6)))
+ if (setsockopt(sock, IPPROTO_IPV6, IPV6_ADD_MEMBERSHIP, &mreq6, sizeof(mreq6))) {
log_android(ANDROID_LOG_ERROR,
"UDP setsockopt IPV6_ADD_MEMBERSHIP error %d: %s",
errno, strerror(errno));
+ }
}
}
@@ -440,8 +457,9 @@ ssize_t write_udp(const struct arguments *args, const struct udp_session *cur,
buffer = malloc(len);
struct iphdr *ip4 = (struct iphdr *) buffer;
udp = (struct udphdr *) (buffer + sizeof(struct iphdr));
- if (datalen)
+ if (datalen) {
memcpy(buffer + sizeof(struct iphdr) + sizeof(struct udphdr), data, datalen);
+ }
// Build IP4 header
memset(ip4, 0, sizeof(struct iphdr));
@@ -470,8 +488,9 @@ ssize_t write_udp(const struct arguments *args, const struct udp_session *cur,
buffer = malloc(len);
struct ip6_hdr *ip6 = (struct ip6_hdr *) buffer;
udp = (struct udphdr *) (buffer + sizeof(struct ip6_hdr));
- if (datalen)
+ if (datalen) {
memcpy(buffer + sizeof(struct ip6_hdr) + sizeof(struct udphdr), data, datalen);
+ }
// Build IP6 header
memset(ip6, 0, sizeof(struct ip6_hdr));
diff --git a/android_app/app/src/main/cpp/util.c b/android_app/app/src/main/cpp/util.c
index 90432452..2a953f07 100644
--- a/android_app/app/src/main/cpp/util.c
+++ b/android_app/app/src/main/cpp/util.c
@@ -1,9 +1,9 @@
-
-
#include "tun2http.h"
extern int loglevel;
+#define LOG_TAG "tun2http_naitive"
+
uint16_t calc_checksum(uint16_t start, const uint8_t *buffer, size_t length) {
register uint32_t sum = start;
register uint16_t *buf = (uint16_t *) buffer;
@@ -14,27 +14,31 @@ uint16_t calc_checksum(uint16_t start, const uint8_t *buffer, size_t length) {
len -= 2;
}
- if (len > 0)
+ if (len > 0) {
sum += *((uint8_t *) buf);
+ }
- while (sum >> 16)
+ while (sum >> 16) {
sum = (sum & 0xFFFF) + (sum >> 16);
+ }
return (uint16_t) sum;
}
int compare_u32(uint32_t s1, uint32_t s2) {
// https://tools.ietf.org/html/rfc1982
- if (s1 == s2)
+ if (s1 == s2) {
return 0;
+ }
int i1 = s1;
int i2 = s2;
if ((i1 < i2 && i2 - i1 < 0x7FFFFFFF) ||
- (i1 > i2 && i1 - i2 > 0x7FFFFFFF))
+ (i1 > i2 && i1 - i2 > 0x7FFFFFFF)) {
return -1;
- else
+ } else {
return 1;
+ }
}
int sdk_int(JNIEnv *env) {
@@ -44,7 +48,10 @@ int sdk_int(JNIEnv *env) {
}
void log_android(int prio, const char *fmt, ...) {
-
+ va_list args;
+ va_start(args, fmt);
+ __android_log_vprint(prio, LOG_TAG, fmt, args);
+ va_end(args);
}
uint8_t char2nible(const char c) {
@@ -56,19 +63,23 @@ uint8_t char2nible(const char c) {
void hex2bytes(const char *hex, uint8_t *buffer) {
size_t len = strlen(hex);
- for (int i = 0; i < len; i += 2)
+ for (int i = 0; i < len; i += 2) {
buffer[i / 2] = (char2nible(hex[i]) << 4) | char2nible(hex[i + 1]);
+ }
}
char *trim(char *str) {
- while (isspace(*str))
+ while (isspace(*str)) {
str++;
- if (*str == 0)
+ }
+ if (*str == 0) {
return str;
+ }
char *end = str + strlen(str) - 1;
- while (end > str && isspace(*end))
+ while (end > str && isspace(*end)) {
end--;
+ }
*(end + 1) = 0;
return str;
}
@@ -124,9 +135,9 @@ int32_t get_local_port(const int sock) {
if (getsockname(sock, (struct sockaddr *) &sin, &len) < 0) {
log_android(ANDROID_LOG_ERROR, "getsockname error %d: %s", errno, strerror(errno));
return -1;
- }
- else
+ } else {
return ntohs(sin.sin_port);
+ }
}
int is_event(int fd, short event) {
@@ -138,11 +149,11 @@ int is_event(int fd, short event) {
if (r < 0) {
log_android(ANDROID_LOG_ERROR, "poll readable error %d: %s", errno, strerror(errno));
return 0;
- }
- else if (r == 0)
+ } else if (r == 0) {
return 0;
- else
+ } else {
return (p.revents & event);
+ }
}
int is_readable(int fd) {
diff --git a/android_app/app/src/main/java/com/tun2http/app/MainActivity.java b/android_app/app/src/main/java/com/tun2http/app/MainActivity.java
deleted file mode 100644
index f7baf391..00000000
--- a/android_app/app/src/main/java/com/tun2http/app/MainActivity.java
+++ /dev/null
@@ -1,194 +0,0 @@
-package com.tun2http.app;
-
-import android.app.Activity;
-import android.content.ComponentName;
-import android.content.Context;
-import android.content.Intent;
-import android.content.ServiceConnection;
-import android.content.SharedPreferences;
-import android.net.VpnService;
-import android.os.Handler;
-import android.os.IBinder;
-import android.preference.PreferenceManager;
-import android.os.Bundle;
-import android.text.TextUtils;
-import android.view.View;
-import android.widget.Button;
-import android.widget.EditText;
-
-import com.tun2http.app.service.Tun2HttpVpnService;
-
-public class MainActivity extends Activity {
- Button start;
- Button stop;
- EditText hostEditText;
-
- Handler statusHandler = new Handler();
-
- private Tun2HttpVpnService service;
-
- @Override
- protected void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- setContentView(R.layout.activity_main);
-
- start = findViewById(R.id.start);
- stop = findViewById(R.id.stop);
- hostEditText = findViewById(R.id.host);
-
- start.setOnClickListener(new View.OnClickListener() {
- @Override
- public void onClick(View v) {
- startVpn();
- }
- });
- stop.setOnClickListener(new View.OnClickListener() {
- @Override
- public void onClick(View v) {
- stopVpn();
- }
- });
-
-
- start.setEnabled(true);
- stop.setEnabled(false);
-
- loadHostPort();
- }
-
-
- private ServiceConnection serviceConnection = new ServiceConnection() {
- public void onServiceConnected(ComponentName className, IBinder binder) {
- Tun2HttpVpnService.ServiceBinder serviceBinder = (Tun2HttpVpnService.ServiceBinder) binder;
- service = serviceBinder.getService();
- }
-
- public void onServiceDisconnected(ComponentName className) {
- service = null;
- }
- };
-
- @Override
- protected void onResume() {
- super.onResume();
-
- start.setEnabled(false);
- stop.setEnabled(false);
- updateStatus();
-
- statusHandler.postDelayed(statusRunnable, 1000);
-
- Intent intent = new Intent(this, Tun2HttpVpnService.class);
- bindService(intent, serviceConnection, Context.BIND_AUTO_CREATE);
- }
-
- boolean isRunning() {
- return service != null && service.isRunning();
- }
-
- Runnable statusRunnable = new Runnable() {
- @Override
- public void run() {
- updateStatus();
- statusHandler.postDelayed(statusRunnable, 1000);
- }
- };
-
- @Override
- protected void onPause() {
- super.onPause();
- statusHandler.removeCallbacks(statusRunnable);
-
- unbindService(serviceConnection);
- }
-
- void updateStatus() {
- if (service == null) {
- return;
- }
- if (isRunning()) {
- start.setEnabled(false);
- stop.setEnabled(true);
- } else {
- start.setEnabled(true);
- stop.setEnabled(false);
- }
- }
-
- private void stopVpn() {
- start.setEnabled(true);
- stop.setEnabled(false);
-
- Tun2HttpVpnService.stop(this);
- }
-
- private void startVpn() {
-
- Intent i = VpnService.prepare(this);
- if (i != null) {
- startActivityForResult(i, 0);
- } else {
- onActivityResult(0, Activity.RESULT_OK, null);
- }
- }
-
- @Override
- protected void onActivityResult(int requestCode, int resultCode, Intent data) {
- super.onActivityResult(requestCode, resultCode, data);
-
- if (resultCode == Activity.RESULT_OK && parseAndSaveHostPort()) {
- start.setEnabled(false);
- stop.setEnabled(true);
- Tun2HttpVpnService.start(this);
- }
- }
-
- private void loadHostPort() {
- SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this);
- String proxyHost = prefs.getString(Tun2HttpVpnService.PREF_PROXY_HOST, "");
- int proxyPort = prefs.getInt(Tun2HttpVpnService.PREF_PROXY_PORT, 0);
-
- if(TextUtils.isEmpty(proxyHost)) {
- return;
- }
-
- if(proxyPort == 80) {
- hostEditText.setText(proxyHost);
- } else {
- hostEditText.setText(proxyHost + ":" + proxyPort);
- }
- }
-
- private boolean parseAndSaveHostPort() {
- String hostPort = hostEditText.getText().toString();
- if (hostPort.isEmpty()) {
- hostEditText.setError(getString(R.string.enter_host));
- return false;
- }
-
- String parts[] = hostPort.split(":");
- int port = 0;
- if (parts.length > 1) {
- try {
- port = Integer.parseInt(parts[1]);
- } catch (Exception e) {
- hostEditText.setError(getString(R.string.enter_host));
- return false;
- }
- }
- String[] ipParts = parts[0].split("\\.");
- if(ipParts.length != 4) {
- hostEditText.setError(getString(R.string.enter_host));
- return false;
- }
- String host = parts[0];
- SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this);
- SharedPreferences.Editor edit = prefs.edit();
-
- edit.putString(Tun2HttpVpnService.PREF_PROXY_HOST, host);
- edit.putInt(Tun2HttpVpnService.PREF_PROXY_PORT, port);
-
- edit.commit();
- return true;
- }
-}
diff --git a/android_app/app/src/main/java/com/tun2http/app/receiver/BootReceiver.java b/android_app/app/src/main/java/com/tun2http/app/receiver/BootReceiver.java
deleted file mode 100644
index 57aef690..00000000
--- a/android_app/app/src/main/java/com/tun2http/app/receiver/BootReceiver.java
+++ /dev/null
@@ -1,34 +0,0 @@
-package com.tun2http.app.receiver;
-
-import android.content.BroadcastReceiver;
-import android.content.Context;
-import android.content.Intent;
-import android.content.SharedPreferences;
-import android.net.VpnService;
-import android.preference.PreferenceManager;
-import android.util.Log;
-
-import com.tun2http.app.service.Tun2HttpVpnService;
-
-
-public class BootReceiver extends BroadcastReceiver {
-
- @Override
- public void onReceive(final Context context, Intent intent) {
- if(intent != null && !Intent.ACTION_BOOT_COMPLETED.equals(intent.getAction())) {
- return;
- }
-
- SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
- boolean isRunning = prefs.getBoolean(Tun2HttpVpnService.PREF_RUNNING, false);
- if(isRunning) {
- Intent prepare = VpnService.prepare(context);
- if(prepare == null) {
- Log.d("Tun2Http.Boot", "Starting vpn");
- Tun2HttpVpnService.start(context);
- } else {
- Log.d("Tun2Http.Boot", "Not prepared");
- }
- }
- }
-}
\ No newline at end of file
diff --git a/android_app/app/src/main/java/com/tun2http/app/utils/IPUtil.java b/android_app/app/src/main/java/com/tun2http/app/utils/IPUtil.java
deleted file mode 100644
index 7ddf61f0..00000000
--- a/android_app/app/src/main/java/com/tun2http/app/utils/IPUtil.java
+++ /dev/null
@@ -1,120 +0,0 @@
-package com.tun2http.app.utils;
-
-
-import android.util.Log;
-
-import java.net.InetAddress;
-import java.net.UnknownHostException;
-import java.util.ArrayList;
-import java.util.List;
-
-public class IPUtil {
- private static final String TAG = "Tun2Http.IPUtil";
-
- public static List toCIDR(String start, String end) throws UnknownHostException {
- return toCIDR(InetAddress.getByName(start), InetAddress.getByName(end));
- }
-
- public static List toCIDR(InetAddress start, InetAddress end) throws UnknownHostException {
- List listResult = new ArrayList<>();
-
- Log.i(TAG, "toCIDR(" + start.getHostAddress() + "," + end.getHostAddress() + ")");
-
- long from = inet2long(start);
- long to = inet2long(end);
- while (to >= from) {
- byte prefix = 32;
- while (prefix > 0) {
- long mask = prefix2mask(prefix - 1);
- if ((from & mask) != from)
- break;
- prefix--;
- }
-
- byte max = (byte) (32 - Math.floor(Math.log(to - from + 1) / Math.log(2)));
- if (prefix < max)
- prefix = max;
-
- listResult.add(new CIDR(long2inet(from), prefix));
-
- from += Math.pow(2, (32 - prefix));
- }
-
- for (CIDR cidr : listResult)
- Log.i(TAG, cidr.toString());
-
- return listResult;
- }
-
- private static long prefix2mask(int bits) {
- return (0xFFFFFFFF00000000L >> bits) & 0xFFFFFFFFL;
- }
-
- private static long inet2long(InetAddress addr) {
- long result = 0;
- if (addr != null)
- for (byte b : addr.getAddress())
- result = result << 8 | (b & 0xFF);
- return result;
- }
-
- private static InetAddress long2inet(long addr) {
- try {
- byte[] b = new byte[4];
- for (int i = b.length - 1; i >= 0; i--) {
- b[i] = (byte) (addr & 0xFF);
- addr = addr >> 8;
- }
- return InetAddress.getByAddress(b);
- } catch (UnknownHostException ignore) {
- return null;
- }
- }
-
- public static InetAddress minus1(InetAddress addr) {
- return long2inet(inet2long(addr) - 1);
- }
-
- public static InetAddress plus1(InetAddress addr) {
- return long2inet(inet2long(addr) + 1);
- }
-
- public static class CIDR implements Comparable {
- public InetAddress address;
- public int prefix;
-
- public CIDR(InetAddress address, int prefix) {
- this.address = address;
- this.prefix = prefix;
- }
-
- public CIDR(String ip, int prefix) {
- try {
- this.address = InetAddress.getByName(ip);
- this.prefix = prefix;
- } catch (UnknownHostException ex) {
- Log.e(TAG, ex.toString() + "\n" + Log.getStackTraceString(ex));
- }
- }
-
- public InetAddress getStart() {
- return long2inet(inet2long(this.address) & prefix2mask(this.prefix));
- }
-
- public InetAddress getEnd() {
- return long2inet((inet2long(this.address) & prefix2mask(this.prefix)) + (1L << (32 - this.prefix)) - 1);
- }
-
- @Override
- public String toString() {
- return address.getHostAddress() + "/" + prefix + "=" + getStart().getHostAddress() + "..." + getEnd().getHostAddress();
- }
-
- @Override
- public int compareTo(CIDR other) {
- Long lcidr = IPUtil.inet2long(this.address);
- Long lother = IPUtil.inet2long(other.address);
- return lcidr.compareTo(lother);
- }
- }
-}
diff --git a/android_app/app/src/main/java/com/tun2http/app/utils/Util.java b/android_app/app/src/main/java/com/tun2http/app/utils/Util.java
deleted file mode 100644
index ffe3b8d1..00000000
--- a/android_app/app/src/main/java/com/tun2http/app/utils/Util.java
+++ /dev/null
@@ -1,64 +0,0 @@
-package com.tun2http.app.utils;
-
-/*
- This file is part of NetGuard.
-
- NetGuard is free software: you can redistribute it and/or modify
- it under the terms of the GNU General Public License as published by
- the Free Software Foundation, either version 3 of the License, or
- (at your option) any later version.
-
- NetGuard is distributed in the hope that it will be useful,
- but WITHOUT ANY WARRANTY; without even the implied warranty of
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- GNU General Public License for more details.
-
- You should have received a copy of the GNU General Public License
- along with NetGuard. If not, see .
-
- Copyright 2015-2017 by Marcel Bokhorst (M66B)
-*/
-
-import android.content.Context;
-import android.net.ConnectivityManager;
-import android.net.LinkProperties;
-import android.net.Network;
-import android.os.Build;
-import android.text.TextUtils;
-
-import java.net.InetAddress;
-import java.util.ArrayList;
-import java.util.List;
-
-public class Util {
- private static native String jni_getprop(String name);
-
- public static List getDefaultDNS(Context context) {
- String dns1 = null;
- String dns2 = null;
- if (Build.VERSION.SDK_INT > Build.VERSION_CODES.N_MR1) {
- ConnectivityManager cm = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
- Network an = cm.getActiveNetwork();
- if (an != null) {
- LinkProperties lp = cm.getLinkProperties(an);
- if (lp != null) {
- List dns = lp.getDnsServers();
- if (dns != null) {
- if (dns.size() > 0)
- dns1 = dns.get(0).getHostAddress();
- if (dns.size() > 1)
- dns2 = dns.get(1).getHostAddress();
- }
- }
- }
- } else {
- dns1 = jni_getprop("net.dns1");
- dns2 = jni_getprop("net.dns2");
- }
-
- List listDns = new ArrayList<>();
- listDns.add(TextUtils.isEmpty(dns1) ? "8.8.8.8" : dns1);
- listDns.add(TextUtils.isEmpty(dns2) ? "8.8.4.4" : dns2);
- return listDns;
- }
-}
diff --git a/android_app/app/src/main/java/tun/proxy/HostPortPair.java b/android_app/app/src/main/java/tun/proxy/HostPortPair.java
new file mode 100644
index 00000000..ea0e0f7a
--- /dev/null
+++ b/android_app/app/src/main/java/tun/proxy/HostPortPair.java
@@ -0,0 +1,68 @@
+package tun.proxy;
+
+import java.util.Locale;
+
+public class HostPortPair {
+
+ private String host = "";
+ private int port = -1;
+
+ public HostPortPair() {
+
+ }
+
+ public HostPortPair(String host, int port) {
+ this.host = host;
+ this.port = port;
+ }
+
+ public String getHost() {
+ return host;
+ }
+
+ public void setHost(String host) {
+ this.host = host;
+ }
+
+ public int getPort() {
+ return port;
+ }
+
+ public void setPort(int port) {
+ this.port = port;
+ }
+
+ public static HostPortPair parse(String hostPort) throws IllegalArgumentException {
+ final HostPortPair pair = new HostPortPair();
+ String[] parts = hostPort.split(":");
+ int port = -1; // デフォルト値にはならないはず
+ if (parts.length == 2) {
+ port = Integer.parseInt(parts[1]);
+ pair.host = parts[0];
+ pair.port = port;
+ } else {
+ throw new IllegalArgumentException("Invalid host:port format:" + hostPort);
+ }
+ return pair;
+ }
+
+ public static String valueOf(String host, int port) {
+ return String.format(Locale.ROOT, "%s:%d", host, port);
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ HostPortPair other = (HostPortPair) obj;
+ if (other == null)
+ return false;
+
+ if (!this.host.equals(other.host))
+ return false;
+
+ if (this.port != other.port)
+ return false;
+
+ return true;
+ }
+
+}
diff --git a/android_app/app/src/main/java/tun/proxy/MainActivity.java b/android_app/app/src/main/java/tun/proxy/MainActivity.java
new file mode 100644
index 00000000..ccae9c57
--- /dev/null
+++ b/android_app/app/src/main/java/tun/proxy/MainActivity.java
@@ -0,0 +1,362 @@
+package tun.proxy;
+
+import android.graphics.Color;
+import android.net.VpnService;
+import android.os.Bundle;
+
+import android.app.AlertDialog;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.ServiceConnection;
+import android.content.SharedPreferences;
+import android.content.pm.PackageManager;
+
+import androidx.activity.EdgeToEdge;
+import androidx.activity.SystemBarStyle;
+import androidx.appcompat.app.AppCompatActivity;
+import androidx.appcompat.widget.Toolbar;
+import androidx.core.graphics.Insets;
+import androidx.core.view.ViewCompat;
+import androidx.core.view.WindowInsetsCompat;
+import androidx.preference.PreferenceManager;
+import androidx.activity.result.ActivityResultLauncher;
+import androidx.activity.result.contract.ActivityResultContracts;
+
+import android.os.Handler;
+import android.os.IBinder;
+import android.os.Looper;
+import android.view.View;
+import android.view.Menu;
+import android.view.MenuItem;
+import android.widget.ArrayAdapter;
+import android.widget.Button;
+import android.widget.EditText;
+import android.widget.AdapterView;
+import android.widget.Spinner;
+import android.widget.Toast;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import tun.proxy.service.Tun2HttpVpnService;
+import tun.utils.NetUtil;
+
+public class MainActivity extends AppCompatActivity {
+ private static final String TAG = "MainActivity";
+
+ Button startButton;
+ Button stopButton;
+ EditText hostPortEditText;
+ Spinner proxyTypeSpinner;
+ Spinner profileSpinner;
+ private List profileList;
+ private ArrayAdapter profileAdapter;
+ private final ActivityResultLauncher vpnRequestLauncher = registerForActivityResult(
+ new ActivityResultContracts.StartActivityForResult(),
+ result -> {
+ if (result.getResultCode() == RESULT_OK) {
+ vpnPrepared();
+ }
+ }
+ );
+ Handler statusHandler = new Handler(Looper.getMainLooper());
+ private Tun2HttpVpnService service;
+ private final ServiceConnection serviceConnection = new ServiceConnection() {
+ public void onServiceConnected(ComponentName className, IBinder binder) {
+ Tun2HttpVpnService.ServiceBinder serviceBinder = (Tun2HttpVpnService.ServiceBinder) binder;
+ service = serviceBinder.getService();
+ }
+
+ public void onServiceDisconnected(ComponentName className) {
+ service = null;
+ }
+ };
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ EdgeToEdge.enable(this, SystemBarStyle.dark(Color.TRANSPARENT));
+ setContentView(R.layout.activity_main);
+ ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.container), (v, insets) -> {
+ Insets systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars());
+ v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom);
+ return WindowInsetsCompat.CONSUMED;
+ });
+ Toolbar toolbar = findViewById(R.id.toolbar);
+ setSupportActionBar(toolbar);
+
+ startButton = findViewById(R.id.start);
+ stopButton = findViewById(R.id.stop);
+ hostPortEditText = findViewById(R.id.host);
+ proxyTypeSpinner = findViewById(R.id.proxy_type);
+ profileSpinner = findViewById(R.id.spinner_profile);
+
+ ArrayAdapter adapter = ArrayAdapter.createFromResource(this,
+ R.array.proxy_types, R.layout.spinner_item);
+ adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
+ proxyTypeSpinner.setAdapter(adapter);
+
+ setupProfileSpinner();
+
+ startButton.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ startVpn();
+ }
+ });
+ stopButton.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ stopVpn();
+ }
+ });
+ startButton.setEnabled(true);
+ stopButton.setEnabled(false);
+ loadHostPort();
+ }
+
+ private void setupProfileSpinner() {
+ profileList = new ArrayList<>();
+ profileList.add(new ProfileItem(getString(R.string.profile_manual), "", -1, MyApplication.ProxyType.HTTP));
+ profileList.addAll(MyApplication.getInstance().loadProfiles());
+
+ profileAdapter = new ArrayAdapter<>(this, R.layout.spinner_item, profileList);
+ profileAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
+ profileSpinner.setAdapter(profileAdapter);
+
+ profileSpinner.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
+ @Override
+ public void onItemSelected(AdapterView> parent, View view, int position, long id) {
+ hostPortEditText.setError(null);
+ if (position == 0) { // "Manual"
+ hostPortEditText.setText("");
+ proxyTypeSpinner.setSelection(MyApplication.ProxyType.HTTP.ordinal());
+ } else if (position > 0) { // Not "Manual"
+ ProfileItem profile = profileList.get(position);
+ hostPortEditText.setText(HostPortPair.valueOf(profile.getHost(), profile.getPort()));
+ proxyTypeSpinner.setSelection(profile.getType().ordinal());
+ }
+ }
+
+ @Override
+ public void onNothingSelected(AdapterView> parent) {
+ }
+ });
+ }
+
+ @Override
+ public boolean onCreateOptionsMenu(Menu menu) {
+ // Inflate the menu; this adds items to the action bar if it is present.
+ getMenuInflater().inflate(R.menu.menu_main, menu);
+ return true;
+ }
+
+ @Override
+ public boolean onPrepareOptionsMenu(Menu menu) {
+ MenuItem itemSettings = menu.findItem(R.id.action_activity_settings);
+ MenuItem itemProfile = menu.findItem(R.id.action_profile_settings);
+ boolean enabled = this.startButton.isEnabled();
+ if (itemSettings != null) itemSettings.setEnabled(enabled);
+ if (itemProfile != null) itemProfile.setEnabled(enabled);
+ return true;
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ int item_id = item.getItemId();
+ if (item_id == R.id.action_activity_settings) {
+ Intent intent = new android.content.Intent(this, SettingsActivity.class);
+ startActivity(intent);
+ } else if (item_id == R.id.action_profile_settings) {
+ Intent intent = new Intent(this, ProfileSettingsActivity.class);
+ startActivity(intent);
+ } else if (item_id == R.id.action_show_about) {
+ new AlertDialog.Builder(this)
+ .setTitle(getString(R.string.app_name) + getVersionName())
+ .setMessage(R.string.app_name)
+ .show();
+ } else {
+ return super.onOptionsItemSelected(item);
+ }
+ return true;
+ }
+
+ protected String getVersionName() {
+ PackageManager packageManager = getPackageManager();
+ if (packageManager == null) {
+ return null;
+ }
+
+ try {
+ return packageManager.getPackageInfo(getPackageName(), 0).versionName;
+ } catch (PackageManager.NameNotFoundException e) {
+ return null;
+ }
+ }
+
+ @Override
+ protected void onResume() {
+ super.onResume();
+ startButton.setEnabled(false);
+ stopButton.setEnabled(false);
+ updateStatus();
+
+ refreshProfileSpinner();
+
+ statusHandler.post(statusRunnable);
+
+ Intent intent = new Intent(this, Tun2HttpVpnService.class);
+ bindService(intent, serviceConnection, Context.BIND_AUTO_CREATE);
+ }
+
+ private void refreshProfileSpinner() {
+ if (profileList != null) {
+ int selected = profileSpinner.getSelectedItemPosition();
+ profileList.clear();
+ profileList.add(new ProfileItem(getString(R.string.profile_manual), "", -1, MyApplication.ProxyType.HTTP));
+ profileList.addAll(MyApplication.getInstance().loadProfiles());
+ profileAdapter.notifyDataSetChanged();
+ if (selected < profileList.size()) {
+ profileSpinner.setSelection(selected);
+ }
+ }
+ }
+
+ boolean isRunning() {
+ return service != null && service.isRunning();
+ }
+
+ @Override
+ protected void onPause() {
+ super.onPause();
+ statusHandler.removeCallbacks(statusRunnable);
+ unbindService(serviceConnection);
+ }
+
+ Runnable statusRunnable = new Runnable() {
+ @Override
+ public void run() {
+ updateStatus();
+ statusHandler.post(statusRunnable);
+ }
+ };
+
+ void updateStatus() {
+ if (service == null) {
+ return;
+ }
+ boolean running = isRunning();
+ if (running) {
+ startButton.setEnabled(false);
+ hostPortEditText.setEnabled(false);
+ proxyTypeSpinner.setEnabled(false);
+ profileSpinner.setEnabled(false);
+ stopButton.setEnabled(true);
+ } else {
+ startButton.setEnabled(true);
+ hostPortEditText.setEnabled(true);
+ proxyTypeSpinner.setEnabled(true);
+ profileSpinner.setEnabled(true);
+ stopButton.setEnabled(false);
+ }
+ }
+
+ private void stopVpn() {
+ startButton.setEnabled(true);
+ stopButton.setEnabled(false);
+ Tun2HttpVpnService.stop(this);
+ }
+
+ private void startVpn() {
+ final MyApplication app = MyApplication.getInstance();
+ assert app != null;
+ final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this);
+ boolean connectivityCheck = app.loadProxyConnectivityCheck(false);
+
+ if (parseAndSaveHostPort()) {
+ String host = prefs.getString(Tun2HttpVpnService.PREF_PROXY_HOST, "");
+ int port = prefs.getInt(Tun2HttpVpnService.PREF_PROXY_PORT, -1);
+
+ new Thread(() -> {
+ boolean resolvable = NetUtil.resolvHost(host);
+ if (!resolvable) {
+ runOnUiThread(() -> {
+ Toast.makeText(this, "DNS resolution failed for " + host, Toast.LENGTH_SHORT).show();
+ });
+ return;
+ }
+ if (connectivityCheck) {
+ boolean reachable = false;
+ if (NetUtil.isNetworkCapabilities(service)) {
+ reachable = NetUtil.isConnection(host, port);
+ }
+ if (!reachable) {
+ runOnUiThread(() -> {
+ Toast.makeText(this, "Connectivity check failed", Toast.LENGTH_SHORT).show();
+ });
+ return;
+ }
+ }
+
+ runOnUiThread(this::proceedWithStartVpn);
+ }).start();
+ }
+ }
+
+ private void proceedWithStartVpn() {
+ Intent i = VpnService.prepare(this);
+ if (i != null) {
+ vpnRequestLauncher.launch(i);
+ } else {
+ vpnPrepared();
+ }
+ }
+
+ private void vpnPrepared() {
+ startButton.setEnabled(false);
+ stopButton.setEnabled(true);
+ Tun2HttpVpnService.start(this);
+ }
+
+ private void loadHostPort() {
+ final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this);
+ final String proxyHost = prefs.getString(Tun2HttpVpnService.PREF_PROXY_HOST, "");
+ int proxyPort = prefs.getInt(Tun2HttpVpnService.PREF_PROXY_PORT, -1);
+ String proxyTypeName = prefs.getString(Tun2HttpVpnService.PREF_PROXY_TYPE, MyApplication.ProxyType.HTTP.name());
+ MyApplication.ProxyType proxyType = Enum.valueOf(MyApplication.ProxyType.class, proxyTypeName);
+
+ if (!NetUtil.isValidHost(proxyHost) || !NetUtil.isValiPort(proxyPort)) {
+ return;
+ }
+ hostPortEditText.setText(HostPortPair.valueOf(proxyHost, proxyPort));
+ proxyTypeSpinner.setSelection(proxyType.ordinal());
+ }
+
+ private boolean parseAndSaveHostPort() {
+ String proxyTarget = hostPortEditText.getText().toString();
+ if (!NetUtil.isValidHostPort(proxyTarget)) {
+ hostPortEditText.setError(getString(R.string.enter_host));
+ return false;
+ }
+ try {
+ // host:port 分離
+ HostPortPair hostPortPair = HostPortPair.parse(proxyTarget);
+ String host = hostPortPair.getHost();
+ int port = hostPortPair.getPort();
+ String proxyType = (String) proxyTypeSpinner.getSelectedItem();
+ SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this);
+ SharedPreferences.Editor edit = prefs.edit();
+ edit.putString(Tun2HttpVpnService.PREF_PROXY_HOST, host);
+ edit.putInt(Tun2HttpVpnService.PREF_PROXY_PORT, port);
+ edit.putString(Tun2HttpVpnService.PREF_PROXY_TYPE, proxyType);
+ edit.apply();
+ } catch (NumberFormatException e) {
+ hostPortEditText.setError(getString(R.string.enter_host));
+ return false;
+ }
+ return true;
+ }
+
+
+}
\ No newline at end of file
diff --git a/android_app/app/src/main/java/tun/proxy/MyApplication.java b/android_app/app/src/main/java/tun/proxy/MyApplication.java
new file mode 100644
index 00000000..d1c301c7
--- /dev/null
+++ b/android_app/app/src/main/java/tun/proxy/MyApplication.java
@@ -0,0 +1,166 @@
+package tun.proxy;
+
+import android.app.Application;
+import android.content.SharedPreferences;
+
+import androidx.preference.PreferenceManager;
+
+import java.util.HashSet;
+import java.util.Set;
+
+public class MyApplication extends Application {
+ private static final String TAG = "MainActivity";
+
+ private final static String PREF_VPN_MODE = "pref_vpn_connection_mode";
+ private final static String[] PREF_APP_KEY = {"pref_vpn_disallowed_application", "pref_vpn_allowed_application"};
+
+ private static MyApplication instance;
+
+ public static MyApplication getInstance() {
+ return instance;
+ }
+
+ @Override
+ public void onCreate() {
+ super.onCreate();
+ instance = this;
+ }
+
+ /*
+ * Profile setting
+ */
+ private static final String PREF_PROFILES = "pref_profiles";
+
+ public java.util.List loadProfiles() {
+ final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(getApplicationContext());
+ String profilesJson = prefs.getString(PREF_PROFILES, "[]");
+ java.util.List profiles = new java.util.ArrayList<>();
+ try {
+ org.json.JSONArray array = new org.json.JSONArray(profilesJson);
+ for (int i = 0; i < array.length(); i++) {
+ profiles.add(ProfileItem.fromJSONObject(array.getJSONObject(i)));
+ }
+ } catch (org.json.JSONException e) {
+ android.util.Log.e(TAG, "Error loading profiles", e);
+ }
+ return profiles;
+ }
+
+ public void storeProfiles(java.util.List profiles) {
+ final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(getApplicationContext());
+ org.json.JSONArray array = new org.json.JSONArray();
+ try {
+ for (ProfileItem profile : profiles) {
+ array.put(profile.toJSONObject());
+ }
+ prefs.edit().putString(PREF_PROFILES, array.toString()).apply();
+ } catch (org.json.JSONException e) {
+ android.util.Log.e(TAG, "Error storing profiles", e);
+ }
+ }
+
+ /*
+ * VPN connection setting
+ */
+
+ public VPNMode loadVPNMode() {
+ final SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(getApplicationContext());
+ final String vpn_mode = sharedPreferences.getString(PREF_VPN_MODE, MyApplication.VPNMode.DISALLOW.name());
+ return VPNMode.valueOf(vpn_mode);
+ }
+
+ public void storeVPNMode(VPNMode mode) {
+ final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(getApplicationContext());
+ final SharedPreferences.Editor editor = prefs.edit();
+ editor.putString(PREF_VPN_MODE, mode.name()).apply();
+ }
+
+ public Set loadVPNApplication(VPNMode mode) {
+ final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(getApplicationContext());
+ final Set preference = prefs.getStringSet(PREF_APP_KEY[mode.ordinal()], new HashSet());
+ return preference;
+ }
+
+ public void storeVPNApplication(VPNMode mode, final Set set) {
+ final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(getApplicationContext());
+ final SharedPreferences.Editor editor = prefs.edit();
+ editor.putStringSet(PREF_APP_KEY[mode.ordinal()], set).apply();
+ }
+
+ public enum VPNMode {DISALLOW, ALLOW}
+
+ public enum AppSortBy {APPNAME, PKGNAME}
+
+ public enum AppOrderBy {ASC, DESC}
+
+ public enum ProxyType {HTTP, SOCKS5}
+
+ /*
+ * Proxy setting
+ */
+ public static final String PREF_PROXY_CONNECTIVITY_CHECK = "pref_proxy_connectivity_check";
+ public static final String PREF_RUNNING = "pref_proxy_running";
+
+ public boolean loadProxyConnectivityCheck(boolean defValue) {
+ final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(getApplicationContext());
+ return prefs.getBoolean(PREF_PROXY_CONNECTIVITY_CHECK, defValue);
+ }
+
+ public void storeProxyConnectivityCheck(boolean connectivity) {
+ final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(getApplicationContext());
+ final SharedPreferences.Editor editor = prefs.edit();
+ editor.putBoolean(PREF_PROXY_CONNECTIVITY_CHECK, connectivity).apply();
+ }
+
+ public boolean loadProxyRunning(boolean defValue) {
+ final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(getApplicationContext());
+ return prefs.getBoolean(PREF_RUNNING, defValue);
+ }
+
+ public void storeProxyRunning(boolean running) {
+ final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(getApplicationContext());
+ final SharedPreferences.Editor editor = prefs.edit();
+ editor.putBoolean(PREF_RUNNING, running).apply();
+ }
+
+ /*
+ * DNS setting
+ */
+
+ private static final String PREF_DNS_USE_CUSTOM = "pref_dns_use_custom";
+ public static final String PREF_DNS_PRIMARY = "pref_dns_primary";
+ public static final String PREF_DNS_SECONDARY = "pref_dns_secondary";
+
+ public boolean loadUseDnsCustom() {
+ final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(getApplicationContext());
+ return prefs.getBoolean(PREF_DNS_USE_CUSTOM, false);
+ }
+
+ public void storeUseDnsCustom(boolean isUseDnsCustom) {
+ final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(getApplicationContext());
+ final SharedPreferences.Editor editor = prefs.edit();
+ editor.putBoolean(PREF_DNS_USE_CUSTOM, isUseDnsCustom).apply();
+ }
+
+ public String loadPrimaryDns(String defValue) {
+ final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(getApplicationContext());
+ return prefs.getString(PREF_DNS_PRIMARY, defValue);
+ }
+
+ public void storePrimaryDns(String primaryDns) {
+ final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(getApplicationContext());
+ final SharedPreferences.Editor editor = prefs.edit();
+ editor.putString(PREF_DNS_PRIMARY, primaryDns).apply();
+ }
+
+ public String loadSecondaryDns(String defValue) {
+ final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(getApplicationContext());
+ return prefs.getString(PREF_DNS_SECONDARY, defValue);
+ }
+
+ public void storeSecondaryDns(String secondaryDns) {
+ final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(getApplicationContext());
+ final SharedPreferences.Editor editor = prefs.edit();
+ editor.putString(PREF_DNS_SECONDARY, secondaryDns).apply();
+ }
+}
diff --git a/android_app/app/src/main/java/tun/proxy/ProfileItem.java b/android_app/app/src/main/java/tun/proxy/ProfileItem.java
new file mode 100644
index 00000000..a8f829c8
--- /dev/null
+++ b/android_app/app/src/main/java/tun/proxy/ProfileItem.java
@@ -0,0 +1,95 @@
+package tun.proxy;
+
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import java.io.Serializable;
+
+import tun.proxy.service.Tun2HttpVpnService;
+
+public class ProfileItem implements Serializable {
+ private String name;
+ // private String host;
+// private int port;
+ private final HostPortPair hostPort;
+ private MyApplication.ProxyType type;
+
+ public ProfileItem(String name, String host, int port, MyApplication.ProxyType type) {
+ this.name = name;
+ this.hostPort = new HostPortPair(host, port);
+ this.type = type;
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public void setName(String name) {
+ this.name = name;
+ }
+
+ public String getHost() {
+ return this.hostPort.getHost();
+ }
+
+ public void setHost(String host) {
+ this.hostPort.setHost(host);
+ }
+
+ public int getPort() {
+ return this.hostPort.getPort();
+ }
+
+ public void setPort(int port) {
+ this.hostPort.setPort(port);
+ }
+
+ public MyApplication.ProxyType getType() {
+ return type;
+ }
+
+ public void setType(MyApplication.ProxyType type) {
+ this.type = type;
+ }
+
+ public JSONObject toJSONObject() throws JSONException {
+ JSONObject obj = new JSONObject();
+ obj.put("name", name);
+ obj.put("host", this.hostPort.getHost());
+ obj.put("port", this.hostPort.getPort());
+ obj.put("type", type.name());
+ return obj;
+ }
+
+ public static ProfileItem fromJSONObject(JSONObject obj) throws JSONException {
+ return new ProfileItem(
+ obj.getString("name"),
+ obj.getString("host"),
+ obj.getInt("port"),
+ MyApplication.ProxyType.valueOf(obj.getString("type"))
+ );
+ }
+
+ @Override
+ public String toString() {
+ return name;
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ ProfileItem other = (ProfileItem) obj;
+ if (other == null)
+ return false;
+
+ if (!this.name.equals(other.name))
+ return false;
+
+ if (!this.hostPort.equals(other.hostPort))
+ return false;
+
+ if (this.type != other.getType())
+ return false;
+
+ return true;
+ }
+}
diff --git a/android_app/app/src/main/java/tun/proxy/ProfileSettingsActivity.java b/android_app/app/src/main/java/tun/proxy/ProfileSettingsActivity.java
new file mode 100644
index 00000000..d06cf811
--- /dev/null
+++ b/android_app/app/src/main/java/tun/proxy/ProfileSettingsActivity.java
@@ -0,0 +1,194 @@
+package tun.proxy;
+
+import android.graphics.Color;
+import android.os.Bundle;
+import android.view.LayoutInflater;
+import android.view.Menu;
+import android.view.MenuItem;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ArrayAdapter;
+import android.widget.EditText;
+import android.widget.ImageButton;
+import android.widget.ListView;
+import android.widget.Spinner;
+import android.widget.TextView;
+
+import androidx.activity.EdgeToEdge;
+import androidx.activity.SystemBarStyle;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.appcompat.app.ActionBar;
+import androidx.appcompat.app.AlertDialog;
+import androidx.appcompat.app.AppCompatActivity;
+import androidx.core.graphics.Insets;
+import androidx.core.view.ViewCompat;
+import androidx.core.view.WindowInsetsCompat;
+
+import java.util.List;
+import java.util.Locale;
+
+import tun.utils.NetUtil;
+
+public class ProfileSettingsActivity extends AppCompatActivity {
+
+ private ArrayAdapter adapter;
+ private List profileList;
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ EdgeToEdge.enable(this, SystemBarStyle.dark(Color.TRANSPARENT));
+ setContentView(R.layout.activity_profile_settings);
+
+ ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.profile_setting_container), (v, insets) -> {
+ Insets systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars());
+ v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom);
+ return WindowInsetsCompat.CONSUMED;
+ });
+
+ ActionBar actionBar = getSupportActionBar();
+ if (actionBar != null) {
+ actionBar.setDisplayHomeAsUpEnabled(true);
+ actionBar.setTitle(R.string.title_profile_settings);
+ }
+
+ ListView listView = findViewById(R.id.profile_list);
+ profileList = MyApplication.getInstance().loadProfiles();
+ adapter = new ArrayAdapter(this, R.layout.item_profile, profileList) {
+ @NonNull
+ @Override
+ public View getView(int position, @Nullable View convertView, @NonNull ViewGroup parent) {
+ if (convertView == null) {
+ convertView = LayoutInflater.from(getContext()).inflate(R.layout.item_profile, parent, false);
+ }
+ ProfileItem item = getItem(position);
+ if (item != null) {
+ TextView textName = convertView.findViewById(R.id.text_name);
+ TextView textHostPort = convertView.findViewById(R.id.text_host_port);
+ TextView textType = convertView.findViewById(R.id.text_type);
+ ImageButton btnEdit = convertView.findViewById(R.id.btn_edit);
+ ImageButton btnDelete = convertView.findViewById(R.id.btn_delete);
+
+ textName.setText(item.getName());
+ textHostPort.setText(HostPortPair.valueOf(item.getHost(), item.getPort()));
+ textType.setText(item.getType().name());
+
+ boolean running = MyApplication.getInstance().loadProxyRunning(false);
+ btnEdit.setEnabled(!running);
+ btnDelete.setEnabled(!running);
+
+ btnEdit.setOnClickListener(v -> showProfileEditDialog(item, position));
+ btnDelete.setOnClickListener(v -> {
+ new AlertDialog.Builder(ProfileSettingsActivity.this)
+ .setTitle(R.string.profile_delete_title)
+ .setMessage(R.string.profile_delete_msg)
+ .setPositiveButton(android.R.string.ok, (dialog, which) -> {
+ profileList.remove(position);
+ MyApplication.getInstance().storeProfiles(profileList);
+ notifyDataSetChanged();
+ })
+ .setNegativeButton(android.R.string.cancel, null)
+ .show();
+ });
+ }
+ return convertView;
+ }
+ };
+ listView.setAdapter(adapter);
+ }
+
+ @Override
+ public boolean onCreateOptionsMenu(Menu menu) {
+ getMenuInflater().inflate(R.menu.menu_profile_settings, menu);
+ return true;
+ }
+
+ @Override
+ public boolean onPrepareOptionsMenu(Menu menu) {
+ MenuItem item = menu.findItem(R.id.action_add_profile);
+ if (item != null) {
+ boolean running = MyApplication.getInstance().loadProxyRunning(false);
+ item.setEnabled(!running);
+ }
+ return super.onPrepareOptionsMenu(menu);
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ if (item.getItemId() == R.id.action_add_profile) {
+ showProfileEditDialog(null, -1);
+ return true;
+ }
+ return super.onOptionsItemSelected(item);
+ }
+
+ private void showProfileEditDialog(ProfileItem profile, int position) {
+ View dialogView = LayoutInflater.from(this).inflate(R.layout.dialog_profile_edit, null);
+ EditText editName = dialogView.findViewById(R.id.edit_name);
+ EditText editHostPort = dialogView.findViewById(R.id.edit_host_port);
+ Spinner spinnerType = dialogView.findViewById(R.id.spinner_type);
+ TextView textError = dialogView.findViewById(R.id.text_error);
+
+ if (profile != null) {
+ editName.setText(profile.getName());
+ editHostPort.setText(HostPortPair.valueOf(profile.getHost(), profile.getPort()));
+ spinnerType.setSelection(profile.getType().ordinal());
+ }
+
+ AlertDialog dialog = new AlertDialog.Builder(this)
+ .setTitle(profile == null ? R.string.profile_new_title : R.string.profile_edit_title)
+ .setView(dialogView)
+ .setPositiveButton(android.R.string.ok, null)
+ .setNegativeButton(android.R.string.cancel, null)
+ .create();
+
+ dialog.show();
+
+ dialog.getButton(AlertDialog.BUTTON_POSITIVE).setOnClickListener(v -> {
+ String name = editName.getText().toString();
+ String hostPort = editHostPort.getText().toString();
+ if (name.isEmpty()) {
+ textError.setText(R.string.profile_error_name_fields);
+ textError.setVisibility(View.VISIBLE);
+ return;
+ }
+ if (!NetUtil.isValidHostPort(hostPort)) {
+ textError.setText(R.string.profile_error_format);
+ textError.setVisibility(View.VISIBLE);
+ return;
+ }
+ try {
+ HostPortPair hostPortPair = HostPortPair.parse(hostPort);
+ String host = hostPortPair.getHost();
+ int port = hostPortPair.getPort();
+ if (!NetUtil.isValiPort(port)) {
+ textError.setText(R.string.profile_error_port);
+ textError.setVisibility(View.VISIBLE);
+ return;
+ }
+ MyApplication.ProxyType type = MyApplication.ProxyType.values()[spinnerType.getSelectedItemPosition()];
+ if (profile == null) {
+ profileList.add(new ProfileItem(name, host, port, type));
+ } else {
+ profile.setName(name);
+ profile.setHost(host);
+ profile.setPort(port);
+ profile.setType(type);
+ }
+ MyApplication.getInstance().storeProfiles(profileList);
+ adapter.notifyDataSetChanged();
+ dialog.dismiss();
+ } catch (IllegalArgumentException e) {
+ textError.setText(R.string.profile_error_format);
+ textError.setVisibility(View.VISIBLE);
+ }
+ });
+ }
+
+ @Override
+ public boolean onSupportNavigateUp() {
+ finish();
+ return true;
+ }
+}
diff --git a/android_app/app/src/main/java/tun/proxy/SettingsActivity.java b/android_app/app/src/main/java/tun/proxy/SettingsActivity.java
new file mode 100644
index 00000000..8ea68904
--- /dev/null
+++ b/android_app/app/src/main/java/tun/proxy/SettingsActivity.java
@@ -0,0 +1,900 @@
+package tun.proxy;
+
+import android.content.Context;
+import android.content.DialogInterface;
+import android.content.Intent;
+import android.content.SharedPreferences;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.PackageInfo;
+import android.content.pm.PackageManager;
+//import android.os.AsyncTask;
+import android.graphics.Color;
+import android.os.Build;
+import android.os.Bundle;
+
+import androidx.activity.EdgeToEdge;
+import androidx.activity.SystemBarStyle;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.appcompat.app.ActionBar;
+import androidx.appcompat.app.AlertDialog;
+import androidx.appcompat.app.AppCompatActivity;
+import androidx.core.graphics.Insets;
+import androidx.core.view.ViewCompat;
+import androidx.core.view.WindowInsetsCompat;
+import androidx.core.view.MenuProvider;
+import androidx.fragment.app.DialogFragment;
+import androidx.fragment.app.Fragment;
+import androidx.fragment.app.FragmentManager;
+import androidx.lifecycle.Lifecycle;
+import androidx.preference.*;
+
+import android.text.InputType;
+import android.view.Menu;
+import android.view.MenuInflater;
+import android.view.MenuItem;
+import android.view.View;
+import android.widget.*;
+
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.EnumSet;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Set;
+
+import tun.utils.NetUtil;
+import tun.utils.ProgressTask;
+
+public class SettingsActivity extends AppCompatActivity implements
+ PreferenceFragmentCompat.OnPreferenceStartFragmentCallback {
+ private static final String TAG = "SettingsActivity";
+ private static final String TITLE_TAG = "Settings";
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ EdgeToEdge.enable(this, SystemBarStyle.dark(Color.TRANSPARENT));
+ setContentView(R.layout.activity_settings);
+ ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.settings_container), (v, insets) -> {
+ Insets systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars());
+ v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom);
+ return WindowInsetsCompat.CONSUMED;
+ });
+ if (savedInstanceState == null) {
+ getSupportFragmentManager()
+ .beginTransaction()
+ .replace(R.id.activity_settings, new SettingsFragment(), "preference_root")
+ .commit();
+ } else {
+ setTitle(savedInstanceState.getCharSequence(TITLE_TAG));
+ }
+ getSupportFragmentManager().addOnBackStackChangedListener(new FragmentManager.OnBackStackChangedListener() {
+ @Override
+ public void onBackStackChanged() {
+ if (getSupportFragmentManager().getBackStackEntryCount() == 0) {
+ setTitle(R.string.title_activity_settings);
+ }
+ }
+ });
+ ActionBar actionBar = getSupportActionBar();
+ if (actionBar != null) {
+ actionBar.setDisplayHomeAsUpEnabled(true);
+ }
+ }
+
+ @Override
+ public void onSaveInstanceState(@NonNull Bundle outState) {
+ super.onSaveInstanceState(outState);
+ outState.putCharSequence(TITLE_TAG, getTitle());
+ }
+
+ @Override
+ public boolean onSupportNavigateUp() {
+ if (getSupportFragmentManager().popBackStackImmediate()) {
+ return true;
+ }
+ return super.onSupportNavigateUp();
+ }
+
+ @Override
+ public boolean onPreferenceStartFragment(@NonNull PreferenceFragmentCompat caller, Preference pref) {
+ final Bundle args = pref.getExtras();
+ assert pref.getFragment() != null;
+ final Fragment fragment = getSupportFragmentManager().getFragmentFactory().instantiate(getClassLoader(), pref.getFragment());
+ fragment.setArguments(args);
+ getSupportFragmentManager().beginTransaction()
+ .replace(R.id.activity_settings, fragment)
+ .addToBackStack(null)
+ .commit();
+ setTitle(pref.getTitle());
+ return true;
+ }
+
+ public enum FilterAppType {
+ SYSTEM_APP,
+ OS_APP;
+
+ public static EnumSet parseEnumSet(String s) {
+ EnumSet filterType = EnumSet.noneOf(FilterAppType.class);
+ if (!s.startsWith("[") && s.endsWith("]")) {
+ throw new IllegalArgumentException("No enum constant " + FilterAppType.class.getCanonicalName() + "." + s);
+ }
+ String content = s.substring(1, s.length() - 1).trim();
+ if (content.isEmpty()) {
+ return filterType;
+ }
+ for (String t : content.split(",")) {
+ String v = t.trim();
+ filterType.add(Enum.valueOf(FilterAppType.class, v.replace("\"", "")));
+ }
+ return filterType;
+ }
+
+ }
+
+ /**
+ * Inner Classes.
+ */
+
+ public static class SettingsFragment extends PreferenceFragmentCompat implements Preference.OnPreferenceClickListener {
+
+ /*
+ * VPN connection setting
+ */
+ public static final String VPN_CONNECTION_MODE = "vpn_connection_mode";
+ public static final String VPN_DISALLOWED_APPLICATION_LIST = "vpn_disallowed_application_list";
+ public static final String VPN_ALLOWED_APPLICATION_LIST = "vpn_allowed_application_list";
+ public static final String VPN_CLEAR_ALL_SELECTION = "vpn_dialog_clear_all_selection";
+ public static final String PROXY_CONNECTIVITY_CHECK = "proxy_connectivity_check";
+
+ /*
+ * DNS setting
+ */
+ public static final String DNS_USE_CUSTOM = "dns_use_custom";
+ public static final String DNS_PRIMARY = "dns_primary";
+ public static final String DNS_SECONDARY = "dns_secondary";
+
+ @Override
+ public void onCreatePreferences(Bundle savedInstanceState, String rootKey) {
+ addPreferencesFromResource(R.xml.preferences);
+
+ final MyApplication app = MyApplication.getInstance();
+ assert app != null;
+
+ /*
+ * VPN connection setting
+ */
+
+ /* Allowed / Disallowed Application */
+ final ListPreference prefVpnMode = this.findPreference(VPN_CONNECTION_MODE);
+ assert prefVpnMode != null;
+ final PreferenceScreen prefDisallow = findPreference(VPN_DISALLOWED_APPLICATION_LIST);
+ assert prefDisallow != null;
+ final PreferenceScreen prefAllow = findPreference(VPN_ALLOWED_APPLICATION_LIST);
+ assert prefAllow != null;
+ final PreferenceScreen clearAllSelection = findPreference(VPN_CLEAR_ALL_SELECTION);
+ assert clearAllSelection != null;
+ clearAllSelection.setOnPreferenceClickListener(this);
+
+ prefVpnMode.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() {
+ @Override
+ public boolean onPreferenceChange(Preference preference, Object value) {
+ if (preference instanceof ListPreference) {
+ final ListPreference listPreference = (ListPreference) preference;
+ int index = listPreference.findIndexOfValue((String) value);
+ prefDisallow.setEnabled(index == MyApplication.VPNMode.DISALLOW.ordinal());
+ prefAllow.setEnabled(index == MyApplication.VPNMode.ALLOW.ordinal());
+
+ // Set the summary to reflect the new value.
+ preference.setSummary(index >= 0 ? listPreference.getEntries()[index] : null);
+
+ MyApplication.VPNMode mode = MyApplication.VPNMode.values()[index];
+ app.storeVPNMode(mode);
+ }
+ return true;
+ }
+ });
+ prefVpnMode.setSummary(prefVpnMode.getEntry());
+ prefDisallow.setEnabled(MyApplication.VPNMode.DISALLOW.name().equals(prefVpnMode.getValue()));
+ prefAllow.setEnabled(MyApplication.VPNMode.ALLOW.name().equals(prefVpnMode.getValue()));
+
+ /*
+ * Proxy setting
+ */
+ final SwitchPreference prefConnectionChek = this.findPreference(PROXY_CONNECTIVITY_CHECK);
+ assert prefConnectionChek != null;
+ prefConnectionChek.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() {
+ @Override
+ public boolean onPreferenceChange(@NonNull Preference preference, Object newValue) {
+ if (preference instanceof SwitchPreference) {
+ if (newValue instanceof Boolean) {
+ app.storeProxyConnectivityCheck((Boolean) newValue);
+ }
+ }
+ return true;
+ }
+ });
+
+ /*
+ * DNS setting
+ */
+
+ final SwitchPreference prefUseDns = this.findPreference(DNS_USE_CUSTOM);
+ assert prefUseDns != null;
+ prefUseDns.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() {
+ @Override
+ public boolean onPreferenceChange(@NonNull Preference preference, Object newValue) {
+ if (preference instanceof SwitchPreference) {
+ if (newValue instanceof Boolean) {
+ app.storeUseDnsCustom((Boolean) newValue);
+ }
+ }
+ return true;
+ }
+ });
+ final EditTextPreference prefDnsPrimary = this.findPreference(DNS_PRIMARY);
+ assert prefDnsPrimary != null;
+ prefDnsPrimary.setOnBindEditTextListener(new EditTextPreference.OnBindEditTextListener() {
+ @Override
+ public void onBindEditText(@NonNull EditText editText) {
+ editText.setSingleLine(true);
+ editText.setInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_URI);
+ }
+ });
+ prefDnsPrimary.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() {
+ @Override
+ public boolean onPreferenceChange(@NonNull Preference preference, Object newValue) {
+ if (newValue instanceof String) {
+ String ipAddress = (String) newValue;
+ if (NetUtil.isValidIPv4Address(ipAddress) || NetUtil.isValidIPv6Address(ipAddress)) {
+ app.storePrimaryDns(ipAddress);
+ return true;
+ }
+ }
+ return false;
+ }
+ });
+ final EditTextPreference prefDnsSecondary = this.findPreference(DNS_SECONDARY);
+ assert prefDnsSecondary != null;
+ prefDnsSecondary.setOnBindEditTextListener(new EditTextPreference.OnBindEditTextListener() {
+ @Override
+ public void onBindEditText(@NonNull EditText editText) {
+ editText.setSingleLine(true);
+ editText.setInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_URI);
+ }
+ });
+ prefDnsSecondary.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() {
+ @Override
+ public boolean onPreferenceChange(@NonNull Preference preference, Object newValue) {
+ if (newValue instanceof String) {
+ String ipAddress = (String) newValue;
+ if (NetUtil.isValidIPv4Address(ipAddress) || NetUtil.isValidIPv6Address(ipAddress)) {
+ app.storeSecondaryDns(ipAddress);
+ return true;
+ }
+ }
+ return false;
+ }
+ });
+ updateMenuItem();
+ }
+
+ @Override
+ public void onDisplayPreferenceDialog(@NonNull Preference preference) {
+ if (getParentFragmentManager().findFragmentByTag("androidx.preference.PreferenceFragment.DIALOG") != null ||
+ getChildFragmentManager().findFragmentByTag("androidx.preference.PreferenceFragment.DIALOG") != null) {
+ return;
+ }
+
+ if (preference instanceof EditTextPreference &&
+ (DNS_PRIMARY.equals(preference.getKey()) || DNS_SECONDARY.equals(preference.getKey()))) {
+ DialogFragment f = DnsEditTextPreferenceDialogFragment.newInstance(preference.getKey());
+ f.setTargetFragment(this, 0);
+ f.show(getParentFragmentManager(), "androidx.preference.PreferenceFragment.DIALOG");
+ } else {
+ super.onDisplayPreferenceDialog(preference);
+ }
+ }
+
+ private void updateMenuItem() {
+ final PreferenceScreen prefDisallow = findPreference(VPN_DISALLOWED_APPLICATION_LIST);
+ assert prefDisallow != null;
+ final PreferenceScreen prefAllow = findPreference(VPN_ALLOWED_APPLICATION_LIST);
+ assert prefAllow != null;
+
+ final MyApplication app = MyApplication.getInstance();
+ assert app != null;
+ int countDisallow = app.loadVPNApplication(MyApplication.VPNMode.DISALLOW).size();
+ int countAllow = app.loadVPNApplication(MyApplication.VPNMode.ALLOW).size();
+ prefDisallow.setTitle(getString(R.string.vpn_disallowed_application_list));
+ prefDisallow.setSummary(getString(R.string.vpn_disallowed_application_count) + String.format(Locale.ROOT, " %d", countDisallow));
+ prefAllow.setTitle(getString(R.string.vpn_allowed_application_list));
+ prefAllow.setSummary(getString(R.string.vpn_allowed_application_count) + String.format(Locale.ROOT, " %d", countAllow));
+ }
+
+ /*
+ * https://developer.android.com/guide/topics/ui/settings/organize-your-settings
+ */
+
+ // リスナー部分
+ @Override
+ public boolean onPreferenceClick(Preference preference) {
+ final MyApplication app = MyApplication.getInstance();
+ assert app != null;
+
+ // keyを見てクリックされたPreferenceを特定
+ switch (preference.getKey()) {
+ case VPN_DISALLOWED_APPLICATION_LIST:
+ case VPN_ALLOWED_APPLICATION_LIST:
+ break;
+ case VPN_CLEAR_ALL_SELECTION: {
+ new AlertDialog.Builder(requireActivity())
+ .setTitle(getString(R.string.title_activity_settings))
+ .setMessage(getString(R.string.vpn_dialog_clear_all_application_msg))
+ .setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ Set set = new HashSet<>();
+ app.storeVPNApplication(MyApplication.VPNMode.ALLOW, set);
+ app.storeVPNApplication(MyApplication.VPNMode.DISALLOW, set);
+ updateMenuItem();
+ }
+ })
+ .setNegativeButton(android.R.string.cancel, null)
+ .show();
+ break;
+ }
+ }
+ return false;
+ }
+
+ }
+
+ public static class DnsEditTextPreferenceDialogFragment extends EditTextPreferenceDialogFragmentCompat {
+ public static DnsEditTextPreferenceDialogFragment newInstance(String key) {
+ DnsEditTextPreferenceDialogFragment fragment = new DnsEditTextPreferenceDialogFragment();
+ Bundle b = new Bundle(1);
+ b.putString(ARG_KEY, key);
+ fragment.setArguments(b);
+ return fragment;
+ }
+
+ @Override
+ public void onStart() {
+ super.onStart();
+ final AlertDialog dialog = (AlertDialog) getDialog();
+ if (dialog != null) {
+ final Button okButton = dialog.getButton(AlertDialog.BUTTON_POSITIVE);
+ okButton.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ EditText editText = dialog.findViewById(android.R.id.edit);
+ if (editText != null) {
+ String value = editText.getText().toString();
+ if (NetUtil.isValidIPv4Address(value) || NetUtil.isValidIPv6Address(value)) {
+ DnsEditTextPreferenceDialogFragment.this.onClick(dialog, DialogInterface.BUTTON_POSITIVE);
+ dialog.dismiss();
+ } else {
+ editText.setError(getContext().getString(R.string.invalid_ip_address));
+ Toast.makeText(getContext(), R.string.invalid_ip_address, Toast.LENGTH_SHORT).show();
+ }
+ } else {
+ DnsEditTextPreferenceDialogFragment.this.onClick(dialog, DialogInterface.BUTTON_POSITIVE);
+ dialog.dismiss();
+ }
+ }
+ });
+ }
+ }
+ }
+
+ public static class DisallowedPackageListFragment extends PackageListFragment {
+ public DisallowedPackageListFragment() {
+ super(MyApplication.VPNMode.DISALLOW);
+ }
+ }
+
+ public static class AllowedPackageListFragment extends PackageListFragment {
+ public AllowedPackageListFragment() {
+ super(MyApplication.VPNMode.ALLOW);
+ }
+ }
+
+ protected static class PackageListFragment extends PreferenceFragmentCompat
+ implements SearchView.OnQueryTextListener, SearchView.OnCloseListener {
+ private final static String PREF_VPN_APPLICATION_APP_TYPE = "pref_vpn_application_app_system";
+ private final static String PREF_VPN_APPLICATION_ORDER_BY = "pref_vpn_application_app_orderby";
+ private final static String PREF_VPN_APPLICATION_FILTER_BY = "pref_vpn_application_app_filterby";
+ private final static String PREF_VPN_APPLICATION_SORT_BY = "pref_vpn_application_app_sortby";
+ private final Map mAllPackageInfoMap = new HashMap<>();
+ private AsyncTaskProgress task;
+
+ private final MyApplication.VPNMode mode;
+
+ private EnumSet filterAppType = EnumSet.noneOf(FilterAppType.class);
+ private MyApplication.AppSortBy appSortBy = MyApplication.AppSortBy.APPNAME;
+ private MyApplication.AppOrderBy appOrderBy = MyApplication.AppOrderBy.ASC;
+ private MyApplication.AppSortBy appFilterBy = MyApplication.AppSortBy.APPNAME;
+ private PreferenceScreen mFilterPreferenceScreen;
+ private String searchFilter = "";
+ private SearchView searchView;
+
+ public PackageListFragment(MyApplication.VPNMode mode) {
+ super();
+ this.mode = mode;
+ this.task = new AsyncTaskProgress(this);
+ }
+
+ @Override
+ public void onCreatePreferences(Bundle savedInstanceState, String rootKey) {
+ mFilterPreferenceScreen = getPreferenceManager().createPreferenceScreen(getActivity());
+ setPreferenceScreen(mFilterPreferenceScreen);
+ }
+
+ @Override
+ public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
+ super.onViewCreated(view, savedInstanceState);
+ requireActivity().addMenuProvider(new MenuProvider() {
+ @Override
+ public void onCreateMenu(@NonNull Menu menu, @NonNull MenuInflater menuInflater) {
+ menuInflater.inflate(R.menu.menu_search, menu);
+
+ final MenuItem menuSearch = menu.findItem(R.id.menu_search_item);
+ searchView = (SearchView) menuSearch.getActionView();
+ assert searchView != null;
+ searchView.setOnQueryTextListener(PackageListFragment.this);
+ searchView.setOnCloseListener(PackageListFragment.this);
+ searchView.setSubmitButtonEnabled(false);
+
+ final MenuItem menuShowSystemApp = menu.findItem(R.id.menu_filter_app_system);
+ menuShowSystemApp.setChecked(filterAppType.contains(FilterAppType.SYSTEM_APP));
+
+ switch (appOrderBy) {
+ case ASC: {
+ final MenuItem menuItem = menu.findItem(R.id.menu_sort_order_asc);
+ menuItem.setChecked(true);
+ break;
+ }
+ case DESC: {
+ final MenuItem menuItem = menu.findItem(R.id.menu_sort_order_desc);
+ menuItem.setChecked(true);
+ break;
+ }
+ }
+
+ switch (appFilterBy) {
+ case APPNAME: {
+ final MenuItem menuItem = menu.findItem(R.id.menu_filter_app_name);
+ menuItem.setChecked(true);
+ break;
+ }
+ case PKGNAME: {
+ final MenuItem menuItem = menu.findItem(R.id.menu_filter_pkg_name);
+ menuItem.setChecked(true);
+ break;
+ }
+ }
+
+ switch (appSortBy) {
+ case APPNAME: {
+ final MenuItem menuItem = menu.findItem(R.id.menu_sort_app_name);
+ menuItem.setChecked(true);
+ break;
+ }
+ case PKGNAME: {
+ final MenuItem menuItem = menu.findItem(R.id.menu_sort_pkg_name);
+ menuItem.setChecked(true);
+ break;
+ }
+ }
+ }
+
+ @Override
+ public boolean onMenuItemSelected(@NonNull MenuItem item) {
+ int item_id = item.getItemId();
+ if (item_id == android.R.id.home) {
+ startActivity(new Intent(getActivity(), SettingsActivity.class));
+ return true;
+ } else if (item_id == R.id.menu_filter_app_system) {
+ item.setChecked(!item.isChecked());
+ if (item.isChecked()) {
+ filterAppType.add(FilterAppType.SYSTEM_APP);
+ } else {
+ filterAppType.remove(FilterAppType.SYSTEM_APP);
+ }
+ filter(null, appFilterBy, MyApplication.AppOrderBy.ASC, appSortBy, filterAppType);
+ return true;
+ } else if (item_id == R.id.menu_sort_order_asc) {
+ item.setChecked(!item.isChecked());
+ filter(null, appFilterBy, MyApplication.AppOrderBy.ASC, appSortBy, filterAppType);
+ return true;
+ } else if (item_id == R.id.menu_sort_order_desc) {
+ item.setChecked(!item.isChecked());
+ filter(null, appFilterBy, MyApplication.AppOrderBy.DESC, appSortBy, filterAppType);
+ return true;
+ } else if (item_id == R.id.menu_filter_app_name) {
+ item.setChecked(!item.isChecked());
+ appFilterBy = MyApplication.AppSortBy.APPNAME;
+ filter(null, appFilterBy, appOrderBy, MyApplication.AppSortBy.APPNAME, filterAppType);
+ return true;
+ } else if (item_id == R.id.menu_filter_pkg_name) {
+ item.setChecked(!item.isChecked());
+ appFilterBy = MyApplication.AppSortBy.PKGNAME;
+ filter(null, appFilterBy, appOrderBy, MyApplication.AppSortBy.PKGNAME, filterAppType);
+ return true;
+ } else if (item_id == R.id.menu_sort_app_name) {
+ item.setChecked(!item.isChecked());
+ filter(null, appFilterBy, appOrderBy, MyApplication.AppSortBy.APPNAME, filterAppType);
+ return true;
+ } else if (item_id == R.id.menu_sort_pkg_name) {
+ item.setChecked(!item.isChecked());
+ filter(null, appFilterBy, appOrderBy, MyApplication.AppSortBy.PKGNAME, filterAppType);
+ return true;
+ }
+ return false;
+ }
+ }, getViewLifecycleOwner(), Lifecycle.State.RESUMED);
+ }
+
+ protected void filter(String filter) {
+ this.filter(filter, this.appFilterBy, this.appOrderBy, this.appSortBy, this.filterAppType);
+ }
+
+ protected void filter(String filter, final MyApplication.AppSortBy filterBy, final MyApplication.AppOrderBy orderBy, final MyApplication.AppSortBy sortBy, EnumSet filterAppType) {
+ if (filter == null) {
+ filter = searchFilter;
+ } else {
+ searchFilter = filter;
+ }
+ this.filterAppType = filterAppType;
+ this.appFilterBy = filterBy;
+ this.appOrderBy = orderBy;
+ this.appSortBy = sortBy;
+
+ Set selected = this.getAllSelectedPackageSet();
+ storeSelectedPackageSet(selected);
+
+ this.removeAllPreferenceScreen();
+
+ if (task != null && task.getStatus() == ProgressTask.Status.PENDING) {
+ task.execute();
+ } else {
+ task = new AsyncTaskProgress(this);
+ task.execute();
+ }
+// this.filterPackagesPreferences(filter, sortBy, orderBy);
+ }
+
+ @Override
+ public void onPause() {
+ super.onPause();
+ if (this.task != null) {
+ this.task.cancel(true);
+ this.task = null;
+ }
+ final MyApplication app = MyApplication.getInstance();
+ assert app != null;
+ Set selected = this.getAllSelectedPackageSet();
+ storeSelectedPackageSet(selected);
+ SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(app.getApplicationContext());
+ SharedPreferences.Editor edit = prefs.edit();
+ edit.putString(PREF_VPN_APPLICATION_APP_TYPE, this.filterAppType.toString());
+ edit.putString(PREF_VPN_APPLICATION_ORDER_BY, this.appOrderBy.name());
+ edit.putString(PREF_VPN_APPLICATION_FILTER_BY, this.appFilterBy.name());
+ edit.putString(PREF_VPN_APPLICATION_SORT_BY, this.appSortBy.name());
+ edit.apply();
+ }
+
+ @Override
+ public void onResume() {
+ super.onResume();
+ final MyApplication app = MyApplication.getInstance();
+ assert app != null;
+ Set loadMap = app.loadVPNApplication(this.mode);
+ for (String pkgName : loadMap) {
+ this.mAllPackageInfoMap.put(pkgName, loadMap.contains(pkgName));
+ }
+ SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(app.getApplicationContext());
+ String filterAppType = prefs.getString(PREF_VPN_APPLICATION_APP_TYPE, this.filterAppType.toString());
+ this.filterAppType = FilterAppType.parseEnumSet(filterAppType);
+ String appOrderBy = prefs.getString(PREF_VPN_APPLICATION_ORDER_BY, MyApplication.AppOrderBy.ASC.name());
+ String appFilterBy = prefs.getString(PREF_VPN_APPLICATION_FILTER_BY, MyApplication.AppSortBy.APPNAME.name());
+ String appSortBy = prefs.getString(PREF_VPN_APPLICATION_SORT_BY, MyApplication.AppSortBy.APPNAME.name());
+ this.appOrderBy = Enum.valueOf(MyApplication.AppOrderBy.class, appOrderBy);
+ this.appFilterBy = Enum.valueOf(MyApplication.AppSortBy.class, appFilterBy);
+ this.appSortBy = Enum.valueOf(MyApplication.AppSortBy.class, appSortBy);
+ filter(null);
+ }
+
+ private void removeAllPreferenceScreen() {
+ this.mFilterPreferenceScreen.removeAll();
+ }
+
+// private void filterPackagesPreferences(String filter, final MyApplication.AppSortBy sortBy, final MyApplication.AppOrderBy orderBy) {
+// final Context context = MyApplication.getInstance().getApplicationContext();
+// final PackageManager pm = context.getPackageManager();
+// final List installedPackages = pm.getInstalledPackages(PackageManager.GET_META_DATA);
+// Collections.sort(installedPackages, new Comparator() {
+// @Override
+// public int compare(PackageInfo o1, PackageInfo o2) {
+// String t1 = "";
+// String t2 = "";
+// switch (sortBy) {
+// case APPNAME:
+// t1 = o1.applicationInfo.loadLabel(pm).toString();
+// t2 = o2.applicationInfo.loadLabel(pm).toString();
+// break;
+// case PKGNAME:
+// t1 = o1.packageName;
+// t2 = o2.packageName;
+// break;
+// }
+// if (MyApplication.AppOrderBy.ASC.equals(orderBy))
+// return t1.compareTo(t2);
+// else
+// return t2.compareTo(t1);
+// }
+// });
+//
+// final Map installedPackageMap = new HashMap<>();
+// for (final PackageInfo pi : installedPackages) {
+// // exclude self package
+// if (pi.packageName.equals(MyApplication.getInstance().getPackageName())) {
+// continue;
+// }
+// boolean checked = this.mAllPackageInfoMap.containsKey(pi.packageName) ? this.mAllPackageInfoMap.get(pi.packageName) : false;
+// installedPackageMap.put(pi.packageName, checked);
+// }
+// this.mAllPackageInfoMap.clear();
+// this.mAllPackageInfoMap.putAll(installedPackageMap);
+//
+// for (final PackageInfo pi : installedPackages) {
+// // exclude self package
+// if (pi.packageName.equals(MyApplication.getInstance().getPackageName())) {
+// continue;
+// }
+// String t1 = pi.applicationInfo.loadLabel(pm).toString();
+// if (filter.trim().isEmpty() || t1.toLowerCase().contains(filter.toLowerCase())) {
+// final Preference preference = buildPackagePreferences(pm, pi);
+// this.mFilterPreferenceScreen.addPreference(preference);
+// }
+// }
+// }
+
+ private Preference buildPackagePreferences(final PackageManager pm, final PackageInfo pi) {
+ final CheckBoxPreference prefCheck = new CheckBoxPreference(requireActivity());
+ prefCheck.setIcon(pi.applicationInfo.loadIcon(pm));
+ prefCheck.setTitle(pi.applicationInfo.loadLabel(pm).toString());
+ prefCheck.setSummary(pi.packageName);
+ boolean checked = this.mAllPackageInfoMap.containsKey(pi.packageName) ? this.mAllPackageInfoMap.get(pi.packageName) : false;
+ prefCheck.setChecked(checked);
+ Preference.OnPreferenceClickListener click = new Preference.OnPreferenceClickListener() {
+ @Override
+ public boolean onPreferenceClick(Preference preference) {
+ mAllPackageInfoMap.put(prefCheck.getSummary().toString(), prefCheck.isChecked());
+ return false;
+ }
+ };
+ prefCheck.setOnPreferenceClickListener(click);
+ return prefCheck;
+ }
+
+ private Set getFilterSelectedPackageSet() {
+ final Set selected = new HashSet<>();
+ for (int i = 0; i < this.mFilterPreferenceScreen.getPreferenceCount(); i++) {
+ Preference pref = this.mFilterPreferenceScreen.getPreference(i);
+ if ((pref instanceof CheckBoxPreference)) {
+ CheckBoxPreference prefCheck = (CheckBoxPreference) pref;
+ if (prefCheck.isChecked()) {
+ selected.add(prefCheck.getSummary().toString());
+ }
+ }
+ }
+ return selected;
+ }
+
+ private void setSelectedPackageSet(Set selected) {
+ for (int i = 0; i < this.mFilterPreferenceScreen.getPreferenceCount(); i++) {
+ Preference pref = this.mFilterPreferenceScreen.getPreference(i);
+ if ((pref instanceof CheckBoxPreference)) {
+ CheckBoxPreference prefCheck = (CheckBoxPreference) pref;
+ if (selected.contains((prefCheck.getSummary()))) {
+ prefCheck.setChecked(true);
+ }
+ }
+ }
+ }
+
+ private void clearAllSelectedPackageSet() {
+ final Set selected = this.getFilterSelectedPackageSet();
+ for (Map.Entry value : this.mAllPackageInfoMap
+ .entrySet()) {
+ if (value.getValue()) {
+ value.setValue(false);
+ }
+ }
+ }
+
+ private Set getAllSelectedPackageSet() {
+ final Set selected = this.getFilterSelectedPackageSet();
+ for (Map.Entry value : this.mAllPackageInfoMap.entrySet()) {
+ if (value.getValue()) {
+ selected.add(value.getKey());
+ }
+ }
+ return selected;
+ }
+
+ private void storeSelectedPackageSet(final Set set) {
+ final MyApplication app = MyApplication.getInstance();
+ assert app != null;
+ app.storeVPNMode(this.mode);
+ app.storeVPNApplication(this.mode, set);
+ }
+
+ @Override
+ public boolean onQueryTextSubmit(String query) {
+ this.searchView.clearFocus();
+ if (!query.trim().isEmpty()) {
+ filter(query);
+ return true;
+ } else {
+ filter("");
+ return true;
+ }
+ }
+
+ @Override
+ public boolean onQueryTextChange(String newText) {
+ return false;
+ }
+
+ @Override
+ public boolean onClose() {
+ Set selected = this.getAllSelectedPackageSet();
+ storeSelectedPackageSet(selected);
+ filter("");
+ return false;
+ }
+ }
+
+ /*
+ * AsyncTask
+ * https://developer.android.com/reference/android/os/AsyncTask
+ * Deprecated in API level R
+ * */
+ public static class AsyncTaskProgress extends ProgressTask> {
+
+ final PackageListFragment packageFragment;
+
+ public AsyncTaskProgress(PackageListFragment packageFragment) {
+ this.packageFragment = packageFragment;
+ }
+
+ @Override
+ protected void onPreExecute() {
+ packageFragment.mFilterPreferenceScreen.addPreference(new ProgressPreference(packageFragment.getActivity()));
+ }
+
+ @Override
+ protected List doInBackground(String... params) {
+ return filterPackages(packageFragment.searchFilter, packageFragment.appFilterBy, packageFragment.appOrderBy, packageFragment.appSortBy, packageFragment.filterAppType);
+ }
+
+ private List filterPackages(String filter, final MyApplication.AppSortBy filterBy, final MyApplication.AppOrderBy orderBy, final MyApplication.AppSortBy sortBy, EnumSet filterAppType) {
+ final MyApplication app = MyApplication.getInstance();
+ assert app != null;
+ final Context context = app.getApplicationContext();
+ final PackageManager pm = context.getPackageManager();
+ List installedPackages = List.of();
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
+ // Android 13 (API 33) 以降
+ installedPackages = pm.getInstalledPackages(PackageManager.PackageInfoFlags.of(0));
+ } else {
+ // Android 12 以前
+ installedPackages = pm.getInstalledPackages(0);
+ }
+ Collections.sort(installedPackages, new Comparator() {
+ @Override
+ public int compare(PackageInfo o1, PackageInfo o2) {
+ String t1 = "";
+ String t2 = "";
+ switch (sortBy) {
+ case APPNAME: {
+ assert o1.applicationInfo != null;
+ t1 = o1.applicationInfo.loadLabel(pm).toString();
+ assert o2.applicationInfo != null;
+ t2 = o2.applicationInfo.loadLabel(pm).toString();
+ break;
+ }
+ case PKGNAME: {
+ t1 = o1.packageName;
+ t2 = o2.packageName;
+ break;
+ }
+ }
+ if (MyApplication.AppOrderBy.ASC.equals(orderBy)) {
+ return t1.compareTo(t2);
+ } else {
+ return t2.compareTo(t1);
+ }
+ }
+ });
+ final Map installedPackageMap = new HashMap<>();
+ for (final PackageInfo pi : installedPackages) {
+ if (isCancelled()) continue;
+ // exclude self package
+ if (pi.packageName.equals(app.getPackageName())) {
+ continue;
+ }
+ // exclude system app
+ if (!filterAppType.contains(FilterAppType.SYSTEM_APP)) {
+ assert pi.applicationInfo != null;
+ if ((pi.applicationInfo.flags & ApplicationInfo.FLAG_SYSTEM) == ApplicationInfo.FLAG_SYSTEM) {
+ continue;
+ }
+ }
+ boolean checked = packageFragment.mAllPackageInfoMap.containsKey(pi.packageName) ? packageFragment.mAllPackageInfoMap.get(pi.packageName) : false;
+ installedPackageMap.put(pi.packageName, checked);
+ }
+ packageFragment.mAllPackageInfoMap.clear();
+ packageFragment.mAllPackageInfoMap.putAll(installedPackageMap);
+ return installedPackages;
+ }
+
+ @Override
+ protected void onPostExecute(List installedPackages) {
+ final MyApplication app = MyApplication.getInstance();
+ assert app != null;
+ final Context context = app.getApplicationContext();
+ final PackageManager pm = context.getPackageManager();
+ packageFragment.mFilterPreferenceScreen.removeAll();
+ for (final PackageInfo pi : installedPackages) {
+ // exclude self package
+ if (pi.packageName.equals(app.getPackageName())) {
+ continue;
+ }
+ // exclude system app
+ if (!packageFragment.filterAppType.contains(FilterAppType.SYSTEM_APP)) {
+ assert pi.applicationInfo != null;
+ if ((pi.applicationInfo.flags & ApplicationInfo.FLAG_SYSTEM) == ApplicationInfo.FLAG_SYSTEM) {
+ continue;
+ }
+ }
+ String t1 = "";
+ String t2 = packageFragment.searchFilter.trim();
+ switch (packageFragment.appFilterBy) {
+ case APPNAME:
+ t1 = pi.applicationInfo.loadLabel(pm).toString();
+ break;
+ case PKGNAME:
+ t1 = pi.packageName;
+ break;
+ }
+ if (t2.isEmpty() || t1.toLowerCase(Locale.ROOT).contains(t2.toLowerCase(Locale.ROOT))) {
+ final Preference preference = packageFragment.buildPackagePreferences(pm, pi);
+ packageFragment.mFilterPreferenceScreen.addPreference(preference);
+ }
+ }
+ }
+
+ @Override
+ protected void onCancelled() {
+ super.onCancelled();
+ packageFragment.mAllPackageInfoMap.clear();
+ packageFragment.mFilterPreferenceScreen.removeAll();
+ }
+ }
+
+ protected static class ProgressPreference extends Preference {
+ public ProgressPreference(Context context) {
+ super(context);
+ setLayoutResource(R.layout.preference_progress);
+ }
+ }
+}
+
diff --git a/android_app/app/src/main/java/tun/proxy/receiver/BootReceiver.java b/android_app/app/src/main/java/tun/proxy/receiver/BootReceiver.java
new file mode 100644
index 00000000..21f807ba
--- /dev/null
+++ b/android_app/app/src/main/java/tun/proxy/receiver/BootReceiver.java
@@ -0,0 +1,38 @@
+package tun.proxy.receiver;
+
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.SharedPreferences;
+import android.net.VpnService;
+
+import androidx.preference.PreferenceManager;
+
+import android.util.Log;
+
+import tun.proxy.MyApplication;
+import tun.proxy.R;
+
+import tun.proxy.service.Tun2HttpVpnService;
+
+public class BootReceiver extends BroadcastReceiver {
+
+ @Override
+ public void onReceive(final Context context, Intent intent) {
+ if (intent != null && !Intent.ACTION_BOOT_COMPLETED.equals(intent.getAction())) {
+ return;
+ }
+ final MyApplication app = MyApplication.getInstance();
+ assert app != null;
+ boolean isRunning = app.loadProxyRunning(false);
+ if (isRunning) {
+ Intent prepare = VpnService.prepare(context);
+ if (prepare == null) {
+ Log.d(context.getString(R.string.app_name) + ".Boot", "Starting vpn");
+ Tun2HttpVpnService.start(context);
+ } else {
+ Log.d(context.getString(R.string.app_name) + ".Boot", "Not prepared");
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/android_app/app/src/main/java/com/tun2http/app/service/Tun2HttpVpnService.java b/android_app/app/src/main/java/tun/proxy/service/Tun2HttpVpnService.java
similarity index 58%
rename from android_app/app/src/main/java/com/tun2http/app/service/Tun2HttpVpnService.java
rename to android_app/app/src/main/java/tun/proxy/service/Tun2HttpVpnService.java
index ed888ff3..92f770de 100644
--- a/android_app/app/src/main/java/com/tun2http/app/service/Tun2HttpVpnService.java
+++ b/android_app/app/src/main/java/tun/proxy/service/Tun2HttpVpnService.java
@@ -1,114 +1,104 @@
-package com.tun2http.app.service;
+package tun.proxy.service;
-import android.annotation.TargetApi;
+import android.annotation.SuppressLint;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.content.pm.PackageManager;
import android.net.ConnectivityManager;
-import android.net.NetworkInfo;
+import android.net.Network;
+import android.net.NetworkCapabilities;
import android.net.VpnService;
import android.os.Binder;
-import android.os.Build;
import android.os.IBinder;
import android.os.Parcel;
import android.os.ParcelFileDescriptor;
import android.os.PowerManager;
import android.os.RemoteException;
-import android.preference.PreferenceManager;
+
+import androidx.annotation.NonNull;
+import androidx.core.app.ServiceCompat;
+import androidx.preference.PreferenceManager;
+
import android.text.TextUtils;
import android.util.Log;
-
-import com.tun2http.app.R;
-import com.tun2http.app.utils.IPUtil;
-import com.tun2http.app.utils.Util;
+import android.widget.Toast;
import java.io.IOException;
-import java.math.BigInteger;
-import java.net.Inet4Address;
import java.net.InetAddress;
-import java.net.InterfaceAddress;
-import java.net.NetworkInterface;
-import java.net.SocketException;
-import java.net.UnknownHostException;
import java.util.ArrayList;
-import java.util.Collections;
-import java.util.Enumeration;
+import java.util.Arrays;
import java.util.List;
+import java.util.Set;
+import tun.proxy.MyApplication;
+import tun.proxy.R;
+import tun.utils.NetUtil;
+@SuppressLint("VpnServicePolicy")
public class Tun2HttpVpnService extends VpnService {
private static final String TAG = "Tun2Http.Service";
- private static final String ACTION_START = "start";
- private static final String ACTION_STOP = "stop";
+
public static final String PREF_PROXY_HOST = "pref_proxy_host";
public static final String PREF_PROXY_PORT = "pref_proxy_port";
- public static final String PREF_RUNNING = "pref_running";
+ public static final String PREF_PROXY_TYPE = "pref_proxy_type";
+ private static final String ACTION_START = "start";
+ private static final String ACTION_STOP = "stop";
+ private static volatile PowerManager.WakeLock wlInstance = null;
+ static {
+ System.loadLibrary("tun2http");
+ }
- private Tun2HttpVpnService.Builder lastBuilder = null;
private ParcelFileDescriptor vpn = null;
- static {
- System.loadLibrary("tun2http");
+ synchronized private static PowerManager.WakeLock getLock(Context context) {
+ if (wlInstance == null) {
+ PowerManager pm = (PowerManager) context.getSystemService(Context.POWER_SERVICE);
+ wlInstance = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, context.getString(R.string.app_name) + " wakelock");
+ wlInstance.setReferenceCounted(true);
+ }
+ return wlInstance;
}
+ public static void start(Context context) {
+ Intent intent = new Intent(context, Tun2HttpVpnService.class);
+ intent.setAction(ACTION_START);
+ context.startService(intent);
+ }
- private static volatile PowerManager.WakeLock wlInstance = null;
+ public static void stop(Context context) {
+ Intent intent = new Intent(context, Tun2HttpVpnService.class);
+ intent.setAction(ACTION_STOP);
+ context.startService(intent);
+ }
private native void jni_init();
- private native void jni_start(int tun, boolean fwd53, int rcode, String proxyIp, int proxyPort);
+ private native void jni_start(int tun, boolean fwd53, int rcode, String proxyIp, int proxyPort, boolean isSocks5);
private native void jni_stop(int tun);
private native int jni_get_mtu();
- private native int jni_done();
-
-
- synchronized private static PowerManager.WakeLock getLock(Context context) {
- if (wlInstance == null) {
- PowerManager pm = (PowerManager) context.getSystemService(Context.POWER_SERVICE);
- wlInstance = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, context.getString(R.string.app_name) + " wakelock");
- wlInstance.setReferenceCounted(true);
- }
- return wlInstance;
- }
+ private native void jni_done();
@Override
public IBinder onBind(Intent intent) {
return new ServiceBinder();
}
- public class ServiceBinder extends Binder {
- @Override
- public boolean onTransact(int code, Parcel data, Parcel reply, int flags)
- throws RemoteException {
- // see Implementation of android.net.VpnService.Callback.onTransact()
- if (code == IBinder.LAST_CALL_TRANSACTION) {
- onRevoke();
- return true;
- }
- return super.onTransact(code, data, reply, flags);
- }
-
- public Tun2HttpVpnService getService() {
- return Tun2HttpVpnService.this;
- }
- }
-
public boolean isRunning() {
return vpn != null;
}
private void start() {
if (vpn == null) {
- lastBuilder = getBuilder();
+ Builder lastBuilder = getBuilder();
vpn = startVPN(lastBuilder);
- if (vpn == null)
+ if (vpn == null) {
throw new IllegalStateException(getString((R.string.msg_start_failed)));
-
+ }
startNative(vpn);
}
}
@@ -119,10 +109,9 @@ private void stop() {
stopVPN(vpn);
vpn = null;
}
- stopForeground(true);
+ ServiceCompat.stopForeground(this, ServiceCompat.STOP_FOREGROUND_REMOVE);
}
-
@Override
public void onRevoke() {
Log.i(TAG, "Revoke");
@@ -139,55 +128,107 @@ private ParcelFileDescriptor startVPN(Builder builder) throws SecurityException
} catch (SecurityException ex) {
throw ex;
} catch (Throwable ex) {
- Log.e(TAG, ex.toString() + "\n" + Log.getStackTraceString(ex));
+ Log.e(TAG, ex + "\n" + Log.getStackTraceString(ex));
return null;
}
}
private Builder getBuilder() {
- SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this);
// Build VPN service
Builder builder = new Builder();
builder.setSession(getString(R.string.app_name));
// VPN address
+ SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this);
String vpn4 = prefs.getString("vpn4", "10.1.10.1");
builder.addAddress(vpn4, 32);
String vpn6 = prefs.getString("vpn6", "fd00:1:fd00:1:fd00:1:fd00:1");
builder.addAddress(vpn6, 128);
builder.addRoute("0.0.0.0", 0);
-
builder.addRoute("0:0:0:0:0:0:0:0", 0);
+ MyApplication app = (MyApplication) this.getApplication();
+ assert app != null;
+ if (app.loadUseDnsCustom()) {
+ String dns1 = app.loadPrimaryDns("8.8.8.8");
+ String dns2 = app.loadSecondaryDns("8.8.4.4");
+ if (!TextUtils.isEmpty(dns1)) {
+ Log.i(TAG, "custom primary DNS:" + dns1);
+ builder.addDnsServer(dns1);
+ }
+ if (!TextUtils.isEmpty(dns2)) {
+ Log.i(TAG, "custom secondary DNS:" + dns2);
+ builder.addDnsServer(dns2);
+ }
+ } else {
+ List dnsList = NetUtil.getDefaultDNS(app.getApplicationContext());
+ for (String dns : dnsList) {
+ Log.i(TAG, "default DNS:" + dns);
+ builder.addDnsServer(dns);
+ }
+ }
+ if (builder.listDns.isEmpty()) {
+ Toast.makeText(this, "DNS settings not found", Toast.LENGTH_SHORT).show();
+ }
+
// MTU
int mtu = jni_get_mtu();
Log.i(TAG, "MTU=" + mtu);
builder.setMtu(mtu);
- // Add list of allowed applications
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
- try {
- builder.addAllowedApplication("com.android.chrome");
- } catch (PackageManager.NameNotFoundException e) {
- e.printStackTrace();
- }
+ // AAdd list of allowed and disallowed applications
+ if (app.loadVPNMode() == MyApplication.VPNMode.DISALLOW) {
+ Set disallow = app.loadVPNApplication(MyApplication.VPNMode.DISALLOW);
+ Log.d(TAG, "disallowed:" + disallow.size());
+ List notFoundPackageList = new ArrayList<>();
+ builder.addDisallowedApplication(Arrays.asList(disallow.toArray(new String[0])), notFoundPackageList);
+ disallow.removeAll(notFoundPackageList);
+ app.storeVPNApplication(MyApplication.VPNMode.DISALLOW, disallow);
+ } else {
+ Set allow = app.loadVPNApplication(MyApplication.VPNMode.ALLOW);
+ Log.d(TAG, "allowed:" + allow.size());
+ List notFoundPackageList = new ArrayList<>();
+ builder.addAllowedApplication(Arrays.asList(allow.toArray(new String[0])), notFoundPackageList);
+ allow.removeAll(notFoundPackageList);
+ app.storeVPNApplication(MyApplication.VPNMode.ALLOW, allow);
}
// Add list of allowed applications
return builder;
}
-
private void startNative(ParcelFileDescriptor vpn) {
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this);
- String proxyHost = prefs.getString(PREF_PROXY_HOST, "");
- int proxyPort = prefs.getInt(PREF_PROXY_PORT, 0);
- if (proxyPort != 0 && !TextUtils.isEmpty(proxyHost)) {
- jni_start(vpn.getFd(), false, 3, proxyHost, proxyPort);
-
- prefs.edit().putBoolean(PREF_RUNNING, true).apply();
+ final String proxyHost = prefs.getString(PREF_PROXY_HOST, "");
+ final int proxyPort = prefs.getInt(PREF_PROXY_PORT, -1);
+ final String proxyTypeName = prefs.getString(PREF_PROXY_TYPE, MyApplication.ProxyType.HTTP.name());
+ final MyApplication.ProxyType proxyType = Enum.valueOf(MyApplication.ProxyType.class, proxyTypeName);
+ final boolean isSocks5 = MyApplication.ProxyType.SOCKS5.equals(proxyType);
+
+ if (NetUtil.isValidHost(proxyHost) && NetUtil.isValiPort(proxyPort)) {
+ new Thread(() -> {
+ String proxyIp = "";
+ try {
+ InetAddress address = InetAddress.getByName(proxyHost);
+ proxyIp = address.getHostAddress();
+ } catch (Exception e) {
+ Log.e(TAG, "DNS resolution failed for " + proxyHost + ": " + e.getMessage());
+ }
+
+ if (TextUtils.isEmpty(proxyIp)) {
+ stop();
+ return;
+ }
+
+ final String finalProxyIp = proxyIp;
+ jni_start(vpn.getFd(), false, 3, finalProxyIp, proxyPort, isSocks5);
+ MyApplication app = (MyApplication) getApplication();
+ if (app != null) {
+ app.storeProxyRunning(true);
+ }
+ }).start();
}
}
@@ -197,21 +238,22 @@ private void stopNative(ParcelFileDescriptor vpn) {
} catch (Throwable ex) {
// File descriptor might be closed
- Log.e(TAG, ex.toString() + "\n" + Log.getStackTraceString(ex));
+ Log.e(TAG, ex + "\n" + Log.getStackTraceString(ex));
jni_stop(-1);
}
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this);
- prefs.edit().putBoolean(PREF_RUNNING, false).apply();
+ MyApplication app = (MyApplication) this.getApplication();
+ assert app != null;
+ app.storeProxyRunning(false);
}
-
private void stopVPN(ParcelFileDescriptor pfd) {
Log.i(TAG, "Stopping");
try {
pfd.close();
} catch (IOException ex) {
- Log.e(TAG, ex.toString() + "\n" + Log.getStackTraceString(ex));
+ Log.e(TAG, ex + "\n" + Log.getStackTraceString(ex));
}
}
@@ -229,7 +271,6 @@ private void nativeError(int error, String message) {
Log.w(TAG, "Native error " + error + ": " + message);
}
-
private boolean isSupported(int protocol) {
return (protocol == 1 /* ICMPv4 */ ||
protocol == 59 /* ICMPv6 */ ||
@@ -237,15 +278,11 @@ private boolean isSupported(int protocol) {
protocol == 17 /* UDP */);
}
-
@Override
public void onCreate() {
-
// Native init
jni_init();
-
super.onCreate();
-
}
@Override
@@ -265,7 +302,6 @@ public int onStartCommand(Intent intent, int flags, int startId) {
return START_STICKY;
}
-
@Override
public void onDestroy() {
Log.i(TAG, "Destroy");
@@ -277,7 +313,7 @@ public void onDestroy() {
vpn = null;
}
} catch (Throwable ex) {
- Log.e(TAG, ex.toString() + "\n" + Log.getStackTraceString(ex));
+ Log.e(TAG, ex + "\n" + Log.getStackTraceString(ex));
}
jni_done();
@@ -285,19 +321,34 @@ public void onDestroy() {
super.onDestroy();
}
+ public class ServiceBinder extends Binder {
+ @Override
+ public boolean onTransact(int code, Parcel data, Parcel reply, int flags)
+ throws RemoteException {
+ // see Implementation of android.net.VpnService.Callback.onTransact()
+ if (code == IBinder.LAST_CALL_TRANSACTION) {
+ onRevoke();
+ return true;
+ }
+ return super.onTransact(code, data, reply, flags);
+ }
+
+ public Tun2HttpVpnService getService() {
+ return Tun2HttpVpnService.this;
+ }
+ }
+
private class Builder extends VpnService.Builder {
- private NetworkInfo networkInfo;
private int mtu;
- private List listAddress = new ArrayList<>();
- private List listRoute = new ArrayList<>();
- private List listDns = new ArrayList<>();
+ private final List listAddress = new ArrayList<>();
+ private final List listRoute = new ArrayList<>();
+ private final List listDns = new ArrayList<>();
private Builder() {
super();
- ConnectivityManager cm = (ConnectivityManager) getSystemService(Context.CONNECTIVITY_SERVICE);
- networkInfo = cm.getActiveNetworkInfo();
}
+ @NonNull
@Override
public VpnService.Builder setMtu(int mtu) {
this.mtu = mtu;
@@ -305,6 +356,7 @@ public VpnService.Builder setMtu(int mtu) {
return this;
}
+ @NonNull
@Override
public Builder addAddress(String address, int prefixLength) {
listAddress.add(address + "/" + prefixLength);
@@ -312,6 +364,7 @@ public Builder addAddress(String address, int prefixLength) {
return this;
}
+ @NonNull
@Override
public Builder addRoute(String address, int prefixLength) {
listRoute.add(address + "/" + prefixLength);
@@ -319,13 +372,46 @@ public Builder addRoute(String address, int prefixLength) {
return this;
}
+ @NonNull
@Override
public Builder addDnsServer(InetAddress address) {
- listDns.add(address);
+ listDns.add(address.getHostAddress());
+ super.addDnsServer(address);
+ return this;
+ }
+
+ @NonNull
+ @Override
+ public Builder addDnsServer(String address) {
+// listDns.add(address);
super.addDnsServer(address);
return this;
}
+ // min sdk 26
+ public void addAllowedApplication(final List packageList, final List notFoundPackegeList) {
+ for (String pkg : packageList) {
+ try {
+ Log.i(TAG, "allowed:" + pkg);
+ addAllowedApplication(pkg);
+ } catch (PackageManager.NameNotFoundException e) {
+ notFoundPackegeList.add(pkg);
+ }
+ }
+ }
+
+ public void addDisallowedApplication(final List packageList, final List notFoundPackegeList) {
+ //
+ for (String pkg : packageList) {
+ try {
+ Log.i(TAG, "disallowed:" + pkg);
+ addDisallowedApplication(pkg);
+ } catch (PackageManager.NameNotFoundException e) {
+ notFoundPackegeList.add(pkg);
+ }
+ }
+ }
+
@Override
public boolean equals(Object obj) {
Builder other = (Builder) obj;
@@ -333,10 +419,6 @@ public boolean equals(Object obj) {
if (other == null)
return false;
- if (this.networkInfo == null || other.networkInfo == null ||
- this.networkInfo.getType() != other.networkInfo.getType())
- return false;
-
if (this.mtu != other.mtu)
return false;
@@ -357,7 +439,7 @@ public boolean equals(Object obj) {
if (!other.listRoute.contains(route))
return false;
- for (InetAddress dns : this.listDns)
+ for (String dns : this.listDns)
if (!other.listDns.contains(dns))
return false;
@@ -365,17 +447,4 @@ public boolean equals(Object obj) {
}
}
-
- public static void start(Context context) {
- Intent intent = new Intent(context, Tun2HttpVpnService.class);
- intent.setAction(ACTION_START);
- context.startService(intent);
- }
-
-
- public static void stop(Context context) {
- Intent intent = new Intent(context, Tun2HttpVpnService.class);
- intent.setAction(ACTION_STOP);
- context.startService(intent);
- }
}
diff --git a/android_app/app/src/main/java/tun/utils/CertificateUtil.java b/android_app/app/src/main/java/tun/utils/CertificateUtil.java
new file mode 100644
index 00000000..8805c856
--- /dev/null
+++ b/android_app/app/src/main/java/tun/utils/CertificateUtil.java
@@ -0,0 +1,187 @@
+package tun.utils;
+
+import android.content.Intent;
+import android.security.KeyChain;
+import android.util.Log;
+
+import java.io.*;
+import java.nio.charset.StandardCharsets;
+import java.security.KeyStore;
+import java.security.KeyStoreException;
+import java.security.NoSuchAlgorithmException;
+import java.security.cert.CertificateEncodingException;
+import java.security.cert.CertificateException;
+import java.security.cert.CertificateFactory;
+import java.security.cert.X509Certificate;
+import java.util.*;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+public class CertificateUtil {
+ private static final String TAG = "CertificateManager";
+ private final static Pattern CA_COMMON_NAME = Pattern.compile("CN=([^,]+),?.*$");
+ private final static Pattern CA_ORGANIZATION = Pattern.compile("O=([^,]+),?.*$");
+
+ public static boolean findCAStore(String caName) {
+ boolean found = false;
+ try {
+ KeyStore ks = KeyStore.getInstance("AndroidCAStore");
+ if (ks == null)
+ return false;
+
+ ks.load(null, null);
+ X509Certificate rootCACert = null;
+ Enumeration aliases = ks.aliases();
+ while (aliases.hasMoreElements()) {
+ String alias = aliases.nextElement();
+ rootCACert = (X509Certificate) ks.getCertificate(alias);
+ if (rootCACert.getIssuerDN().getName().contains(caName)) {
+ found = true;
+ break;
+ }
+ }
+ } catch (IOException | KeyStoreException | NoSuchAlgorithmException |
+ CertificateException e) {
+ Log.e(TAG, e.getMessage(), e);
+ }
+ return found;
+ }
+
+ public static List getRootCAStore() {
+ final List rootCAList = new ArrayList<>();
+ try {
+ KeyStore ks = KeyStore.getInstance("AndroidCAStore");
+ if (ks == null)
+ return null;
+
+ ks.load(null, null);
+ X509Certificate rootCACert = null;
+ Enumeration aliases = ks.aliases();
+ boolean found = false;
+ while (aliases.hasMoreElements()) {
+ String alias = aliases.nextElement();
+ X509Certificate cert = (X509Certificate) ks.getCertificate(alias);
+ System.out.println(alias + "/" + cert.getIssuerX500Principal().getName());
+ rootCAList.add(cert);
+ }
+ } catch (IOException | KeyStoreException | NoSuchAlgorithmException |
+ CertificateException e) {
+ Log.e(TAG, e.getMessage(), e);
+ }
+ return rootCAList;
+ }
+
+ public static Map getRootCAMap(EnumSet type) {
+ final Map rootCAMap = new HashMap<>();
+ try {
+ KeyStore ks = KeyStore.getInstance("AndroidCAStore");
+ if (ks == null) {
+ return null;
+ }
+
+ ks.load(null, null);
+ X509Certificate rootCACert = null;
+ Enumeration aliases = ks.aliases();
+ List certList = new ArrayList<>();
+ while (aliases.hasMoreElements()) {
+ String alias = aliases.nextElement();
+ X509Certificate cert = (X509Certificate) ks.getCertificate(alias);
+ if (type.contains(CertificateInstallType.SYSTEM) && alias.startsWith("system:")) {
+ certList.add(cert);
+ }
+ if (type.contains(CertificateInstallType.USER) && alias.startsWith("user:")) {
+ certList.add(cert);
+ }
+ }
+ Collections.sort(certList, new Comparator() {
+ @Override
+ public int compare(X509Certificate t1, X509Certificate t2) {
+ String t1cn = CertificateUtil.getCommonName(t1.getIssuerX500Principal().getName());
+ String t2cn = CertificateUtil.getCommonName(t2.getIssuerX500Principal().getName());
+ return t1cn.compareToIgnoreCase(t2cn);
+ }
+ });
+ // ソート後
+ List rootCANameList = new ArrayList<>();
+ List rootCAList = new ArrayList<>();
+ for (X509Certificate cert : certList) {
+ String cn = CertificateUtil.getCommonName(cert.getIssuerX500Principal().getName());
+ if (cn.trim().isEmpty()) continue;
+ //String o = CertificateUtil.getOrganization( cert.getIssuerX500Principal().getName());
+ rootCANameList.add(cn);
+ rootCAList.add(encode(cert.getEncoded()));
+ }
+ rootCAMap.put("entry", rootCANameList.toArray(new String[0]));
+ rootCAMap.put("value", rootCAList.toArray(new String[0]));
+ } catch (IOException | KeyStoreException | NoSuchAlgorithmException |
+ CertificateException e) {
+ Log.e(TAG, e.getMessage(), e);
+ }
+ return rootCAMap;
+ }
+
+ public static String encode(byte[] b) {
+ return new String(b, StandardCharsets.ISO_8859_1);
+ }
+
+ public static byte[] decode(String s) {
+ return s.getBytes(StandardCharsets.ISO_8859_1);
+ }
+
+ public static Intent trustRootCA(X509Certificate cert) {
+ Log.d(TAG, "root CA is not yet trusted");
+ Intent intent = KeyChain.createInstallIntent();
+ try {
+ if (findCAStore(cert.getIssuerDN().getName())) return null;
+ intent.putExtra(KeyChain.EXTRA_CERTIFICATE, cert.getEncoded());
+ intent.putExtra(KeyChain.EXTRA_NAME, getCommonName(cert.getIssuerDN().getName()));
+ } catch (CertificateEncodingException e) {
+ Log.e(TAG, e.getMessage(), e);
+ }
+ return intent;
+ }
+
+ // get the CA certificate by the path
+ public static X509Certificate getCACertificate(byte[] buff) {
+ X509Certificate ca = null;
+ try {
+ CertificateFactory cf = CertificateFactory.getInstance("X.509");
+ ca = (X509Certificate) cf.generateCertificate(new ByteArrayInputStream(buff));
+ } catch (CertificateException e) {
+ Log.e(TAG, e.getMessage(), e);
+ }
+ return ca;
+ }
+
+ // get the CA certificate by the path
+ public static X509Certificate getCACertificate(File caFile) {
+ try (InputStream inStream = new FileInputStream(caFile)) {
+ CertificateFactory cf = CertificateFactory.getInstance("X.509");
+ return (X509Certificate) cf.generateCertificate(inStream);
+ } catch (IOException | CertificateException e) {
+ Log.e(TAG, e.getMessage(), e);
+ }
+ return null;
+ }
+
+ public static String getCommonName(String dn) {
+ String cn = "";
+ Matcher m = CA_COMMON_NAME.matcher(dn);
+ if (m.find()) {
+ cn = m.group(1);
+ }
+ return cn;
+ }
+
+ public static String getOrganization(String dn) {
+ String on = "";
+ Matcher m = CA_ORGANIZATION.matcher(dn);
+ if (m.find()) {
+ on = m.group(1);
+ }
+ return on;
+ }
+
+ public enum CertificateInstallType {SYSTEM, USER}
+
+}
diff --git a/android_app/app/src/main/java/tun/utils/NetUtil.java b/android_app/app/src/main/java/tun/utils/NetUtil.java
new file mode 100644
index 00000000..4c1e2f8b
--- /dev/null
+++ b/android_app/app/src/main/java/tun/utils/NetUtil.java
@@ -0,0 +1,360 @@
+package tun.utils;
+
+import android.content.Context;
+import android.net.ConnectivityManager;
+import android.net.LinkProperties;
+import android.net.Network;
+import android.net.NetworkCapabilities;
+import android.util.Log;
+
+import java.io.IOException;
+import java.math.BigInteger;
+import java.net.InetAddress;
+import java.net.InetSocketAddress;
+import java.net.Socket;
+import java.net.UnknownHostException;
+import java.util.ArrayList;
+import java.util.List;
+
+import tun.proxy.HostPortPair;
+
+public class NetUtil {
+ private static final String TAG = "Tun2Http.NetUtil";
+
+ public static boolean isConnection(String host, int port) {
+ boolean reachable = false;
+ if (isValiPort(port)) {
+ try (Socket socket = new Socket()) {
+ socket.connect(new InetSocketAddress(host, port), 1000);
+ reachable = true;
+ } catch (IOException ignored) {
+ // ignored
+ }
+ }
+ return reachable;
+ }
+
+ public static boolean isNetworkCapabilities(Context context) {
+ if (context != null) {
+ final ConnectivityManager cm = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
+ if (cm != null) {
+ final Network n = cm.getActiveNetwork();
+ if (n != null) {
+ final NetworkCapabilities nc = cm.getNetworkCapabilities(n);
+ if (nc != null) {
+ return (nc.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR) || nc.hasTransport(NetworkCapabilities.TRANSPORT_WIFI));
+ }
+ }
+ }
+ }
+ return false;
+ }
+
+ public static boolean resolvHost(String hostName) {
+ boolean resolvable = false;
+ try {
+ InetAddress.getByName(hostName);
+ resolvable = true;
+ } catch (UnknownHostException ignored) {
+ // ignored
+ }
+ return resolvable;
+ }
+
+ public static boolean isValidIPv4Address(String ipv4) {
+ if (ipv4 == null || ipv4.isEmpty()) {
+ return false;
+ }
+ String[] ipParts = ipv4.split("\\.", -1);
+ if (ipParts.length != 4) {
+ return false;
+ } else {
+ for (int i = 0; i < ipParts.length; i++) {
+ int ipPart = -1;
+ try {
+ ipPart = Integer.parseInt(ipParts[i]);
+ } catch (NumberFormatException e) {
+ return false;
+ }
+ if (!(0 <= ipPart && ipPart <= 255)) {
+ return false;
+ }
+ }
+ }
+ return true;
+ }
+
+ public static boolean isValidIPv6Address(String ipv6) {
+ if (ipv6 == null || ipv6.isEmpty()) {
+ return false;
+ }
+
+ // --- 【追加】ブラケット [...] で囲まれている場合の処理 ---
+ if (ipv6.startsWith("[") && ipv6.endsWith("]")) {
+ // 中身が空("[]")なら不正
+ if (ipv6.length() == 2) {
+ return false;
+ }
+ // 角括弧を外して中身の文字列を取り出す
+ ipv6 = ipv6.substring(1, ipv6.length() - 1);
+ } else {
+ // 片方だけ角括弧があるような不正な形式(例: "[2001::" や "2001::]")を弾く
+ if (ipv6.contains("[") || ipv6.contains("]")) {
+ return false;
+ }
+ }
+ // --------------------------------------------------
+
+ // 「::」そのものの場合は有効(未指定アドレス)
+ if (ipv6.equals("::")) {
+ return true;
+ }
+
+ // 先頭または末尾が ":" の場合の特殊処理
+ if (ipv6.startsWith(":")) {
+ if (!ipv6.startsWith("::")) return false;
+ }
+ if (ipv6.endsWith(":")) {
+ if (!ipv6.endsWith("::")) return false;
+ }
+
+ // "::" が何回使われているかを正確にカウント
+ int countDoubleColon = 0;
+ for (int i = 0; i < ipv6.length() - 1; i++) {
+ if (ipv6.charAt(i) == ':' && ipv6.charAt(i + 1) == ':') {
+ countDoubleColon++;
+ if (i + 2 < ipv6.length() && ipv6.charAt(i + 2) == ':') {
+ return false;
+ }
+ i++;
+ }
+ }
+
+ if (countDoubleColon > 1) {
+ return false;
+ }
+
+ // splitする際、末尾の空要素を保持するために第二引数に -1 を指定
+ String[] parts = ipv6.split(":", -1);
+
+ boolean hasDoubleColon = countDoubleColon == 1;
+
+ // ブロック数の検証
+ if (hasDoubleColon) {
+ if (parts.length > 9 || parts.length < 3) {
+ return false;
+ }
+ } else {
+ if (parts.length != 8) {
+ return false;
+ }
+ }
+
+ int emptyCount = 0;
+
+ for (String part : parts) {
+ if (part.isEmpty()) {
+ emptyCount++;
+ if (emptyCount > 2) {
+ return false;
+ }
+ continue;
+ }
+
+ if (part.length() > 4) {
+ return false;
+ }
+
+ try {
+ int value = Integer.parseInt(part, 16);
+ if (value < 0 || value > 0xFFFF) {
+ return false;
+ }
+ } catch (NumberFormatException e) {
+ return false;
+ }
+ }
+
+ if (!hasDoubleColon && emptyCount > 0) {
+ return false;
+ }
+
+ return true;
+ }
+
+ public static boolean isValidHostPort(String hostPort) {
+ if (hostPort == null || hostPort.isEmpty()) {
+ return false;
+ }
+ try {
+ HostPortPair hostPortPair = HostPortPair.parse(hostPort);
+ if (!(isValidIPv4Address(hostPortPair.getHost()) || isDomain(hostPortPair.getHost()))) {
+ return false;
+ }
+ if (!isValiPort(hostPortPair.getPort())) {
+ return false;
+ }
+ } catch (IllegalArgumentException e) {
+ return false;
+ }
+ return true;
+ }
+
+ public static boolean isValiPort(int port) {
+ if (0 <= port && port < 65536) {
+ return true;
+ }
+ return false;
+ }
+
+ public static boolean isDomain(String host) {
+ if (host == null || host.isEmpty()) return false;
+ // Basic domain validation:
+ // 1. Starts with an alphabetic character
+ // 2. Contains only alphanumeric characters, dots, and hyphens
+ // 3. No leading/trailing hyphens in labels
+ return host.matches("^[a-zA-Z]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(\\.[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$");
+ }
+
+ public static boolean isValidHost(String host) {
+ return isValidIPv4Address(host) || isDomain(host);
+ }
+
+ public static List toCIDR(String start, String end) throws UnknownHostException {
+ return toCIDR(InetAddress.getByName(start), InetAddress.getByName(end));
+ }
+
+ public static List toCIDR(InetAddress start, InetAddress end) throws UnknownHostException {
+ List listResult = new ArrayList<>();
+
+ Log.i(TAG, "toCIDR(" + start.getHostAddress() + "," + end.getHostAddress() + ")");
+
+ long from = inet2long(start);
+ long to = inet2long(end);
+ while (to >= from) {
+ byte prefix = 32;
+ while (prefix > 0) {
+ long mask = prefix2mask(prefix - 1);
+ if ((from & mask) != from)
+ break;
+ prefix--;
+ }
+ byte max = (byte) (32 - Math.floor(Math.log(to - from + 1) / Math.log(2)));
+ if (prefix < max)
+ prefix = max;
+
+ listResult.add(new CIDR(long2inet(from), prefix));
+
+ BigInteger p = BigInteger.valueOf(2).pow(32 - prefix);
+ from += p.longValue();
+// from += (long) Math.pow(2, (32 - prefix));
+ }
+ for (CIDR cidr : listResult)
+ Log.i(TAG, cidr.toString());
+
+ return listResult;
+ }
+
+ private static long prefix2mask(int bits) {
+ return (0xFFFFFFFF00000000L >> bits) & 0xFFFFFFFFL;
+ }
+
+ private static long inet2long(InetAddress addr) {
+ long result = 0;
+ if (addr != null)
+ for (byte b : addr.getAddress())
+ result = result << 8 | (b & 0xFF);
+ return result;
+ }
+
+ private static InetAddress long2inet(long addr) {
+ try {
+ byte[] b = new byte[4];
+ for (int i = b.length - 1; i >= 0; i--) {
+ b[i] = (byte) (addr & 0xFF);
+ addr = addr >> 8;
+ }
+ return InetAddress.getByAddress(b);
+ } catch (UnknownHostException ignore) {
+ return null;
+ }
+ }
+
+ public static InetAddress minus1(InetAddress addr) {
+ return long2inet(inet2long(addr) - 1);
+ }
+
+ public static InetAddress plus1(InetAddress addr) {
+ return long2inet(inet2long(addr) + 1);
+ }
+
+ private static native String jni_getprop(String name);
+
+ public static List getDefaultDNS(Context applicationContext) {
+ List dnsServers = new ArrayList<>();
+ // ConnectivityManagerの取得
+ ConnectivityManager cm = (ConnectivityManager) applicationContext.getSystemService(Context.CONNECTIVITY_SERVICE);
+ if (cm == null) {
+ return dnsServers;
+ }
+
+ // 現在アクティブなネットワークを取得
+ Network activeNetwork = cm.getActiveNetwork();
+ if (activeNetwork == null) {
+ return dnsServers;
+ }
+ // ネットワークのリンクプロパティ(DNS情報などを含む)を取得
+ LinkProperties linkProperties = cm.getLinkProperties(activeNetwork);
+ if (linkProperties == null) {
+ return dnsServers;
+ }
+ // DNSサーバーのリスト(InetAddress形式)を取得
+ List dnsAddresses = linkProperties.getDnsServers();
+ for (InetAddress dnsAddress : dnsAddresses) {
+ // ホスト名/IPアドレスの文字列を取得(IPv4/IPv6 両対応)
+ dnsServers.add(dnsAddress.getHostAddress());
+ }
+ return dnsServers;
+ }
+
+ public static class CIDR implements Comparable {
+ public InetAddress address;
+ public int prefix;
+
+ public CIDR(InetAddress address, int prefix) {
+ this.address = address;
+ this.prefix = prefix;
+ }
+
+ public CIDR(String ip, int prefix) {
+ try {
+ this.address = InetAddress.getByName(ip);
+ this.prefix = prefix;
+ } catch (UnknownHostException ex) {
+ Log.e(TAG, ex + "\n" + Log.getStackTraceString(ex));
+ }
+ }
+
+ public InetAddress getStart() {
+ return long2inet(inet2long(this.address) & prefix2mask(this.prefix));
+ }
+
+ public InetAddress getEnd() {
+ return long2inet((inet2long(this.address) & prefix2mask(this.prefix)) + (1L << (32 - this.prefix)) - 1);
+ }
+
+ @Override
+ public String toString() {
+ return this.address.getHostAddress() + "/" + this.prefix + "=" + getStart().getHostAddress() + "..." + getEnd().getHostAddress();
+ }
+
+ @Override
+ public int compareTo(CIDR other) {
+ Long lcidr = NetUtil.inet2long(this.address);
+ Long lother = NetUtil.inet2long(other.address);
+ return lcidr.compareTo(lother);
+ }
+
+
+ }
+}
diff --git a/android_app/app/src/main/java/tun/utils/ProgressTask.java b/android_app/app/src/main/java/tun/utils/ProgressTask.java
new file mode 100644
index 00000000..2dbca629
--- /dev/null
+++ b/android_app/app/src/main/java/tun/utils/ProgressTask.java
@@ -0,0 +1,95 @@
+package tun.utils;
+
+import android.os.Handler;
+import android.os.Looper;
+import android.util.Log;
+
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+
+import tun.proxy.R;
+
+public abstract class ProgressTask {
+ private static final String TAG = "ProgressTask";
+
+ private volatile Status mStatus = Status.PENDING;
+ private boolean canceled = false;
+
+ public final ProgressTask.Status getStatus() {
+ return mStatus;
+ }
+
+ @SafeVarargs
+ public final void execute(Params... params) {
+ try (ExecutorService executorService = Executors.newSingleThreadExecutor()) {
+ executorService.submit(new ProgressRunnable(params));
+ }
+ }
+
+ protected void onPreExecute() {
+ }
+
+ @SuppressWarnings("unchecked")
+ protected abstract Result doInBackground(Params... params);
+
+ protected void onPostExecute(Result result) {
+ }
+
+ public void cancel(boolean flag) {
+ canceled = flag;
+ }
+
+ public final boolean isCancelled() {
+ return canceled;
+ }
+
+ protected void onCancelled() {
+ }
+
+ public enum Status {PENDING, RUNNING, FINISHED}
+
+ private class ProgressRunnable implements Runnable {
+
+ final Params[] params;
+ Handler handler = new Handler(Looper.getMainLooper());
+ private Result result;
+
+ @SafeVarargs
+ public ProgressRunnable(Params... params) {
+ this.params = params;
+ }
+
+ @Override
+ public void run() {
+ if (mStatus != Status.PENDING) {
+ switch (mStatus) {
+ case RUNNING:
+ throw new IllegalStateException("Cannot execute task:"
+ + " the task is already running.");
+ case FINISHED:
+ throw new IllegalStateException("Cannot execute task:"
+ + " the task has already been executed "
+ + "(a task can be executed only once)");
+ }
+ }
+ mStatus = Status.RUNNING;
+ try {
+ onPreExecute();
+ result = doInBackground(params);
+ } catch (Exception ex) {
+ Log.e(TAG, ex.getMessage(), ex);
+ }
+ handler.post(new Runnable() {
+ @Override
+ public void run() {
+ if (!canceled) {
+ onPostExecute(result);
+ mStatus = Status.FINISHED;
+ } else {
+ onCancelled();
+ }
+ }
+ });
+ }
+ }
+}
\ No newline at end of file
diff --git a/android_app/app/src/main/res/drawable-v24/ic_launcher_foreground.xml b/android_app/app/src/main/res/drawable-v24/ic_launcher_foreground.xml
index c3903edf..2b068d11 100644
--- a/android_app/app/src/main/res/drawable-v24/ic_launcher_foreground.xml
+++ b/android_app/app/src/main/res/drawable-v24/ic_launcher_foreground.xml
@@ -1,34 +1,30 @@
-
+ xmlns:aapt="http://schemas.android.com/aapt"
+ android:width="108dp"
+ android:height="108dp"
+ android:viewportWidth="108"
+ android:viewportHeight="108">
+
+ android:offset="0.0" />
+ android:offset="1.0" />
-
+ android:pathData="M65.3,45.828l3.8,-6.6c0.2,-0.4 0.1,-0.9 -0.3,-1.1c-0.4,-0.2 -0.9,-0.1 -1.1,0.3l-3.9,6.7c-6.3,-2.8 -13.4,-2.8 -19.7,0l-3.9,-6.7c-0.2,-0.4 -0.7,-0.5 -1.1,-0.3C38.8,38.328 38.7,38.828 38.9,39.228l3.8,6.6C36.2,49.428 31.7,56.028 31,63.928h46C76.3,56.028 71.8,49.428 65.3,45.828zM43.4,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2c-0.3,-0.7 -0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C45.3,56.528 44.5,57.328 43.4,57.328L43.4,57.328zM64.6,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2s-0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C66.5,56.528 65.6,57.328 64.6,57.328L64.6,57.328z"
+ android:strokeWidth="1"
+ android:strokeColor="#00000000" />
+
\ No newline at end of file
diff --git a/android_app/app/src/main/res/drawable/ic_launcher_background.xml b/android_app/app/src/main/res/drawable/ic_launcher_background.xml
index 5713f34a..07d5da9c 100644
--- a/android_app/app/src/main/res/drawable/ic_launcher_background.xml
+++ b/android_app/app/src/main/res/drawable/ic_launcher_background.xml
@@ -1,171 +1,170 @@
-
+ android:viewportWidth="108"
+ android:viewportHeight="108">
+ android:fillColor="#3DDC84"
+ android:pathData="M0,0h108v108h-108z" />
+ android:strokeWidth="0.8"
+ android:strokeColor="#33FFFFFF" />
+ android:strokeWidth="0.8"
+ android:strokeColor="#33FFFFFF" />
+ android:strokeWidth="0.8"
+ android:strokeColor="#33FFFFFF" />
+ android:strokeWidth="0.8"
+ android:strokeColor="#33FFFFFF" />
+ android:strokeWidth="0.8"
+ android:strokeColor="#33FFFFFF" />
+ android:strokeWidth="0.8"
+ android:strokeColor="#33FFFFFF" />
+ android:strokeWidth="0.8"
+ android:strokeColor="#33FFFFFF" />
+ android:strokeWidth="0.8"
+ android:strokeColor="#33FFFFFF" />
+ android:strokeWidth="0.8"
+ android:strokeColor="#33FFFFFF" />
+ android:strokeWidth="0.8"
+ android:strokeColor="#33FFFFFF" />
+ android:strokeWidth="0.8"
+ android:strokeColor="#33FFFFFF" />
+ android:strokeWidth="0.8"
+ android:strokeColor="#33FFFFFF" />
+ android:strokeWidth="0.8"
+ android:strokeColor="#33FFFFFF" />
+ android:strokeWidth="0.8"
+ android:strokeColor="#33FFFFFF" />
+ android:strokeWidth="0.8"
+ android:strokeColor="#33FFFFFF" />
+ android:strokeWidth="0.8"
+ android:strokeColor="#33FFFFFF" />
+ android:strokeWidth="0.8"
+ android:strokeColor="#33FFFFFF" />
+ android:strokeWidth="0.8"
+ android:strokeColor="#33FFFFFF" />
+ android:strokeWidth="0.8"
+ android:strokeColor="#33FFFFFF" />
+ android:strokeWidth="0.8"
+ android:strokeColor="#33FFFFFF" />
+ android:strokeWidth="0.8"
+ android:strokeColor="#33FFFFFF" />
+ android:strokeWidth="0.8"
+ android:strokeColor="#33FFFFFF" />
+ android:strokeWidth="0.8"
+ android:strokeColor="#33FFFFFF" />
+ android:strokeWidth="0.8"
+ android:strokeColor="#33FFFFFF" />
+ android:strokeWidth="0.8"
+ android:strokeColor="#33FFFFFF" />
+ android:strokeWidth="0.8"
+ android:strokeColor="#33FFFFFF" />
+ android:strokeWidth="0.8"
+ android:strokeColor="#33FFFFFF" />
+ android:strokeWidth="0.8"
+ android:strokeColor="#33FFFFFF" />
+ android:strokeWidth="0.8"
+ android:strokeColor="#33FFFFFF" />
+ android:strokeWidth="0.8"
+ android:strokeColor="#33FFFFFF" />
+ android:strokeWidth="0.8"
+ android:strokeColor="#33FFFFFF" />
+ android:strokeWidth="0.8"
+ android:strokeColor="#33FFFFFF" />
diff --git a/android_app/app/src/main/res/drawable/ic_profile_add.xml b/android_app/app/src/main/res/drawable/ic_profile_add.xml
new file mode 100644
index 00000000..333ef596
--- /dev/null
+++ b/android_app/app/src/main/res/drawable/ic_profile_add.xml
@@ -0,0 +1,20 @@
+
+
+
+
+
+
diff --git a/android_app/app/src/main/res/drawable/ic_profile_delete.xml b/android_app/app/src/main/res/drawable/ic_profile_delete.xml
new file mode 100644
index 00000000..197a1de9
--- /dev/null
+++ b/android_app/app/src/main/res/drawable/ic_profile_delete.xml
@@ -0,0 +1,20 @@
+
+
+
+
+
+
diff --git a/android_app/app/src/main/res/drawable/ic_profile_edit.xml b/android_app/app/src/main/res/drawable/ic_profile_edit.xml
new file mode 100644
index 00000000..9c787fe8
--- /dev/null
+++ b/android_app/app/src/main/res/drawable/ic_profile_edit.xml
@@ -0,0 +1,20 @@
+
+
+
+
+
+
diff --git a/android_app/app/src/main/res/layout/activity_main.xml b/android_app/app/src/main/res/layout/activity_main.xml
index 8ed07922..0240b3b1 100644
--- a/android_app/app/src/main/res/layout/activity_main.xml
+++ b/android_app/app/src/main/res/layout/activity_main.xml
@@ -1,33 +1,27 @@
-
+ android:background="@color/colorPrimaryDark"
+ tools:context=".MainActivity">
-
+ android:theme="@style/AppTheme.AppBarOverlay">
-
+
-
+
+
+
-
+
\ No newline at end of file
diff --git a/android_app/app/src/main/res/layout/activity_profile_settings.xml b/android_app/app/src/main/res/layout/activity_profile_settings.xml
new file mode 100644
index 00000000..3a0d86c9
--- /dev/null
+++ b/android_app/app/src/main/res/layout/activity_profile_settings.xml
@@ -0,0 +1,15 @@
+
+
+
+
+
+
diff --git a/android_app/app/src/main/res/layout/activity_settings.xml b/android_app/app/src/main/res/layout/activity_settings.xml
new file mode 100644
index 00000000..9c4e517f
--- /dev/null
+++ b/android_app/app/src/main/res/layout/activity_settings.xml
@@ -0,0 +1,15 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/android_app/app/src/main/res/layout/content_main.xml b/android_app/app/src/main/res/layout/content_main.xml
new file mode 100644
index 00000000..ec07875a
--- /dev/null
+++ b/android_app/app/src/main/res/layout/content_main.xml
@@ -0,0 +1,65 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/android_app/app/src/main/res/layout/dialog_profile_edit.xml b/android_app/app/src/main/res/layout/dialog_profile_edit.xml
new file mode 100644
index 00000000..84e9800d
--- /dev/null
+++ b/android_app/app/src/main/res/layout/dialog_profile_edit.xml
@@ -0,0 +1,36 @@
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/android_app/app/src/main/res/layout/item_profile.xml b/android_app/app/src/main/res/layout/item_profile.xml
new file mode 100644
index 00000000..f079fef4
--- /dev/null
+++ b/android_app/app/src/main/res/layout/item_profile.xml
@@ -0,0 +1,63 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/android_app/app/src/main/res/layout/preference_progress.xml b/android_app/app/src/main/res/layout/preference_progress.xml
new file mode 100644
index 00000000..18470aa1
--- /dev/null
+++ b/android_app/app/src/main/res/layout/preference_progress.xml
@@ -0,0 +1,76 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/android_app/app/src/main/res/layout/spinner_item.xml b/android_app/app/src/main/res/layout/spinner_item.xml
new file mode 100644
index 00000000..0d258289
--- /dev/null
+++ b/android_app/app/src/main/res/layout/spinner_item.xml
@@ -0,0 +1,9 @@
+
+
\ No newline at end of file
diff --git a/android_app/app/src/main/res/menu/menu_main.xml b/android_app/app/src/main/res/menu/menu_main.xml
new file mode 100644
index 00000000..6f073a0c
--- /dev/null
+++ b/android_app/app/src/main/res/menu/menu_main.xml
@@ -0,0 +1,19 @@
+
diff --git a/android_app/app/src/main/res/menu/menu_profile_settings.xml b/android_app/app/src/main/res/menu/menu_profile_settings.xml
new file mode 100644
index 00000000..20a5fe52
--- /dev/null
+++ b/android_app/app/src/main/res/menu/menu_profile_settings.xml
@@ -0,0 +1,9 @@
+
+
diff --git a/android_app/app/src/main/res/menu/menu_search.xml b/android_app/app/src/main/res/menu/menu_search.xml
new file mode 100644
index 00000000..585f5117
--- /dev/null
+++ b/android_app/app/src/main/res/menu/menu_search.xml
@@ -0,0 +1,102 @@
+
+
\ No newline at end of file
diff --git a/android_app/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/android_app/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
index bbd3e021..c4a603d4 100644
--- a/android_app/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
+++ b/android_app/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
@@ -1,5 +1,5 @@
-
+
\ No newline at end of file
diff --git a/android_app/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/android_app/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
index bbd3e021..c4a603d4 100644
--- a/android_app/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
+++ b/android_app/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
@@ -1,5 +1,5 @@
-
+
\ No newline at end of file
diff --git a/android_app/app/src/main/res/mipmap-hdpi/ic_launcher.png b/android_app/app/src/main/res/mipmap-hdpi/ic_launcher.png
index a2f59082..ef233549 100644
Binary files a/android_app/app/src/main/res/mipmap-hdpi/ic_launcher.png and b/android_app/app/src/main/res/mipmap-hdpi/ic_launcher.png differ
diff --git a/android_app/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png b/android_app/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png
new file mode 100644
index 00000000..3e61e3ef
Binary files /dev/null and b/android_app/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png differ
diff --git a/android_app/app/src/main/res/mipmap-hdpi/ic_launcher_round.png b/android_app/app/src/main/res/mipmap-hdpi/ic_launcher_round.png
index 1b523998..211c2f8f 100644
Binary files a/android_app/app/src/main/res/mipmap-hdpi/ic_launcher_round.png and b/android_app/app/src/main/res/mipmap-hdpi/ic_launcher_round.png differ
diff --git a/android_app/app/src/main/res/mipmap-mdpi/ic_launcher.png b/android_app/app/src/main/res/mipmap-mdpi/ic_launcher.png
index ff10afd6..b1dc259a 100644
Binary files a/android_app/app/src/main/res/mipmap-mdpi/ic_launcher.png and b/android_app/app/src/main/res/mipmap-mdpi/ic_launcher.png differ
diff --git a/android_app/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png b/android_app/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png
new file mode 100644
index 00000000..c419935d
Binary files /dev/null and b/android_app/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png differ
diff --git a/android_app/app/src/main/res/mipmap-mdpi/ic_launcher_round.png b/android_app/app/src/main/res/mipmap-mdpi/ic_launcher_round.png
index 115a4c76..5869e1b4 100644
Binary files a/android_app/app/src/main/res/mipmap-mdpi/ic_launcher_round.png and b/android_app/app/src/main/res/mipmap-mdpi/ic_launcher_round.png differ
diff --git a/android_app/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/android_app/app/src/main/res/mipmap-xhdpi/ic_launcher.png
index dcd3cd80..cfe7d98f 100644
Binary files a/android_app/app/src/main/res/mipmap-xhdpi/ic_launcher.png and b/android_app/app/src/main/res/mipmap-xhdpi/ic_launcher.png differ
diff --git a/android_app/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png b/android_app/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png
new file mode 100644
index 00000000..3f2c2057
Binary files /dev/null and b/android_app/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png differ
diff --git a/android_app/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png b/android_app/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png
index 459ca609..1fe80536 100644
Binary files a/android_app/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png and b/android_app/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png differ
diff --git a/android_app/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/android_app/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
index 8ca12fe0..69e1e604 100644
Binary files a/android_app/app/src/main/res/mipmap-xxhdpi/ic_launcher.png and b/android_app/app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ
diff --git a/android_app/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png b/android_app/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png
new file mode 100644
index 00000000..17e82c46
Binary files /dev/null and b/android_app/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png differ
diff --git a/android_app/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png b/android_app/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png
index 8e19b410..99f53c51 100644
Binary files a/android_app/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png and b/android_app/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png differ
diff --git a/android_app/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/android_app/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
index b824ebdd..6849686a 100644
Binary files a/android_app/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png and b/android_app/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ
diff --git a/android_app/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png b/android_app/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png
new file mode 100644
index 00000000..7c457752
Binary files /dev/null and b/android_app/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png differ
diff --git a/android_app/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png b/android_app/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png
index 4c19a13c..931d6153 100644
Binary files a/android_app/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png and b/android_app/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png differ
diff --git a/android_app/app/src/main/res/navigation/nav_graph.xml b/android_app/app/src/main/res/navigation/nav_graph.xml
new file mode 100644
index 00000000..cd64c551
--- /dev/null
+++ b/android_app/app/src/main/res/navigation/nav_graph.xml
@@ -0,0 +1,7 @@
+
+
+
+
diff --git a/android_app/app/src/main/res/values/colors.xml b/android_app/app/src/main/res/values/colors.xml
index 3ab3e9cb..030098fe 100644
--- a/android_app/app/src/main/res/values/colors.xml
+++ b/android_app/app/src/main/res/values/colors.xml
@@ -1,6 +1,6 @@
- #3F51B5
- #303F9F
- #FF4081
+ #6200EE
+ #3700B3
+ #03DAC5
diff --git a/android_app/app/src/main/res/values/dimens.xml b/android_app/app/src/main/res/values/dimens.xml
new file mode 100644
index 00000000..59a0b0c4
--- /dev/null
+++ b/android_app/app/src/main/res/values/dimens.xml
@@ -0,0 +1,3 @@
+
+ 16dp
+
diff --git a/android_app/app/src/main/res/values/strings.xml b/android_app/app/src/main/res/values/strings.xml
index 407964bb..8b78948b 100644
--- a/android_app/app/src/main/res/values/strings.xml
+++ b/android_app/app/src/main/res/values/strings.xml
@@ -1,10 +1,84 @@
- tun2http
+ TunProxy
+
+
Proxy is running
Proxy is running
Start
Stop
Start failed
Proxy address (host:port)
- Enter valid ip v4 address
+ Enter valid address and port
+
+
+ About
+ Settings
+ Profile Settings
+
+
+ Search
+ show system app
+ sort by
+ app name
+ package name
+ order by
+ ascending
+ descending
+ filter by
+ Please wait
+ Progress
+
+
+ VPN connection setting
+
+ Allowed / Disallowed Application
+ Allowed application list
+ selected application count:
+ Disallowed application list
+ selected application count:
+ Clear all selections of Allowed / Disallowed application list
+
+
+ - Disallowed Application
+ - Allowed Application
+
+
+ - DISALLOW
+ - ALLOW
+
+
+
+ Proxy Setting
+ Connectivity Check
+ Check proxy connectivity before starting
+
+ - HTTP
+ - SOCKS5
+
+
+
+ DNS Setting
+ Use Custom DNS
+ Enable to specify custom DNS servers
+ Primary DNS
+ Secondary DNS
+ Profile Name
+ Host:Port
+
+
+ Manual
+ Profile Name
+ Delete Profile
+ Are you sure you want to delete this profile?
+ New
+ New Profile
+ Edit Profile
+ Please enter name fields
+ Invalid Host:Port format
+ Invalid port number(0 >= port >= 65536)
+ Edit
+ Delete
+ Profile Settings
+ Invalid IP Address
+
diff --git a/android_app/app/src/main/res/values/styles.xml b/android_app/app/src/main/res/values/styles.xml
index 5885930d..083c770c 100644
--- a/android_app/app/src/main/res/values/styles.xml
+++ b/android_app/app/src/main/res/values/styles.xml
@@ -8,4 +8,12 @@
- @color/colorAccent
+
+
+
+
+
diff --git a/android_app/app/src/main/res/xml/network_security_config.xml b/android_app/app/src/main/res/xml/network_security_config.xml
new file mode 100644
index 00000000..45f56937
--- /dev/null
+++ b/android_app/app/src/main/res/xml/network_security_config.xml
@@ -0,0 +1,10 @@
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/android_app/app/src/main/res/xml/preferences.xml b/android_app/app/src/main/res/xml/preferences.xml
new file mode 100644
index 00000000..40c7c94b
--- /dev/null
+++ b/android_app/app/src/main/res/xml/preferences.xml
@@ -0,0 +1,64 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/android_app/app/src/test/java/com/tun2http/app/ExampleUnitTest.java b/android_app/app/src/test/java/com/tun2http/app/ExampleUnitTest.java
deleted file mode 100644
index ea8d45d5..00000000
--- a/android_app/app/src/test/java/com/tun2http/app/ExampleUnitTest.java
+++ /dev/null
@@ -1,17 +0,0 @@
-package com.tun2http.app;
-
-import org.junit.Test;
-
-import static org.junit.Assert.*;
-
-/**
- * Example local unit test, which will execute on the development machine (host).
- *
- * @see Testing documentation
- */
-public class ExampleUnitTest {
- @Test
- public void addition_isCorrect() throws Exception {
- assertEquals(4, 2 + 2);
- }
-}
\ No newline at end of file
diff --git a/android_app/app/src/test/java/tun/proxy/HostPortPairTest.java b/android_app/app/src/test/java/tun/proxy/HostPortPairTest.java
new file mode 100644
index 00000000..6e708ecd
--- /dev/null
+++ b/android_app/app/src/test/java/tun/proxy/HostPortPairTest.java
@@ -0,0 +1,72 @@
+package tun.proxy;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+import org.junit.Test;
+
+import java.util.EnumSet;
+
+public class HostPortPairTest {
+ @Test
+ public void testHostPortPair() {
+ HostPortPair pair = new HostPortPair("127.0.0.1", 8080);
+ assertEquals("127.0.0.1", pair.getHost());
+ assertEquals(8080, pair.getPort());
+ }
+
+ @Test
+ public void testParse() {
+ try {
+ HostPortPair pair = HostPortPair.parse("127.0.0.1:8080");
+ assertEquals("127.0.0.1", pair.getHost());
+ assertEquals(8080, pair.getPort());
+ } catch (IllegalArgumentException ex) {
+ fail();
+ }
+ try {
+ HostPortPair pair = HostPortPair.parse("www.example.com:80");
+ assertEquals("www.example.com", pair.getHost());
+ assertEquals(80, pair.getPort());
+ } catch (IllegalArgumentException ex) {
+ fail();
+ }
+ try {
+ HostPortPair pair = HostPortPair.parse("127.0.0.1:8080:8008");
+ fail();
+ } catch (IllegalArgumentException ex) {
+ assertTrue(true);
+ }
+ try {
+ HostPortPair pair = HostPortPair.parse("127.0.0.1");
+ fail();
+ } catch (IllegalArgumentException ex) {
+ assertTrue(true);
+ }
+ try {
+ HostPortPair pair = HostPortPair.parse("127.0.0.1:");
+ fail();
+ } catch (IllegalArgumentException ex) {
+ assertTrue(true);
+ }
+ }
+
+ @Test
+ public void testValueOf() {
+ {
+ String hostPort = HostPortPair.valueOf("127.0.0.1", 8080);
+ assertEquals("127.0.0.1:8080", hostPort);
+ }
+ {
+ String hostPort = HostPortPair.valueOf("192.168.0.1", 0);
+ assertEquals("192.168.0.1:0", hostPort);
+ }
+ {
+ String hostPort = HostPortPair.valueOf("www.example.com", 80);
+ assertEquals("www.example.com:80", hostPort);
+ }
+
+ }
+
+}
\ No newline at end of file
diff --git a/android_app/app/src/test/java/tun/proxy/ProfileItemTest.java b/android_app/app/src/test/java/tun/proxy/ProfileItemTest.java
new file mode 100644
index 00000000..1e7f88e1
--- /dev/null
+++ b/android_app/app/src/test/java/tun/proxy/ProfileItemTest.java
@@ -0,0 +1,57 @@
+package tun.proxy;
+
+import org.json.JSONException;
+import org.json.JSONObject;
+import org.junit.Test;
+
+import static org.junit.Assert.*;
+
+public class ProfileItemTest {
+
+ @Test
+ public void testSerialization() throws JSONException {
+ ProfileItem item = new ProfileItem("Test Profile", "127.0.0.1", 8080, MyApplication.ProxyType.HTTP);
+ JSONObject json = item.toJSONObject();
+
+ assertEquals("Test Profile", json.getString("name"));
+ assertEquals("127.0.0.1", json.getString("host"));
+ assertEquals(8080, json.getInt("port"));
+ assertEquals("HTTP", json.getString("type"));
+
+ ProfileItem item2 = ProfileItem.fromJSONObject(json);
+ assertEquals(item.getName(), item2.getName());
+ assertEquals(item.getHost(), item2.getHost());
+ assertEquals(item.getPort(), item2.getPort());
+ assertEquals(item.getType(), item2.getType());
+
+ item.setName("Edit Profile");
+ item.setHost("192.168.0.2");
+ item.setPort(1080);
+ item.setType(MyApplication.ProxyType.SOCKS5);
+
+ assertEquals("Edit Profile", item.getName());
+ assertEquals("192.168.0.2", item.getHost());
+ assertEquals(1080, item.getPort());
+ assertEquals(MyApplication.ProxyType.SOCKS5, item.getType());
+ }
+
+ @Test
+ public void testSettersAndGetters() {
+ ProfileItem item = new ProfileItem("Name", "Host", 1, MyApplication.ProxyType.HTTP);
+ item.setName("New Name");
+ item.setHost("New Host");
+ item.setPort(2);
+ item.setType(MyApplication.ProxyType.SOCKS5);
+
+ assertEquals("New Name", item.getName());
+ assertEquals("New Host", item.getHost());
+ assertEquals(2, item.getPort());
+ assertEquals(MyApplication.ProxyType.SOCKS5, item.getType());
+ }
+
+ @Test
+ public void testToString() {
+ ProfileItem item = new ProfileItem("Profile Name", "host", 80, MyApplication.ProxyType.HTTP);
+ assertEquals("Profile Name", item.toString());
+ }
+}
diff --git a/android_app/app/src/test/java/tun/proxy/TunTest.java b/android_app/app/src/test/java/tun/proxy/TunTest.java
new file mode 100644
index 00000000..4698dade
--- /dev/null
+++ b/android_app/app/src/test/java/tun/proxy/TunTest.java
@@ -0,0 +1,22 @@
+package tun.proxy;
+
+import static org.junit.Assert.assertEquals;
+
+import org.junit.Test;
+
+import java.util.EnumSet;
+
+/**
+ * Example local unit test, which will execute on the development machine (host).
+ *
+ * @see Testing documentation
+ */
+public class TunTest {
+ @Test
+ public void testSettingsActivity() {
+ EnumSet filterType = EnumSet.allOf(SettingsActivity.FilterAppType.class);
+ System.out.println("FilterAppType:" + filterType.toString());
+ EnumSet result = SettingsActivity.FilterAppType.parseEnumSet(filterType.toString());
+ assertEquals(result, filterType);
+ }
+}
\ No newline at end of file
diff --git a/android_app/app/src/test/java/tun/utils/CertificateUtilTest.java b/android_app/app/src/test/java/tun/utils/CertificateUtilTest.java
new file mode 100644
index 00000000..4c7b743e
--- /dev/null
+++ b/android_app/app/src/test/java/tun/utils/CertificateUtilTest.java
@@ -0,0 +1,34 @@
+package tun.utils;
+
+import org.junit.Test;
+
+import static org.junit.Assert.*;
+
+public class CertificateUtilTest {
+
+ @Test
+ public void testGetCommonName() {
+ assertEquals("Example CA", CertificateUtil.getCommonName("CN=Example CA, O=Example Org, C=US"));
+ assertEquals("Example CA", CertificateUtil.getCommonName("CN=Example CA"));
+ assertEquals("Example CA", CertificateUtil.getCommonName("O=Example Org, CN=Example CA, C=US"));
+ assertEquals("Example CA", CertificateUtil.getCommonName("O=Example Org, C=US, CN=Example CA"));
+ assertEquals("", CertificateUtil.getCommonName("O=Example Org, C=US"));
+ }
+
+ @Test
+ public void testGetOrganization() {
+ assertEquals("Example Org", CertificateUtil.getOrganization("CN=Example CA, O=Example Org, C=US"));
+ assertEquals("Example Org", CertificateUtil.getOrganization("O=Example Org, CN=Example CA, C=US"));
+ assertEquals("Example Org", CertificateUtil.getOrganization("CN=Example CA, C=US, O=Example Org"));
+ assertEquals("Example Org", CertificateUtil.getOrganization("O=Example Org"));
+ assertEquals("", CertificateUtil.getOrganization("CN=Example CA, C=US"));
+ }
+
+ @Test
+ public void testEncodeDecode() {
+ byte[] original = new byte[]{0x48, 0x65, 0x6c, 0x6c, 0x6f}; // "Hello"
+ String encoded = CertificateUtil.encode(original);
+ byte[] decoded = CertificateUtil.decode(encoded);
+ assertArrayEquals(original, decoded);
+ }
+}
diff --git a/android_app/app/src/test/java/tun/utils/NetUtilTest.java b/android_app/app/src/test/java/tun/utils/NetUtilTest.java
new file mode 100644
index 00000000..10d5c903
--- /dev/null
+++ b/android_app/app/src/test/java/tun/utils/NetUtilTest.java
@@ -0,0 +1,118 @@
+package tun.utils;
+
+import org.junit.Test;
+
+import java.net.InetAddress;
+import java.net.UnknownHostException;
+import java.util.List;
+
+import static org.junit.Assert.*;
+
+public class NetUtilTest {
+
+ @Test
+ public void testIsValidPostPort() {
+ assertFalse(NetUtil.isValidHostPort(""));
+ assertFalse(NetUtil.isValidHostPort("127.0.0.1"));
+ assertFalse(NetUtil.isValidHostPort("127.0.0.1:"));
+ assertTrue(NetUtil.isValidHostPort("127.0.0.1:0"));
+ assertTrue(NetUtil.isValidHostPort("127.0.0.1:1"));
+ assertTrue(NetUtil.isValidHostPort("127.0.0.1:65535"));
+ assertFalse(NetUtil.isValidHostPort("127.0.0.1:65536"));
+ assertFalse(NetUtil.isValidHostPort("127:8000"));
+ assertFalse(NetUtil.isValidHostPort("127.0:8000"));
+ assertFalse(NetUtil.isValidHostPort("127.0.0.0.1:8000"));
+ assertTrue(NetUtil.isValidHostPort("127.255.0.1:8000"));
+ assertFalse(NetUtil.isValidHostPort("127.256.0.1:8000"));
+ assertTrue(NetUtil.isValidHostPort("www.example.com:8000"));
+ assertTrue(NetUtil.isValidHostPort("localhost:8080"));
+ }
+
+ @Test
+ public void testIsValiPort() {
+ assertTrue(NetUtil.isValiPort(0));
+ assertTrue(NetUtil.isValiPort(80));
+ assertTrue(NetUtil.isValiPort(65535));
+ assertFalse(NetUtil.isValiPort(-1));
+ assertFalse(NetUtil.isValiPort(65536));
+ }
+
+ @Test
+ public void testPlusMinus1() throws UnknownHostException {
+ InetAddress addr = InetAddress.getByName("127.0.0.1");
+ InetAddress next = NetUtil.plus1(addr);
+ assertEquals("127.0.0.2", next.getHostAddress());
+
+ InetAddress prev = NetUtil.minus1(next);
+ assertEquals("127.0.0.1", prev.getHostAddress());
+ }
+
+ @Test
+ public void testToCIDR() throws UnknownHostException {
+ List cidrs = NetUtil.toCIDR("192.168.1.0", "192.168.1.255");
+ assertEquals(1, cidrs.size());
+ assertEquals("192.168.1.0/24", cidrs.get(0).address.getHostAddress() + "/" + cidrs.get(0).prefix);
+
+ cidrs = NetUtil.toCIDR("192.168.1.1", "192.168.1.1");
+ assertEquals(1, cidrs.size());
+ assertEquals("192.168.1.1/32", cidrs.get(0).address.getHostAddress() + "/" + cidrs.get(0).prefix);
+ }
+
+ @Test
+ public void testIsDomain() {
+ assertFalse(NetUtil.isDomain(""));
+ assertFalse(NetUtil.isDomain("127"));
+ assertTrue(NetUtil.isDomain("localhost"));
+ assertTrue(NetUtil.isDomain("www.example.com"));
+ assertTrue(NetUtil.isDomain("xn--eckwd4c7cu47r2wf.jp"));
+ }
+
+ @Test
+ public void testIsValidIPv4Address() {
+ assertFalse(NetUtil.isValidIPv4Address(""));
+ assertFalse(NetUtil.isValidIPv4Address("192"));
+ assertFalse(NetUtil.isValidIPv4Address("192.168"));
+ assertFalse(NetUtil.isValidIPv4Address("192.168.0"));
+ assertFalse(NetUtil.isValidIPv4Address("192.168.0."));
+ assertTrue(NetUtil.isValidIPv4Address("127.0.0.1"));
+ assertFalse(NetUtil.isValidIPv4Address("127.0.0.1:"));
+ assertFalse(NetUtil.isValidIPv4Address("127.0.0.1:0"));
+ assertFalse(NetUtil.isValidIPv4Address("127.0.0.1:1"));
+ assertFalse(NetUtil.isValidIPv4Address("127.0.0.1:65535"));
+ assertTrue(NetUtil.isValidIPv4Address("192.168.2.11"));
+ assertFalse(NetUtil.isValidIPv4Address(".192.168.2.11."));
+ assertFalse(NetUtil.isValidIPv4Address("192.168.2.11."));
+ assertFalse(NetUtil.isValidIPv4Address("192.168.2.256"));
+ assertFalse(NetUtil.isValidIPv4Address("192.168.256.11"));
+ assertFalse(NetUtil.isValidIPv4Address("192.256.2.11"));
+ assertTrue(NetUtil.isValidIPv4Address("255.255.255.255"));
+ assertFalse(NetUtil.isValidIPv4Address(null));
+ }
+
+ @Test
+ public void testIsValidIPv6Address() {
+ assertFalse(NetUtil.isValidIPv6Address(""));
+ assertFalse(NetUtil.isValidIPv6Address(":"));
+ assertFalse(NetUtil.isValidIPv6Address("[]"));
+ assertTrue(NetUtil.isValidIPv6Address("::"));
+ assertTrue(NetUtil.isValidIPv6Address("[::]"));
+ assertFalse(NetUtil.isValidIPv6Address(":::"));
+ assertTrue(NetUtil.isValidIPv6Address("::1"));
+ assertTrue(NetUtil.isValidIPv6Address("[::1]"));
+ assertTrue(NetUtil.isValidIPv6Address("2001:0db8:85a3:0000:0000:8a2e:0370:7334"));
+ assertTrue(NetUtil.isValidIPv6Address("2001:0db8:85a3:0:0000:8a2e:370:7334"));
+ assertTrue(NetUtil.isValidIPv6Address("2001:db8:85a3:0:0:8a2e:370:7334"));
+ assertTrue(NetUtil.isValidIPv6Address("2001:db8:85a3::8a2e:370:7334"));
+ assertTrue(NetUtil.isValidIPv6Address("2001:db8::1:0:0:1"));
+ assertTrue(NetUtil.isValidIPv6Address("2001:0db8:0000:0000:3456::"));
+ assertTrue(NetUtil.isValidIPv6Address("2001:0112:0000:0000:0000:0000:0000:0030"));
+ assertTrue(NetUtil.isValidIPv6Address("[2001:0112:0000:0000:0000:0000:0000:0030]"));
+ assertFalse(NetUtil.isValidIPv6Address("[2001:0112:0000:0000:0000:0000:0000:0030"));
+ assertFalse(NetUtil.isValidIPv6Address("2001:0112:0000:0000:0000:0000:0000:0030]"));
+ assertFalse(NetUtil.isValidIPv6Address("[[2001:0112:0000:0000:0000:0000:0000:0030]]"));
+ assertFalse(NetUtil.isValidIPv6Address("2001:0db8::3456::"));
+ assertFalse(NetUtil.isValidIPv6Address("2001:0112::0011::0030"));
+ assertFalse(NetUtil.isValidIPv6Address(null));
+ }
+
+}
diff --git a/android_app/build.gradle b/android_app/build.gradle
index 020eae2e..c12cdb70 100644
--- a/android_app/build.gradle
+++ b/android_app/build.gradle
@@ -1,27 +1,56 @@
// Top-level build file where you can add configuration options common to all sub-projects/modules.
buildscript {
-
+
repositories {
google()
- jcenter()
- }
- dependencies {
- classpath 'com.android.tools.build:gradle:3.0.0'
+ mavenCentral()
+ }
- // NOTE: Do not place your application dependencies here; they belong
- // in the individual module build.gradle files
+ dependencies {
+ classpath 'com.android.tools.build:gradle:9.2.1'
}
}
allprojects {
repositories {
google()
- jcenter()
+ mavenCentral()
+
+ }
+
+ gradle.projectsEvaluated {
+ tasks.withType(JavaCompile).tap {
+ configureEach {
+ options.compilerArgs << "-Xlint:unchecked" << "-Xlint:deprecation"
+ }
+ }
}
}
-task clean(type: Delete) {
- delete rootProject.buildDir
+tasks.register('clean', Delete) {
+ delete rootProject.layout.buildDirectory
+}
+
+tasks.register('release', Zip) {
+ archiveBaseName = "${APP_NAME}_v${VERSION_NAME}"
+ destinationDirectory = file("${projectDir}")
+ from rootProject.rootDir
+ include '*'
+ include 'gradle/**'
+ include 'images/**'
+ include 'app/**'
+ exclude 'build'
+ exclude 'app/.externalNativeBuild'
+ exclude 'app/.cxx'
+ exclude 'app/.gradle'
+ exclude 'app/.idea'
+ exclude 'app/libs'
+ exclude 'app/build'
+ exclude '.git'
+ exclude '.gradle'
+ exclude '.idea'
+ exclude 'local.properties'
+ exclude '*.zip'
}
diff --git a/android_app/changeit.jks b/android_app/changeit.jks
new file mode 100644
index 00000000..fffb1ceb
Binary files /dev/null and b/android_app/changeit.jks differ
diff --git a/android_app/gradle.properties b/android_app/gradle.properties
index aac7c9b4..1a6181b3 100644
--- a/android_app/gradle.properties
+++ b/android_app/gradle.properties
@@ -1,17 +1,31 @@
# Project-wide Gradle settings.
-
# IDE (e.g. Android Studio) users:
# Gradle settings configured through the IDE *will override*
# any settings specified in this file.
-
# For more details on how to configure your build environment visit
# http://www.gradle.org/docs/current/userguide/build_environment.html
-
# Specifies the JVM arguments used for the daemon process.
# The setting is particularly useful for tweaking memory settings.
org.gradle.jvmargs=-Xmx1536m
-
# When configured, Gradle will run in incubating parallel mode.
# This option should only be used with decoupled projects. More details, visit
# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
# org.gradle.parallel=true
+# AndroidX package structure to make it clearer which packages are bundled with the
+# Android operating system and which are packaged with your app's APK
+# https://developer.android.com/topic/libraries/support-library/androidx-rn
+android.useAndroidX=true
+# Automatically convert third-party libraries to use AndroidX
+
+APP_NAME=TunProxy
+VERSION_NAME=1.5.1
+
+productKeyStore=../changeit.jks
+productKeyAlias=key0
+productKeyStorePassword=changeit
+productKeyAliasPassword=changeit
+android.uniquePackageNames=false
+android.dependency.useConstraints=true
+android.r8.strictFullModeForKeepRules=false
+android.newDsl=false
+
diff --git a/android_app/gradle/wrapper/gradle-wrapper.jar b/android_app/gradle/wrapper/gradle-wrapper.jar
index 13372aef..7f93135c 100644
Binary files a/android_app/gradle/wrapper/gradle-wrapper.jar and b/android_app/gradle/wrapper/gradle-wrapper.jar differ
diff --git a/android_app/gradle/wrapper/gradle-wrapper.properties b/android_app/gradle/wrapper/gradle-wrapper.properties
index a0e2b789..a351597e 100644
--- a/android_app/gradle/wrapper/gradle-wrapper.properties
+++ b/android_app/gradle/wrapper/gradle-wrapper.properties
@@ -1,6 +1,7 @@
-#Thu Nov 09 17:21:37 GET 2017
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
+distributionUrl=https\://services.gradle.org/distributions/gradle-9.6.1-bin.zip
+networkTimeout=10000
+validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
-distributionUrl=https\://services.gradle.org/distributions/gradle-4.1-all.zip
diff --git a/android_app/gradlew b/android_app/gradlew
index 9d82f789..1aa94a42 100644
--- a/android_app/gradlew
+++ b/android_app/gradlew
@@ -1,74 +1,127 @@
-#!/usr/bin/env bash
+#!/bin/sh
+
+#
+# Copyright © 2015-2021 the original authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
##############################################################################
-##
-## Gradle start up script for UN*X
-##
+#
+# Gradle start up script for POSIX generated by Gradle.
+#
+# Important for running:
+#
+# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
+# noncompliant, but you have some other compliant shell such as ksh or
+# bash, then to run this script, type that shell name before the whole
+# command line, like:
+#
+# ksh Gradle
+#
+# Busybox and similar reduced shells will NOT work, because this script
+# requires all of these POSIX shell features:
+# * functions;
+# * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
+# «${var#prefix}», «${var%suffix}», and «$( cmd )»;
+# * compound commands having a testable exit status, especially «case»;
+# * various built-in commands including «command», «set», and «ulimit».
+#
+# Important for patching:
+#
+# (2) This script targets any POSIX shell, so it avoids extensions provided
+# by Bash, Ksh, etc; in particular arrays are avoided.
+#
+# The "traditional" practice of packing multiple parameters into a
+# space-separated string is a well documented source of bugs and security
+# problems, so this is (mostly) avoided, by progressively accumulating
+# options in "$@", and eventually passing that to Java.
+#
+# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
+# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
+# see the in-line comments for details.
+#
+# There are tweaks for specific operating systems such as AIX, CygWin,
+# Darwin, MinGW, and NonStop.
+#
+# (3) This script is generated from the Groovy template
+# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
+# within the Gradle project.
+#
+# You can find Gradle at https://github.com/gradle/gradle/.
+#
##############################################################################
-# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
-DEFAULT_JVM_OPTS=""
+# Attempt to set APP_HOME
+
+# Resolve links: $0 may be a link
+app_path=$0
+
+# Need this for daisy-chained symlinks.
+while
+ APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
+ [ -h "$app_path" ]
+do
+ ls=$( ls -ld "$app_path" )
+ link=${ls#*' -> '}
+ case $link in #(
+ /*) app_path=$link ;; #(
+ *) app_path=$APP_HOME$link ;;
+ esac
+done
-APP_NAME="Gradle"
-APP_BASE_NAME=`basename "$0"`
+# This is normally unused
+# shellcheck disable=SC2034
+APP_BASE_NAME=${0##*/}
+# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
+APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit
# Use the maximum available, or set MAX_FD != -1 to use that value.
-MAX_FD="maximum"
+MAX_FD=maximum
-warn ( ) {
+warn () {
echo "$*"
-}
+} >&2
-die ( ) {
+die () {
echo
echo "$*"
echo
exit 1
-}
+} >&2
# OS specific support (must be 'true' or 'false').
cygwin=false
msys=false
darwin=false
-case "`uname`" in
- CYGWIN* )
- cygwin=true
- ;;
- Darwin* )
- darwin=true
- ;;
- MINGW* )
- msys=true
- ;;
+nonstop=false
+case "$( uname )" in #(
+ CYGWIN* ) cygwin=true ;; #(
+ Darwin* ) darwin=true ;; #(
+ MSYS* | MINGW* ) msys=true ;; #(
+ NONSTOP* ) nonstop=true ;;
esac
-# Attempt to set APP_HOME
-# Resolve links: $0 may be a link
-PRG="$0"
-# Need this for relative symlinks.
-while [ -h "$PRG" ] ; do
- ls=`ls -ld "$PRG"`
- link=`expr "$ls" : '.*-> \(.*\)$'`
- if expr "$link" : '/.*' > /dev/null; then
- PRG="$link"
- else
- PRG=`dirname "$PRG"`"/$link"
- fi
-done
-SAVED="`pwd`"
-cd "`dirname \"$PRG\"`/" >/dev/null
-APP_HOME="`pwd -P`"
-cd "$SAVED" >/dev/null
-
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
+
# Determine the Java command to use to start the JVM.
if [ -n "$JAVA_HOME" ] ; then
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
# IBM's JDK on AIX uses strange locations for the executables
- JAVACMD="$JAVA_HOME/jre/sh/java"
+ JAVACMD=$JAVA_HOME/jre/sh/java
else
- JAVACMD="$JAVA_HOME/bin/java"
+ JAVACMD=$JAVA_HOME/bin/java
fi
if [ ! -x "$JAVACMD" ] ; then
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
@@ -77,84 +130,120 @@ Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
else
- JAVACMD="java"
- which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+ JAVACMD=java
+ if ! command -v java >/dev/null 2>&1
+ then
+ die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
+ fi
fi
# Increase the maximum file descriptors if we can.
-if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then
- MAX_FD_LIMIT=`ulimit -H -n`
- if [ $? -eq 0 ] ; then
- if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
- MAX_FD="$MAX_FD_LIMIT"
- fi
- ulimit -n $MAX_FD
- if [ $? -ne 0 ] ; then
- warn "Could not set maximum file descriptor limit: $MAX_FD"
- fi
- else
- warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
- fi
+if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
+ case $MAX_FD in #(
+ max*)
+ # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
+ # shellcheck disable=SC2039,SC3045
+ MAX_FD=$( ulimit -H -n ) ||
+ warn "Could not query maximum file descriptor limit"
+ esac
+ case $MAX_FD in #(
+ '' | soft) :;; #(
+ *)
+ # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
+ # shellcheck disable=SC2039,SC3045
+ ulimit -n "$MAX_FD" ||
+ warn "Could not set maximum file descriptor limit to $MAX_FD"
+ esac
fi
-# For Darwin, add options to specify how the application appears in the dock
-if $darwin; then
- GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
-fi
+# Collect all arguments for the java command, stacking in reverse order:
+# * args from the command line
+# * the main class name
+# * -classpath
+# * -D...appname settings
+# * --module-path (only if needed)
+# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
+
+# For Cygwin or MSYS, switch paths to Windows format before running java
+if "$cygwin" || "$msys" ; then
+ APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
+ CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
+
+ JAVACMD=$( cygpath --unix "$JAVACMD" )
-# For Cygwin, switch paths to Windows format before running java
-if $cygwin ; then
- APP_HOME=`cygpath --path --mixed "$APP_HOME"`
- CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
- JAVACMD=`cygpath --unix "$JAVACMD"`
-
- # We build the pattern for arguments to be converted via cygpath
- ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
- SEP=""
- for dir in $ROOTDIRSRAW ; do
- ROOTDIRS="$ROOTDIRS$SEP$dir"
- SEP="|"
- done
- OURCYGPATTERN="(^($ROOTDIRS))"
- # Add a user-defined pattern to the cygpath arguments
- if [ "$GRADLE_CYGPATTERN" != "" ] ; then
- OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
- fi
# Now convert the arguments - kludge to limit ourselves to /bin/sh
- i=0
- for arg in "$@" ; do
- CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
- CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
-
- if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
- eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
- else
- eval `echo args$i`="\"$arg\""
+ for arg do
+ if
+ case $arg in #(
+ -*) false ;; # don't mess with options #(
+ /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
+ [ -e "$t" ] ;; #(
+ *) false ;;
+ esac
+ then
+ arg=$( cygpath --path --ignore --mixed "$arg" )
fi
- i=$((i+1))
+ # Roll the args list around exactly as many times as the number of
+ # args, so each arg winds up back in the position where it started, but
+ # possibly modified.
+ #
+ # NB: a `for` loop captures its iteration list before it begins, so
+ # changing the positional parameters here affects neither the number of
+ # iterations, nor the values presented in `arg`.
+ shift # remove old arg
+ set -- "$@" "$arg" # push replacement arg
done
- case $i in
- (0) set -- ;;
- (1) set -- "$args0" ;;
- (2) set -- "$args0" "$args1" ;;
- (3) set -- "$args0" "$args1" "$args2" ;;
- (4) set -- "$args0" "$args1" "$args2" "$args3" ;;
- (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
- (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
- (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
- (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
- (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
- esac
fi
-# Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules
-function splitJvmOpts() {
- JVM_OPTS=("$@")
-}
-eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS
-JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME"
-exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@"
+# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
+
+# Collect all arguments for the java command:
+# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
+# and any embedded shellness will be escaped.
+# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
+# treated as '${Hostname}' itself on the command line.
+
+set -- \
+ "-Dorg.gradle.appname=$APP_BASE_NAME" \
+ -classpath "$CLASSPATH" \
+ org.gradle.wrapper.GradleWrapperMain \
+ "$@"
+
+# Stop when "xargs" is not available.
+if ! command -v xargs >/dev/null 2>&1
+then
+ die "xargs is not available"
+fi
+
+# Use "xargs" to parse quoted args.
+#
+# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
+#
+# In Bash we could simply go:
+#
+# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
+# set -- "${ARGS[@]}" "$@"
+#
+# but POSIX shell has neither arrays nor command substitution, so instead we
+# post-process each arg (as a line of input to sed) to backslash-escape any
+# character that might be a shell metacharacter, then use eval to reverse
+# that process (while maintaining the separation between arguments), and wrap
+# the whole thing up as a single "set" statement.
+#
+# This will of course break if any of these variables contains a newline or
+# an unmatched quote.
+#
+
+eval "set -- $(
+ printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
+ xargs -n1 |
+ sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
+ tr '\n' ' '
+ )" '"$@"'
+
+exec "$JAVACMD" "$@"
diff --git a/android_app/gradlew.bat b/android_app/gradlew.bat
index 8a0b282a..7101f8e4 100644
--- a/android_app/gradlew.bat
+++ b/android_app/gradlew.bat
@@ -1,90 +1,92 @@
-@if "%DEBUG%" == "" @echo off
-@rem ##########################################################################
-@rem
-@rem Gradle startup script for Windows
-@rem
-@rem ##########################################################################
-
-@rem Set local scope for the variables with windows NT shell
-if "%OS%"=="Windows_NT" setlocal
-
-@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
-set DEFAULT_JVM_OPTS=
-
-set DIRNAME=%~dp0
-if "%DIRNAME%" == "" set DIRNAME=.
-set APP_BASE_NAME=%~n0
-set APP_HOME=%DIRNAME%
-
-@rem Find java.exe
-if defined JAVA_HOME goto findJavaFromJavaHome
-
-set JAVA_EXE=java.exe
-%JAVA_EXE% -version >NUL 2>&1
-if "%ERRORLEVEL%" == "0" goto init
-
-echo.
-echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
-echo.
-echo Please set the JAVA_HOME variable in your environment to match the
-echo location of your Java installation.
-
-goto fail
-
-:findJavaFromJavaHome
-set JAVA_HOME=%JAVA_HOME:"=%
-set JAVA_EXE=%JAVA_HOME%/bin/java.exe
-
-if exist "%JAVA_EXE%" goto init
-
-echo.
-echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
-echo.
-echo Please set the JAVA_HOME variable in your environment to match the
-echo location of your Java installation.
-
-goto fail
-
-:init
-@rem Get command-line arguments, handling Windowz variants
-
-if not "%OS%" == "Windows_NT" goto win9xME_args
-if "%@eval[2+2]" == "4" goto 4NT_args
-
-:win9xME_args
-@rem Slurp the command line arguments.
-set CMD_LINE_ARGS=
-set _SKIP=2
-
-:win9xME_args_slurp
-if "x%~1" == "x" goto execute
-
-set CMD_LINE_ARGS=%*
-goto execute
-
-:4NT_args
-@rem Get arguments from the 4NT Shell from JP Software
-set CMD_LINE_ARGS=%$
-
-:execute
-@rem Setup the command line
-
-set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
-
-@rem Execute Gradle
-"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
-
-:end
-@rem End local scope for the variables with windows NT shell
-if "%ERRORLEVEL%"=="0" goto mainEnd
-
-:fail
-rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
-rem the _cmd.exe /c_ return code!
-if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
-exit /b 1
-
-:mainEnd
-if "%OS%"=="Windows_NT" endlocal
-
-:omega
+@rem
+@rem Copyright 2015 the original author or authors.
+@rem
+@rem Licensed under the Apache License, Version 2.0 (the "License");
+@rem you may not use this file except in compliance with the License.
+@rem You may obtain a copy of the License at
+@rem
+@rem https://www.apache.org/licenses/LICENSE-2.0
+@rem
+@rem Unless required by applicable law or agreed to in writing, software
+@rem distributed under the License is distributed on an "AS IS" BASIS,
+@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+@rem See the License for the specific language governing permissions and
+@rem limitations under the License.
+@rem
+
+@if "%DEBUG%"=="" @echo off
+@rem ##########################################################################
+@rem
+@rem Gradle startup script for Windows
+@rem
+@rem ##########################################################################
+
+@rem Set local scope for the variables with windows NT shell
+if "%OS%"=="Windows_NT" setlocal
+
+set DIRNAME=%~dp0
+if "%DIRNAME%"=="" set DIRNAME=.
+@rem This is normally unused
+set APP_BASE_NAME=%~n0
+set APP_HOME=%DIRNAME%
+
+@rem Resolve any "." and ".." in APP_HOME to make it shorter.
+for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
+
+@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
+
+@rem Find java.exe
+if defined JAVA_HOME goto findJavaFromJavaHome
+
+set JAVA_EXE=java.exe
+%JAVA_EXE% -version >NUL 2>&1
+if %ERRORLEVEL% equ 0 goto execute
+
+echo. 1>&2
+echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
+echo. 1>&2
+echo Please set the JAVA_HOME variable in your environment to match the 1>&2
+echo location of your Java installation. 1>&2
+
+goto fail
+
+:findJavaFromJavaHome
+set JAVA_HOME=%JAVA_HOME:"=%
+set JAVA_EXE=%JAVA_HOME%/bin/java.exe
+
+if exist "%JAVA_EXE%" goto execute
+
+echo. 1>&2
+echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
+echo. 1>&2
+echo Please set the JAVA_HOME variable in your environment to match the 1>&2
+echo location of your Java installation. 1>&2
+
+goto fail
+
+:execute
+@rem Setup the command line
+
+set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
+
+
+@rem Execute Gradle
+"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
+
+:end
+@rem End local scope for the variables with windows NT shell
+if %ERRORLEVEL% equ 0 goto mainEnd
+
+:fail
+rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
+rem the _cmd.exe /c_ return code!
+set EXIT_CODE=%ERRORLEVEL%
+if %EXIT_CODE% equ 0 set EXIT_CODE=1
+if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
+exit /b %EXIT_CODE%
+
+:mainEnd
+if "%OS%"=="Windows_NT" endlocal
+
+:omega
diff --git a/android_app/settings.gradle b/android_app/settings.gradle
index e7b4def4..806f7205 100644
--- a/android_app/settings.gradle
+++ b/android_app/settings.gradle
@@ -1 +1,2 @@
+rootProject.name='TunProxy'
include ':app'
diff --git a/images/Menu-Profile-Delete.png b/images/Menu-Profile-Delete.png
new file mode 100644
index 00000000..e2e853bb
Binary files /dev/null and b/images/Menu-Profile-Delete.png differ
diff --git a/images/Menu-Profile-Edit.png b/images/Menu-Profile-Edit.png
new file mode 100644
index 00000000..e8871795
Binary files /dev/null and b/images/Menu-Profile-Edit.png differ
diff --git a/images/Menu-Profile-List.png b/images/Menu-Profile-List.png
new file mode 100644
index 00000000..3d4edde4
Binary files /dev/null and b/images/Menu-Profile-List.png differ
diff --git a/images/Menu-Profile-New.png b/images/Menu-Profile-New.png
new file mode 100644
index 00000000..6669da04
Binary files /dev/null and b/images/Menu-Profile-New.png differ
diff --git a/images/Menu-Settings-Menu.png b/images/Menu-Settings-Menu.png
new file mode 100644
index 00000000..e6980c42
Binary files /dev/null and b/images/Menu-Settings-Menu.png differ
diff --git a/images/Menu-Settings-Search.png b/images/Menu-Settings-Search.png
new file mode 100644
index 00000000..79264f6c
Binary files /dev/null and b/images/Menu-Settings-Search.png differ
diff --git a/images/Menu-Settings-app.png b/images/Menu-Settings-app.png
new file mode 100644
index 00000000..a595c138
Binary files /dev/null and b/images/Menu-Settings-app.png differ
diff --git a/images/Menu-Settings.png b/images/Menu-Settings.png
new file mode 100644
index 00000000..ae291825
Binary files /dev/null and b/images/Menu-Settings.png differ
diff --git a/images/Menu.png b/images/Menu.png
new file mode 100644
index 00000000..eed06703
Binary files /dev/null and b/images/Menu.png differ
diff --git a/images/Search.png b/images/Search.png
new file mode 100644
index 00000000..49fa0809
Binary files /dev/null and b/images/Search.png differ
diff --git a/images/TunProxy.png b/images/TunProxy.png
new file mode 100644
index 00000000..03af3790
Binary files /dev/null and b/images/TunProxy.png differ