diff --git a/.gitignore b/.gitignore deleted file mode 100644 index a307a61..0000000 --- a/.gitignore +++ /dev/null @@ -1,37 +0,0 @@ -# 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 diff --git a/Readme-ja.md b/Readme-ja.md new file mode 100644 index 0000000..bf4d697 --- /dev/null +++ b/Readme-ja.md @@ -0,0 +1,141 @@ +Android 通信 Proxy 設定ツール +============= + +Language/[English](https://github.com/raise-isayan/TunProxy/blob/master/Readme.md) + +このツールは、Android の VPNService 機能を利用した Proxy 設定ツールです。 +指定したアプリからの通信のみを取得することが可能です。 + +## 使用方法 + +ユーザ証明書領域に信頼させたい Root CA がない場合はインストールします。 + +TunProxyアプリを起動すると以下の画面が起動します。 + +![Tun Proxy](images/TunProxy.png) + +* Proxy address (ipv4:port) + * 接続先のプロキシサーバを **IPv4アドレス:ポート番号** の形式で指定します。 + IPアドレスはIPv4形式で記載する必要があります。 + +* [Start] ボタン + * 接続を開始します。 +* [Stop] ボタン + * 接続を停止します。 + +## メニュー + +画面上部のメニューアイコン(![Menu](images/Menu.png))からアプリケーションの設定ができます。 + +### Settings + +VPNの接続設定を行います。 + +![Menu Settings](images/Menu-Settings.png) ⇒ ![Menu Settings](images/Menu-Settings-app.png) + +Disallowed Application と Allowed Application の2つのモードがありますが、同時に指定することはできません。 +このためどちらのモードで動作させたいかを選択する必要があります。 +デフォルトは **Disallowed Application** が選択された状態です。 + +* Disallowed Application + * VPN通信から除外したいアプリを選択する。 + 選択したアプリはVPN通信を経由されなくなり、VPNを利用しない場合と同じ挙動となります。 + +* Allowed Application + * VPN通信を行いたいアプリを選択する。 + 選択したアプリはVPN通信を経由するようになります。 + 選択されていないアプリは、VPNを利用しない場合と同じ挙動になります。 + なお、一つも選択されていない場合は、すべてのアプリの通信がVPNを経由します。 + +* Clear all selection + * Allowed / Disallowed Application のすべての選択をクリアします。 + +### Settings 検索 + +![Menu Settings](images/Menu-Settings-Search.png) / ![Menu Settings](images/Menu-Settings-Menu.png) + +画面上部の検索アイコン(![Menu](images/Search.png))から、アプリケーションを絞り込めます。 +アプリケーション名または、パッケージ名に指定したキーワードを含むアプリケーションのみが表示されます。 + +プリケーションリストは、画面上部のメニューアイコン(![Menu](images/Menu.png))からソートできます。 + +### Settings Menu + +アプリ一覧の表示方法を変更します。 + +* show system app + * システムアプリを表示します。 + +#### sort by + +* app name + * アプリケーション名でアプリケーションリストを並べ替えます。 + +* package name + * パッケージ名でアプリケーションリストを並べ替えます。 + +#### order by + +* ascending + * 昇順にソートします + +* descending + * 降順にソートします + +#### filter by + +* app name + * アプリケーション名に指定したキーワードを含むものを検索します。 + +* package name + * パッケージ名に指定したキーワードを含むものを検索します。 + +### 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.owasp.org/index.php/OWASP_Zed_Attack_Proxy_Project + + +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 + +### About +アプリケーションバージョンを表示します。 + +## 動作環境 + +* Android 5.0 (API Level 21) 以降 + +### ビルド + gradlew build + +## 謝辞 + +アプリ作成にあたりコードの大部分は以下のアプリをベースとして作成しました。 + +* forked from MengAndy/tun2http + * https://github.com/MengAndy/tun2http/ + +## 開発環境 + +* JRE(JDK) 1.8以上(Open JDK) +* AndroidStudio 2021.1.1 (https://developer.android.com/studio/index.html) diff --git a/Readme.md b/Readme.md new file mode 100644 index 0000000..a31d8c4 --- /dev/null +++ b/Readme.md @@ -0,0 +1,137 @@ +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. + +![Tun Proxy](images/TunProxy.png) + +* Proxy address (ipv4:port) + * Specify the destination proxy server in the format **IPv4 address:port number**. + The IP address must be described in IPv4 format. + +* [Start] button + * Start the VPN service. +* [Stop] button + * Stop the VPN service. + +## menu + +Application settings can be made from the menu icon (![Menu](images/Menu.png)) at the top of the screen. + +### Settings + +Configure VPN service settings. + +![Menu Settings](images/Menu-Settings.png) ⇒ ![Menu Settings](images/Menu-Settings-app.png) + +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 + +![Menu Settings](images/Menu-Settings-Search.png) / ![Menu Settings](images/Menu-Settings-Menu.png) + +You can narrow down the applications from the search icon.(![Menu](images/Search.png)) +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 (![Menu](images/Menu.png)) 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. + +### 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.owasp.org/index.php/OWASP_Zed_Attack_Proxy_Project + +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 + +### About +Display application version + +## Operating environment + +* Android 5.0 (API Level 21) or later + +### ビルド + 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 2021.1.1 (https://developer.android.com/studio/index.html) diff --git a/android_app/.gitignore b/android_app/.gitignore new file mode 100644 index 0000000..cbc9ec9 --- /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 796b96d..e3b47b6 100644 --- a/android_app/app/.gitignore +++ b/android_app/app/.gitignore @@ -1 +1,3 @@ /build +.cxx +.externalNativeBuild diff --git a/android_app/app/CMakeLists.txt b/android_app/app/CMakeLists.txt index 8f126c2..dd6a6aa 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 46bd3d7..fc60e5f 100644 --- a/android_app/app/build.gradle +++ b/android_app/app/build.gradle @@ -1,38 +1,84 @@ apply plugin: 'com.android.application' android { - compileSdkVersion 26 + compileSdkVersion 31 + buildToolsVersion "31.0.0" + + sourceCompatibility = '1.8' // -source + targetCompatibility = '1.8' // -target + defaultConfig { - applicationId "com.tun2http.app" - minSdkVersion 19 - targetSdkVersion 26 - versionCode 2 - versionName '1.01' + applicationId "tun.proxy" + minSdkVersion 21 + targetSdkVersion 31 + versionCode 100270 + 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 } } + lintOptions { + abortOnError false + } externalNativeBuild { cmake { - path "CMakeLists.txt" + path file('CMakeLists.txt') } } - productFlavors { + + ndkVersion = '22.1.7171670' + + applicationVariants.all { variant -> + if (variant.buildType.name.equals("release")) { + variant.outputs.all { + if (outputFileName != null && outputFileName.endsWith('.apk')) { + def versionName = defaultConfig.versionName + outputFileName = "${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.1.0' + implementation 'androidx.appcompat:appcompat:1.0.2' + implementation 'androidx.constraintlayout:constraintlayout:1.1.3' + implementation 'androidx.vectordrawable:vectordrawable:1.0.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.0.0' + implementation 'androidx.constraintlayout:constraintlayout:1.1.3' + implementation 'androidx.navigation:navigation-fragment:2.0.0' + implementation 'androidx.navigation:navigation-ui:2.0.0' + testImplementation 'junit:junit:4.12' + androidTestImplementation 'androidx.test.ext:junit:1.1.1' + androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0' } 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 9de396e..0000000 --- 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 0000000..676027b --- /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.Util; + +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 = Util.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/ProgressTaskTest.java b/android_app/app/src/androidTest/java/tun/proxy/ProgressTaskTest.java new file mode 100644 index 0000000..2bc4d4d --- /dev/null +++ b/android_app/app/src/androidTest/java/tun/proxy/ProgressTaskTest.java @@ -0,0 +1,66 @@ +package tun.proxy; + +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; +import tun.utils.Util; + +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) { + e.printStackTrace(); + } + } + return null; + } + + }; + + Assert.assertEquals(task.getStatus(), ProgressTask.Status.PENDING); + Log.w(TAG, "progressTask: execute"); + task.execute(); + Assert.assertEquals(task.getStatus(), ProgressTask.Status.RUNNING); + + Log.w(TAG, "progressTask: end"); + + } +} diff --git a/android_app/app/src/main/AndroidManifest.xml b/android_app/app/src/main/AndroidManifest.xml index dbef8e4..2127f48 100644 --- a/android_app/app/src/main/AndroidManifest.xml +++ b/android_app/app/src/main/AndroidManifest.xml @@ -1,40 +1,61 @@ - - - - + package="tun.proxy"> + + + + + + + + - - + - - - + + + + + + + android:permission="android.permission.BIND_VPN_SERVICE" + android:exported="true"> - + + + android:label="@string/app_name" + android:exported="true"> + \ No newline at end of file diff --git a/android_app/app/src/main/cpp/http.c b/android_app/app/src/main/cpp/http.c index 08111d7..5c028ba 100644 --- a/android_app/app/src/main/cpp/http.c +++ b/android_app/app/src/main/cpp/http.c @@ -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 @@ -139,20 +137,42 @@ uint8_t *patch_http_url(uint8_t *data, size_t *data_len) { //GET POST PUT DELETE HEAD OPTIONS PATCH char *word; uint8_t *pos = 0; - if (pos = find_data(data, *data_len, "GET ")) { + if ((pos = find_data(data, *data_len, "GET ")) > 0) { word = "GET "; - } else if (pos = find_data(data, *data_len, "POST ")) { + } else if ((pos = find_data(data, *data_len, "POST ")) > 0) { word = "POST "; - } else if (pos = find_data(data, *data_len, "PUT ")) { + } else if ((pos = find_data(data, *data_len, "PUT ")) > 0) { word = "PUT "; - } else if (pos = find_data(data, *data_len, "DELETE ")) { + } else if ((pos = find_data(data, *data_len, "DELETE ")) > 0) { word = "DELETE "; - } else if (pos = find_data(data, *data_len, "HEAD ")) { + } else if ((pos = find_data(data, *data_len, "HEAD ")) > 0) { word = "HEAD "; - } else if (pos = find_data(data, *data_len, "OPTIONS ")) { + } else if ((pos = find_data(data, *data_len, "OPTIONS ")) > 0) { word = "OPTIONS "; - } else if (pos = find_data(data, *data_len, "PATCH ")) { + } else if ((pos = find_data(data, *data_len, "PATCH ")) > 0) { word = "PATCH "; + } else if ((pos = find_data(data, *data_len, "HEAD ")) > 0) { + word = "HEAD "; + } else if ((pos = find_data(data, *data_len, "TRACE ")) > 0) { + word = "TRACE "; + } else if ((pos = find_data(data, *data_len, "PROPFIND ")) > 0) { + word = "PROPFIND "; + } else if ((pos = find_data(data, *data_len, "PROPPATCH ")) > 0) { + word = "PROPPATCH "; + } else if ((pos = find_data(data, *data_len, "MKCOL ")) > 0) { + word = "MKCOL "; + } else if ((pos = find_data(data, *data_len, "COPY ")) > 0) { + word = "COPY "; + } else if ((pos = find_data(data, *data_len, "MOVE ")) > 0) { + word = "MOVE "; + } else if ((pos = find_data(data, *data_len, "LOCK ")) > 0) { + word = "LOCK "; + } else if ((pos = find_data(data, *data_len, "UNLOCK ")) > 0) { + word = "UNLOCK "; + } else if ((pos = find_data(data, *data_len, "LINK ")) > 0) { + word = "LINK "; + } else if ((pos = find_data(data, *data_len, "UNLINK "))> 0) { + word = "UNLINK "; } if (!pos) { diff --git a/android_app/app/src/main/cpp/http.h b/android_app/app/src/main/cpp/http.h index 94789ca..b940966 100644 --- a/android_app/app/src/main/cpp/http.h +++ b/android_app/app/src/main/cpp/http.h @@ -4,6 +4,10 @@ #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/tcp.c b/android_app/app/src/main/cpp/tcp.c index c7b7837..4930fd5 100644 --- a/android_app/app/src/main/cpp/tcp.c +++ b/android_app/app/src/main/cpp/tcp.c @@ -265,7 +265,7 @@ void check_tcp_socket(const struct arguments *args, if (s->tcp.state == TCP_LISTEN) { // Check socket connect if (ev->events & EPOLLIN) { - uint8_t buffer[512]; + char 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", @@ -274,14 +274,14 @@ void check_tcp_socket(const struct arguments *args, } 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) { s->tcp.connect_sent = TCP_CONNECT_ESTABLISHED; while (recv(s->socket, buffer, sizeof(buffer), 0) > 0) {} s->tcp.state = TCP_SYN_RECV; } else { write_rst(args, &s->tcp); + } if (strcmp(buffer, "HTTP/1.0 200") == 0 || strcmp(buffer, "HTTP/1.1 200") == 0) { + } - } } } else { s->tcp.remote_seq++; // remote SYN @@ -504,8 +504,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; @@ -591,7 +589,7 @@ 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); @@ -731,8 +729,8 @@ jboolean handle_tcp(const struct arguments *args, } 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); + 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) { @@ -956,6 +954,7 @@ void queue_tcp(const struct arguments *args, free(s->data); s->data = malloc(datalen); memcpy(s->data, data, datalen); + s->len = datalen; } else log_android(ANDROID_LOG_ERROR, "%s segment larger %u..%u < %u", session, diff --git a/android_app/app/src/main/cpp/tun2http.c b/android_app/app/src/main/cpp/tun2http.c index 9c50d5e..4c8d701 100644 --- a/android_app/app/src/main/cpp/tun2http.c +++ b/android_app/app/src/main/cpp/tun2http.c @@ -48,7 +48,7 @@ void JNI_OnUnload(JavaVM *vm, void *reserved) { // 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; @@ -72,7 +72,7 @@ Java_com_tun2http_app_service_Tun2HttpVpnService_jni_1init(JNIEnv *env, jobject } JNIEXPORT void JNICALL -Java_com_tun2http_app_service_Tun2HttpVpnService_jni_1start( +Java_tun_proxy_service_Tun2HttpVpnService_jni_1start( JNIEnv *env, jobject instance, jint tun, jboolean fwd53, jint rcode, jstring proxyIp, jint proxyPort) { const char *proxy_ip = (*env)->GetStringUTFChars(env, proxyIp, 0); @@ -117,7 +117,7 @@ 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); @@ -140,13 +140,13 @@ Java_com_tun2http_app_service_Tun2HttpVpnService_jni_1stop( } 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(); @@ -162,7 +162,7 @@ Java_com_tun2http_app_service_Tun2HttpVpnService_jni_1done(JNIEnv *env, jobject // JNI Util JNIEXPORT jstring JNICALL -Java_com_tun2http_app_utils_Util_jni_1getprop(JNIEnv *env, jclass type, jstring name_) { +Java_tun_utils_Util_jni_1getprop(JNIEnv *env, jclass type, jstring name_) { const char *name = (*env)->GetStringUTFChars(env, name_, 0); char value[PROP_VALUE_MAX + 1] = ""; diff --git a/android_app/app/src/main/cpp/tun2http.h b/android_app/app/src/main/cpp/tun2http.h index 129b12e..0fd3342 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 diff --git a/android_app/app/src/main/java/com/tun2http/app/MainActivity.java b/android_app/app/src/main/java/tun/proxy/MainActivity.java similarity index 51% rename from android_app/app/src/main/java/com/tun2http/app/MainActivity.java rename to android_app/app/src/main/java/tun/proxy/MainActivity.java index f7baf39..f06ae0f 100644 --- a/android_app/app/src/main/java/com/tun2http/app/MainActivity.java +++ b/android_app/app/src/main/java/tun/proxy/MainActivity.java @@ -1,29 +1,45 @@ -package com.tun2http.app; +package tun.proxy; -import android.app.Activity; +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.net.VpnService; +import android.content.pm.PackageManager; + +import androidx.appcompat.app.AppCompatActivity; +import androidx.appcompat.widget.Toolbar; +import androidx.fragment.app.Fragment; +import androidx.preference.Preference; +import androidx.preference.PreferenceFragmentCompat; +import androidx.preference.PreferenceManager; + import android.os.Handler; import android.os.IBinder; -import android.preference.PreferenceManager; -import android.os.Bundle; +import android.os.Looper; import android.text.TextUtils; import android.view.View; +import android.view.Menu; +import android.view.MenuItem; import android.widget.Button; import android.widget.EditText; -import com.tun2http.app.service.Tun2HttpVpnService; +import tun.proxy.service.Tun2HttpVpnService; +import tun.utils.IPUtil; + +public class MainActivity extends AppCompatActivity implements + PreferenceFragmentCompat.OnPreferenceStartFragmentCallback { + public static final int REQUEST_VPN = 1; + public static final int REQUEST_CERT = 2; -public class MainActivity extends Activity { Button start; Button stop; EditText hostEditText; - - Handler statusHandler = new Handler(); + Handler statusHandler = new Handler(Looper.getMainLooper()); private Tun2HttpVpnService service; @@ -31,6 +47,8 @@ public class MainActivity extends Activity { protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); + Toolbar toolbar = findViewById(R.id.toolbar); + setSupportActionBar(toolbar); start = findViewById(R.id.start); stop = findViewById(R.id.stop); @@ -48,14 +66,74 @@ public void onClick(View v) { stopVpn(); } }); - - start.setEnabled(true); stop.setEnabled(false); loadHostPort(); + + } + @Override + public boolean onPreferenceStartFragment(PreferenceFragmentCompat caller, Preference pref) { + final Bundle args = pref.getExtras(); + final Fragment fragment = getSupportFragmentManager().getFragmentFactory().instantiate(getClassLoader(), pref.getFragment()); + fragment.setArguments(args); + fragment.setTargetFragment(caller, 0); + getSupportFragmentManager().beginTransaction() + .replace(R.id.activity_settings, fragment) + .addToBackStack(null) + .commit(); + setTitle(pref.getTitle()); + return true; + } + + @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 item = menu.findItem(R.id.action_activity_settings); + item.setEnabled(start.isEnabled()); + return true; + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + // Handle action bar item clicks here. The action bar will + // automatically handle clicks on the Home/Up button, so long + // as you specify a parent activity in AndroidManifest.xml. + switch (item.getItemId()) { + case R.id.action_activity_settings: + Intent intent = new android.content.Intent(this, SettingsActivity.class); + startActivity(intent); + break; + case R.id.action_show_about: + new AlertDialog.Builder(this) + .setTitle(getString(R.string.app_name) + getVersionName()) + .setMessage(R.string.app_name) + .show(); + break; + default: + 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; + } + } private ServiceConnection serviceConnection = new ServiceConnection() { public void onServiceConnected(ComponentName className, IBinder binder) { @@ -71,12 +149,11 @@ public void onServiceDisconnected(ComponentName className) { @Override protected void onResume() { super.onResume(); - start.setEnabled(false); stop.setEnabled(false); updateStatus(); - statusHandler.postDelayed(statusRunnable, 1000); + statusHandler.post(statusRunnable); Intent intent = new Intent(this, Tun2HttpVpnService.class); bindService(intent, serviceConnection, Context.BIND_AUTO_CREATE); @@ -89,8 +166,8 @@ boolean isRunning() { Runnable statusRunnable = new Runnable() { @Override public void run() { - updateStatus(); - statusHandler.postDelayed(statusRunnable, 1000); + updateStatus(); + statusHandler.post(statusRunnable); } }; @@ -98,7 +175,6 @@ public void run() { protected void onPause() { super.onPause(); statusHandler.removeCallbacks(statusRunnable); - unbindService(serviceConnection); } @@ -108,9 +184,11 @@ void updateStatus() { } if (isRunning()) { start.setEnabled(false); + hostEditText.setEnabled(false); stop.setEnabled(true); } else { start.setEnabled(true); + hostEditText.setEnabled(true); stop.setEnabled(false); } } @@ -118,25 +196,25 @@ void updateStatus() { 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); + startActivityForResult(i, REQUEST_VPN); } else { - onActivityResult(0, Activity.RESULT_OK, null); + onActivityResult(REQUEST_VPN, RESULT_OK, null); } } @Override protected void onActivityResult(int requestCode, int resultCode, Intent data) { super.onActivityResult(requestCode, resultCode, data); - - if (resultCode == Activity.RESULT_OK && parseAndSaveHostPort()) { + if (resultCode != RESULT_OK) { + return; + } + if (requestCode == REQUEST_VPN && parseAndSaveHostPort()) { start.setEnabled(false); stop.setEnabled(true); Tun2HttpVpnService.start(this); @@ -144,51 +222,39 @@ protected void onActivityResult(int requestCode, int resultCode, Intent data) { } private void loadHostPort() { - SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this); - String proxyHost = prefs.getString(Tun2HttpVpnService.PREF_PROXY_HOST, ""); + final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this); + final String proxyHost = prefs.getString(Tun2HttpVpnService.PREF_PROXY_HOST, ""); int proxyPort = prefs.getInt(Tun2HttpVpnService.PREF_PROXY_PORT, 0); - if(TextUtils.isEmpty(proxyHost)) { + if (TextUtils.isEmpty(proxyHost)) { return; } - - if(proxyPort == 80) { - hostEditText.setText(proxyHost); - } else { - hostEditText.setText(proxyHost + ":" + proxyPort); - } + hostEditText.setText(proxyHost + ":" + proxyPort); } private boolean parseAndSaveHostPort() { String hostPort = hostEditText.getText().toString(); - if (hostPort.isEmpty()) { + if (!IPUtil.isValidIPv4Address(hostPort)) { 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) { + } catch (NumberFormatException 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; } -} +} \ 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 0000000..1f67c79 --- /dev/null +++ b/android_app/app/src/main/java/tun/proxy/MyApplication.java @@ -0,0 +1,57 @@ +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 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; + } + + public enum VPNMode {DISALLOW, ALLOW}; + public enum AppSortBy {APPNAME, PKGNAME}; + public enum AppOrderBy {ASC, DESC}; + public enum AppFiltertBy {APPNAME, PKGNAME}; + + 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(); + return; + } + + 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(); + return; + } + +} 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 0000000..d0011de --- /dev/null +++ b/android_app/app/src/main/java/tun/proxy/SettingsActivity.java @@ -0,0 +1,684 @@ +package tun.proxy; + +import android.annotation.TargetApi; +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.os.Build; +import android.os.Bundle; +import androidx.appcompat.app.ActionBar; +import androidx.appcompat.app.AlertDialog; +import androidx.appcompat.app.AppCompatActivity; +import androidx.fragment.app.FragmentManager; +import androidx.preference.*; + +import android.util.Log; +import android.view.Menu; +import android.view.MenuInflater; +import android.view.MenuItem; +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.Map; +import java.util.Set; + +import tun.utils.ProgressTask; + +public class SettingsActivity extends AppCompatActivity { + private static final String TAG = "SettingsActivity"; + private static final String TITLE_TAG = "Settings"; + + 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.replaceAll("\"", ""))); + } + return filterType; + } + + }; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_settings); + 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(Bundle outState) { + super.onSaveInstanceState(outState); + outState.putCharSequence(TITLE_TAG, getTitle()); + } + + @Override + public boolean onSupportNavigateUp() { + if (getSupportFragmentManager().popBackStackImmediate()) { + return true; + } + return super.onSupportNavigateUp(); + } + + /** + * Inner Classes. + */ + + public static class SettingsFragment extends PreferenceFragmentCompat implements Preference.OnPreferenceClickListener { + 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_clear_all_selection"; + + @Override + public void onCreatePreferences(Bundle savedInstanceState, String rootKey) { + addPreferencesFromResource(R.xml.preferences); + setHasOptionsMenu(true); + + /* Allowed / Disallowed Application */ + final ListPreference prefPackage = (ListPreference) this.findPreference(VPN_CONNECTION_MODE); + final PreferenceScreen prefDisallow = (PreferenceScreen) findPreference(VPN_DISALLOWED_APPLICATION_LIST); + final PreferenceScreen prefAllow = (PreferenceScreen) findPreference(VPN_ALLOWED_APPLICATION_LIST); + final PreferenceScreen clearAllSelection = (PreferenceScreen) findPreference(VPN_CLEAR_ALL_SELECTION); + clearAllSelection.setOnPreferenceClickListener(this); + + prefPackage.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]; + MyApplication.getInstance().storeVPNMode(mode); + } + return true; + } + }); + prefPackage.setSummary(prefPackage.getEntry()); + prefDisallow.setEnabled(MyApplication.VPNMode.DISALLOW.name().equals(prefPackage.getValue())); + prefAllow.setEnabled(MyApplication.VPNMode.ALLOW.name().equals(prefPackage.getValue())); + + updateMenuItem(); + } + + private void updateMenuItem() { + final PreferenceScreen prefDisallow = (PreferenceScreen) findPreference(VPN_DISALLOWED_APPLICATION_LIST); + final PreferenceScreen prefAllow = (PreferenceScreen) findPreference(VPN_ALLOWED_APPLICATION_LIST); + + int countDisallow = MyApplication.getInstance().loadVPNApplication(MyApplication.VPNMode.DISALLOW).size(); + int countAllow = MyApplication.getInstance().loadVPNApplication(MyApplication.VPNMode.ALLOW).size(); + prefDisallow.setTitle(getString(R.string.pref_header_disallowed_application_list) + String.format(" (%d)", countDisallow)); + prefAllow.setTitle(getString(R.string.pref_header_allowed_application_list) + String.format(" (%d)", countAllow)); + } + + /* + * https://developer.android.com/guide/topics/ui/settings/organize-your-settings + */ + + // リスナー部分 + @Override + public boolean onPreferenceClick(Preference preference) { + // keyを見てクリックされたPreferenceを特定 + switch (preference.getKey()) { + case VPN_DISALLOWED_APPLICATION_LIST: + case VPN_ALLOWED_APPLICATION_LIST: + break; + case VPN_CLEAR_ALL_SELECTION: + new AlertDialog.Builder(getActivity()) + .setTitle(getString(R.string.title_activity_settings)) + .setMessage(getString(R.string.pref_dialog_clear_all_application_msg)) + .setPositiveButton("OK", new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + Set set = new HashSet<>(); + MyApplication.getInstance().storeVPNApplication(MyApplication.VPNMode.ALLOW, set); + MyApplication.getInstance().storeVPNApplication(MyApplication.VPNMode.DISALLOW, set); + updateMenuItem(); + } + }) + .setNegativeButton("Cancel", null) + .show(); + break; + } + return false; + } + + } + + @TargetApi(Build.VERSION_CODES.HONEYCOMB) + public static class DisallowedPackageListFragment extends PackageListFragment { + public DisallowedPackageListFragment() { + super(MyApplication.VPNMode.DISALLOW); + } + } + + @TargetApi(Build.VERSION_CODES.HONEYCOMB) + public static class AllowedPackageListFragment extends PackageListFragment { + public AllowedPackageListFragment() { + super(MyApplication.VPNMode.ALLOW); + } + } + + @TargetApi(Build.VERSION_CODES.HONEYCOMB) + protected static class PackageListFragment extends PreferenceFragmentCompat + implements SearchView.OnQueryTextListener, SearchView.OnCloseListener { + private final Map mAllPackageInfoMap = new HashMap<>(); + 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 AsyncTaskProgress task; + + private 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; + + public PackageListFragment(MyApplication.VPNMode mode) { + super(); + this.mode = mode; + this.task = new AsyncTaskProgress(this); + } + + @Override + public void onCreatePreferences(Bundle savedInstanceState, String rootKey) { + setHasOptionsMenu(true); + mFilterPreferenceScreen = getPreferenceManager().createPreferenceScreen(getActivity()); + setPreferenceScreen(mFilterPreferenceScreen); + } + + @Override + public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { + super.onCreateOptionsMenu(menu, inflater); + // Menuの設定 + inflater.inflate(R.menu.menu_search, menu); + //MenuCompat.setGroupDividerEnabled(menu, true); + + final MenuItem menuSearch = menu.findItem(R.id.menu_search_item); + this.searchView = (SearchView) menuSearch.getActionView(); + this.searchView.setOnQueryTextListener(this); + this.searchView.setOnCloseListener(this); + this.searchView.setSubmitButtonEnabled(false); + + final MenuItem menuShowSystemApp = menu.findItem(R.id.menu_filter_app_system); + menuShowSystemApp.setChecked(this.filterAppType.contains(FilterAppType.SYSTEM_APP)); + + switch (this.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 (this.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 (this.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; + } + } + } + + private String searchFilter = ""; + private SearchView searchView; + + 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; + } + + Set selected = this.getAllSelectedPackageSet(); + storeSelectedPackageSet(selected); + SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(MyApplication.getInstance().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(); + Set loadMap = MyApplication.getInstance().loadVPNApplication(this.mode); + for (String pkgName : loadMap) { + this.mAllPackageInfoMap.put(pkgName, loadMap.contains(pkgName)); + } + SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(MyApplication.getInstance().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(getActivity()); + 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) { + MyApplication.getInstance().storeVPNMode(this.mode); + MyApplication.getInstance().storeVPNApplication(this.mode, set); + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + int id = item.getItemId(); + switch (id) { + case android.R.id.home: + startActivity(new Intent(getActivity(), SettingsActivity.class)); + return true; + case R.id.menu_filter_app_system: + item.setChecked(!item.isChecked()); + if (item.isChecked()) { + this.filterAppType.add(FilterAppType.SYSTEM_APP); + } + else { + this.filterAppType.remove(FilterAppType.SYSTEM_APP); + } + filter(null, appFilterBy, MyApplication.AppOrderBy.ASC, appSortBy, this.filterAppType); + break; + case R.id.menu_sort_order_asc: + item.setChecked(!item.isChecked()); + filter(null, appFilterBy, MyApplication.AppOrderBy.ASC, appSortBy, this.filterAppType); + break; + case R.id.menu_sort_order_desc: + item.setChecked(!item.isChecked()); + filter(null, appFilterBy, MyApplication.AppOrderBy.DESC, appSortBy, this.filterAppType); + break; + case R.id.menu_filter_app_name: + item.setChecked(!item.isChecked()); + this.appFilterBy = MyApplication.AppSortBy.APPNAME; + //filter(null, MyApplication.AppSortBy.APPNAME, appOrderBy, appSortBy); + break; + case R.id.menu_filter_pkg_name: + item.setChecked(!item.isChecked()); + this.appFilterBy = MyApplication.AppSortBy.PKGNAME; + //filter(null, MyApplication.AppSortBy.PKGNAME, appOrderBy, appSortBy); + break; + case R.id.menu_sort_app_name: + item.setChecked(!item.isChecked()); + filter(null, appFilterBy, appOrderBy, MyApplication.AppSortBy.APPNAME, this.filterAppType); + break; + case R.id.menu_sort_pkg_name: + item.setChecked(!item.isChecked()); + filter(null, appFilterBy, appOrderBy, MyApplication.AppSortBy.PKGNAME, this.filterAppType); + break; + } + return super.onOptionsItemSelected(item); + } + + @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 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) { + if (isCancelled()) continue; + // exclude self package + if (pi.packageName.equals(MyApplication.getInstance().getPackageName())) { + continue; + } + // exclude system app + if (!filterAppType.contains(FilterAppType.SYSTEM_APP)) { + 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 Context context = MyApplication.getInstance().getApplicationContext(); + final PackageManager pm = context.getPackageManager(); + packageFragment.mFilterPreferenceScreen.removeAll(); + for (final PackageInfo pi : installedPackages) { + // exclude self package + if (pi.packageName.equals(MyApplication.getInstance().getPackageName())) { + continue; + } + // exclude system app + if (!packageFragment.filterAppType.contains(FilterAppType.SYSTEM_APP)) { + 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().contains(t2.toLowerCase())) { + final Preference preference = packageFragment.buildPackagePreferences(pm, pi); + packageFragment.mFilterPreferenceScreen.addPreference(preference); + } + } + return; + } + + @Override + protected void onCancelled() { + super.onCancelled(); + packageFragment.mAllPackageInfoMap.clear(); + packageFragment.mFilterPreferenceScreen.removeAll(); + return; + } + + } + + 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/com/tun2http/app/receiver/BootReceiver.java b/android_app/app/src/main/java/tun/proxy/receiver/BootReceiver.java similarity index 52% rename from android_app/app/src/main/java/com/tun2http/app/receiver/BootReceiver.java rename to android_app/app/src/main/java/tun/proxy/receiver/BootReceiver.java index 57aef69..b6c2c81 100644 --- a/android_app/app/src/main/java/com/tun2http/app/receiver/BootReceiver.java +++ b/android_app/app/src/main/java/tun/proxy/receiver/BootReceiver.java @@ -1,33 +1,33 @@ -package com.tun2http.app.receiver; +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 android.preference.PreferenceManager; +import androidx.preference.PreferenceManager; import android.util.Log; +import tun.proxy.R; -import com.tun2http.app.service.Tun2HttpVpnService; - +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())) { + if (intent != null && !Intent.ACTION_BOOT_COMPLETED.equals(intent.getAction())) { return; } - SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context); + final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context); boolean isRunning = prefs.getBoolean(Tun2HttpVpnService.PREF_RUNNING, false); - if(isRunning) { + if (isRunning) { Intent prepare = VpnService.prepare(context); - if(prepare == null) { - Log.d("Tun2Http.Boot", "Starting vpn"); + if (prepare == null) { + Log.d(context.getString(R.string.app_name) + ".Boot", "Starting vpn"); Tun2HttpVpnService.start(context); } else { - Log.d("Tun2Http.Boot", "Not prepared"); + Log.d(context.getString(R.string.app_name) + ".Boot", "Not prepared"); } } } 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 68% 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 ed888ff..107e20e 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,12 +1,9 @@ -package com.tun2http.app.service; +package tun.proxy.service; -import android.annotation.TargetApi; 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.VpnService; import android.os.Binder; import android.os.Build; @@ -15,46 +12,57 @@ import android.os.ParcelFileDescriptor; import android.os.PowerManager; import android.os.RemoteException; -import android.preference.PreferenceManager; +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 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.Util; 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"; + private static final String TAG = "Tun2Http.Service"; + 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(); @@ -64,40 +72,13 @@ public class Tun2HttpVpnService extends VpnService { 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; } @@ -122,7 +103,6 @@ private void stop() { stopForeground(true); } - @Override public void onRevoke() { Log.i(TAG, "Revoke"); @@ -158,20 +138,36 @@ private Builder getBuilder() { builder.addAddress(vpn6, 128); builder.addRoute("0.0.0.0", 0); - builder.addRoute("0:0:0:0:0:0:0:0", 0); + List dnsList = Util.getDefaultDNS(MyApplication.getInstance().getApplicationContext()); + for (String dns : dnsList) { + Log.i(TAG, "default DNS:" + dns); + builder.addDnsServer(dns); + } + // MTU int mtu = jni_get_mtu(); Log.i(TAG, "MTU=" + mtu); builder.setMtu(mtu); - // Add list of allowed applications + // AAdd list of allowed and disallowed applications if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { - try { - builder.addAllowedApplication("com.android.chrome"); - } catch (PackageManager.NameNotFoundException e) { - e.printStackTrace(); + MyApplication app = (MyApplication) this.getApplication(); + 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); + MyApplication.getInstance().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); + MyApplication.getInstance().storeVPNApplication(MyApplication.VPNMode.ALLOW, allow); } } @@ -179,7 +175,6 @@ private Builder getBuilder() { return builder; } - private void startNative(ParcelFileDescriptor vpn) { SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this); String proxyHost = prefs.getString(PREF_PROXY_HOST, ""); @@ -205,7 +200,6 @@ private void stopNative(ParcelFileDescriptor vpn) { prefs.edit().putBoolean(PREF_RUNNING, false).apply(); } - private void stopVPN(ParcelFileDescriptor pfd) { Log.i(TAG, "Stopping"); try { @@ -229,7 +223,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,13 +230,10 @@ private boolean isSupported(int protocol) { protocol == 17 /* UDP */); } - @Override public void onCreate() { - // Native init jni_init(); - super.onCreate(); } @@ -265,7 +255,6 @@ public int onStartCommand(Intent intent, int flags, int startId) { return START_STICKY; } - @Override public void onDestroy() { Log.i(TAG, "Destroy"); @@ -285,17 +274,31 @@ 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 List listDns = new ArrayList<>(); private Builder() { super(); - ConnectivityManager cm = (ConnectivityManager) getSystemService(Context.CONNECTIVITY_SERVICE); - networkInfo = cm.getActiveNetworkInfo(); } @Override @@ -320,12 +323,54 @@ public Builder addRoute(String address, int prefixLength) { } @Override - public Builder addDnsServer(InetAddress address) { - listDns.add(address); + public Builder addDnsServer(InetAddress address) { + listDns.add(address.getHostAddress()); + super.addDnsServer(address); + return this; + } + + @Override + public Builder addDnsServer(String address) { +// listDns.add(address); super.addDnsServer(address); return this; } + // min sdk 26 + public Builder 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); + } + } + return this; + } + + public Builder addDisallowedApplication(final List packageList) throws PackageManager.NameNotFoundException { + // + for (String pkg : packageList) { + Log.i(TAG, "disallowed:" + pkg); + addDisallowedApplication(pkg); + } + return this; + } + + public Builder 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); + } + } + return this; + } + @Override public boolean equals(Object obj) { Builder other = (Builder) obj; @@ -333,10 +378,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,25 +398,31 @@ 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; return true; } - } - - public static void start(Context context) { - Intent intent = new Intent(context, Tun2HttpVpnService.class); - intent.setAction(ACTION_START); - context.startService(intent); +// public boolean isNetworkConnected() { +// final ConnectivityManager cm = (ConnectivityManager)getSystemService(Context.CONNECTIVITY_SERVICE); +// if (cm != null) { +// if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) { +// final android.net.NetworkInfo ni = cm.getActiveNetworkInfo(); +// if (ni != null) { +// return (ni.isConnected() && (ni.getType() == ConnectivityManager.TYPE_WIFI || ni.getType() == ConnectivityManager.TYPE_MOBILE)); +// } +// } else { +// final Network n = cm.getActiveNetwork(); +// if (n != null) { +// final NetworkCapabilities nc = cm.getNetworkCapabilities(n); +// return (nc.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR) || nc.hasTransport(NetworkCapabilities.TRANSPORT_WIFI)); +// } +// } +// } +// return false; +// } } - - 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 0000000..4f6b068 --- /dev/null +++ b/android_app/app/src/main/java/tun/utils/CertificateUtil.java @@ -0,0 +1,207 @@ +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"; + + public enum CertificateInstallType {SYSTEM, USER}; + + 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 = (String) aliases.nextElement(); + rootCACert = (X509Certificate) ks.getCertificate(alias); + if (rootCACert.getIssuerDN().getName().contains(caName)) { + found = true; + break; + } + } + } catch (IOException e) { + Log.e(TAG, e.getMessage(), e); + } catch (KeyStoreException e) { + Log.e(TAG, e.getMessage(), e); + } catch (NoSuchAlgorithmException e) { + Log.e(TAG, e.getMessage(), e); + } catch (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 = (String) aliases.nextElement(); + X509Certificate cert = (X509Certificate) ks.getCertificate(alias); + System.out.println(alias + "/" + cert.getIssuerX500Principal().getName()); + rootCAList.add(cert); + } + } catch (IOException e) { + Log.e(TAG, e.getMessage(), e); + } catch (KeyStoreException e) { + Log.e(TAG, e.getMessage(), e); + } catch (NoSuchAlgorithmException e) { + Log.e(TAG, e.getMessage(), e); + } catch (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 = (String) 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 e) { + Log.e(TAG, e.getMessage(), e); + } catch (KeyStoreException e) { + Log.e(TAG, e.getMessage(), e); + } catch (NoSuchAlgorithmException e) { + Log.e(TAG, e.getMessage(), e); + } catch (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 (FileNotFoundException e) { + Log.e(TAG, e.getMessage(), e); + } catch (IOException e) { + Log.e(TAG, e.getMessage(), e); + } catch (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; + } + +} diff --git a/android_app/app/src/main/java/com/tun2http/app/utils/IPUtil.java b/android_app/app/src/main/java/tun/utils/IPUtil.java similarity index 77% rename from android_app/app/src/main/java/com/tun2http/app/utils/IPUtil.java rename to android_app/app/src/main/java/tun/utils/IPUtil.java index 7ddf61f..cdc4d4a 100644 --- a/android_app/app/src/main/java/com/tun2http/app/utils/IPUtil.java +++ b/android_app/app/src/main/java/tun/utils/IPUtil.java @@ -1,5 +1,4 @@ -package com.tun2http.app.utils; - +package tun.utils; import android.util.Log; @@ -11,6 +10,42 @@ public class IPUtil { private static final String TAG = "Tun2Http.IPUtil"; + public static boolean isValidIPv4Address(String address) { + if (address.isEmpty()) { + return false; + } + String parts[] = address.split(":"); + int port = 0; + if (parts.length > 1) { + try { + port = Integer.parseInt(parts[1]); + } catch (NumberFormatException e) { + return false; + } + if (!(0 < port && port < 65536)) { + return false; + } + } + String[] ipParts = parts[0].split("\\."); + 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 List toCIDR(String start, String end) throws UnknownHostException { return toCIDR(InetAddress.getByName(start), InetAddress.getByName(end)); } 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 0000000..abd79f3 --- /dev/null +++ b/android_app/app/src/main/java/tun/utils/ProgressTask.java @@ -0,0 +1,88 @@ +package tun.utils; + +import android.os.Handler; +import android.os.Looper; + +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; + +public abstract class ProgressTask { + private volatile Status mStatus = Status.PENDING; + private boolean canceled = false; + + public final ProgressTask.Status getStatus() { + return mStatus; + } + + private class ProgressRunnable implements Runnable { + + final Params [] params; + + @SafeVarargs + public ProgressRunnable(Params... params) { + this.params = params; + } + + private Result result; + Handler handler = new Handler(Looper.getMainLooper()); + + @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) { + ex.printStackTrace(); + } + handler.post(new Runnable() { + @Override + public void run() { + if (!canceled) { + onPostExecute(result); + mStatus = Status.FINISHED; + } else { + onCancelled(); + } + } + }); + } + } + + public void execute(Params... params) { + ExecutorService executorService = Executors.newSingleThreadExecutor(); + executorService.submit(new ProgressRunnable(params)); + } + + protected void onPreExecute() { + } + + 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 } +} \ No newline at end of file diff --git a/android_app/app/src/main/java/com/tun2http/app/utils/Util.java b/android_app/app/src/main/java/tun/utils/Util.java similarity index 98% rename from android_app/app/src/main/java/com/tun2http/app/utils/Util.java rename to android_app/app/src/main/java/tun/utils/Util.java index ffe3b8d..d595a34 100644 --- a/android_app/app/src/main/java/com/tun2http/app/utils/Util.java +++ b/android_app/app/src/main/java/tun/utils/Util.java @@ -1,4 +1,4 @@ -package com.tun2http.app.utils; +package tun.utils; /* This file is part of NetGuard. 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 c3903ed..2b068d1 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 5713f34..07d5da9 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/layout/activity_main.xml b/android_app/app/src/main/res/layout/activity_main.xml index 8ed0792..9ea1c52 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,26 @@ - + tools:context=".MainActivity"> - + android:theme="@style/AppTheme.AppBarOverlay"> -