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アプリを起動すると以下の画面が起動します。 + +![Tun Proxy](images/TunProxy.png) + +* Profile 選択 + * 選択したプロファイルを適用します。 + * Manual + * 手動で設定します。 + +* Proxy address (host:port) + * 接続先のプロキシサーバを ** host:port ** の形式で指定します。 + IPアドレスの場合はIPv4形式で記載する必要があります。 + +* Proxy Type + * HTTP + HTTPプロキシとして接続します。 + * SOCKS5 + SOCKS5プロキシとして接続します。 + +* [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** が選択された状態です。 + +## VPN connection setting + +* 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 + * パッケージ名に指定したキーワードを含むものを検索します。 + +## Proxy setting + 有効な場合、開始する前にプロキシの接続状態を確認します。 + +## DNS setting + +DNSサーバの設定を行います。 + +* Use Custom DNS + * 有効な場合カスタムDNSサーバを指定します。 + +* Primary DNS + * プライマリDNSを指定します。 + +* Secondary DNS + * セカンダリDNSを指定します。 + +## Profile Settings + +### Profile Lis + +プロファイルの一覧が表示されます。 + +![Profile New](images/Menu-Profile-List.png) + +### Profile New + +プロファイルを新規作成します。 + +![Profile New](images/Menu-Profile-New.png) + +### Profile Edit + +プロファイルを編集します。 + +![Profile New](images/Menu-Profile-Edit.png) + +### Profile Delete + +プロファイルを削除します。 + +![Profile New](images/Menu-Profile-Delete.png) + +## 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. + +![Tun Proxy](images/TunProxy.png) + +* 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 (![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) + +## 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 + +![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. + +## 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](images/Menu-Profile-List.png) + +### Profile New + +Create a new profile. + +![Profile New](images/Menu-Profile-New.png) + +### Profile Edit + +Edit Profile + +![Profile New](images/Menu-Profile-Edit.png) + +### Profile Delete + +Delete Profile + +![Profile New](images/Menu-Profile-Delete.png) + +## 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"> -