├── .github ├── ISSUE_TEMPLATE │ ├── bug.yml │ ├── config.yml │ └── feature.yml └── images │ └── logo.svg ├── .gitignore ├── .gitmodules ├── .idea ├── .gitignore ├── .name ├── codeStyles │ ├── Project.xml │ └── codeStyleConfig.xml ├── compiler.xml ├── gradle.xml ├── kotlinc.xml ├── migrations.xml ├── misc.xml ├── render.experimental.xml └── vcs.xml ├── LICENSE ├── README-ru.md ├── README.md ├── app ├── .gitignore ├── build.gradle.kts ├── proguard-rules.pro └── src │ ├── androidTest │ └── java │ │ └── io │ │ └── github │ │ └── dovecoteescapee │ │ └── byedpi │ │ └── ExampleInstrumentedTest.kt │ ├── main │ ├── AndroidManifest.xml │ ├── cpp │ │ ├── CMakeLists.txt │ │ ├── main.h │ │ ├── native-lib.c │ │ ├── utils.c │ │ └── utils.h │ ├── ic_launcher-playstore.png │ ├── java │ │ └── io │ │ │ └── github │ │ │ └── dovecoteescapee │ │ │ └── byedpi │ │ │ ├── activities │ │ │ ├── MainActivity.kt │ │ │ └── SettingsActivity.kt │ │ │ ├── core │ │ │ ├── ByeDpiProxy.kt │ │ │ ├── ByeDpiProxyPreferences.kt │ │ │ └── TProxyService.kt │ │ │ ├── data │ │ │ ├── Actions.kt │ │ │ ├── AppStatus.kt │ │ │ ├── Broadcasts.kt │ │ │ └── ServiceStatus.kt │ │ │ ├── fragments │ │ │ ├── ByeDpiCommandLineSettingsFragment.kt │ │ │ ├── ByeDpiUISettingsFragment.kt │ │ │ └── MainSettingsFragment.kt │ │ │ ├── services │ │ │ ├── ByeDpiProxyService.kt │ │ │ ├── ByeDpiStatus.kt │ │ │ ├── ByeDpiVpnService.kt │ │ │ ├── LifecycleVpnService.kt │ │ │ ├── QuickTileService.kt │ │ │ └── ServiceManager.kt │ │ │ └── utility │ │ │ ├── ArgumentsUtils.kt │ │ │ ├── NotificationUtils.kt │ │ │ ├── PreferencesUtils.kt │ │ │ └── ValidateUtils.kt │ ├── jni │ │ ├── Android.mk │ │ └── Application.mk │ └── res │ │ ├── drawable │ │ ├── baseline_settings_24.xml │ │ ├── ic_github_36.xml │ │ └── ic_notification.xml │ │ ├── layout │ │ ├── activity_main.xml │ │ └── activity_settings.xml │ │ ├── menu │ │ ├── menu_main.xml │ │ └── menu_settings.xml │ │ ├── mipmap-anydpi-v26 │ │ ├── ic_launcher.xml │ │ └── ic_launcher_round.xml │ │ ├── mipmap-hdpi │ │ ├── ic_launcher.webp │ │ ├── ic_launcher_foreground.webp │ │ └── ic_launcher_round.webp │ │ ├── mipmap-mdpi │ │ ├── ic_launcher.webp │ │ ├── ic_launcher_foreground.webp │ │ └── ic_launcher_round.webp │ │ ├── mipmap-xhdpi │ │ ├── ic_launcher.webp │ │ ├── ic_launcher_foreground.webp │ │ └── ic_launcher_round.webp │ │ ├── mipmap-xxhdpi │ │ ├── ic_launcher.webp │ │ ├── ic_launcher_foreground.webp │ │ └── ic_launcher_round.webp │ │ ├── mipmap-xxxhdpi │ │ ├── ic_launcher.webp │ │ ├── ic_launcher_foreground.webp │ │ └── ic_launcher_round.webp │ │ ├── values-night │ │ └── themes.xml │ │ ├── values │ │ ├── arrays.xml │ │ ├── colors.xml │ │ ├── ic_launcher_background.xml │ │ ├── strings.xml │ │ └── themes.xml │ │ └── xml │ │ ├── backup_rules.xml │ │ ├── byedpi_cmd_settings.xml │ │ ├── byedpi_ui_settings.xml │ │ ├── data_extraction_rules.xml │ │ └── main_settings.xml │ └── test │ └── java │ └── io │ └── github │ └── dovecoteescapee │ └── byedpi │ └── ExampleUnitTest.kt ├── build.gradle.kts ├── fastlane └── metadata │ └── android │ ├── en-US │ ├── full_description.txt │ ├── images │ │ ├── icon.png │ │ └── phoneScreenshots │ │ │ ├── screenshot1.png │ │ │ ├── screenshot2.png │ │ │ └── screenshot3.png │ └── short_description.txt │ └── ru-RU │ ├── full_description.txt │ └── short_description.txt ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat └── settings.gradle.kts /.github/ISSUE_TEMPLATE/bug.yml: -------------------------------------------------------------------------------- 1 | name: Bug Report / Сообщение об ошибке 2 | description: File a bug report / Сообщить об ошибке в программе 3 | labels: [ bug ] 4 | assignees: dovecoteescapee 5 | body: 6 | - type: markdown 7 | attributes: 8 | value: | 9 | ### If you have trouble with specific websites or or can't configure the app, please use the [discussions section](https://github.com/dovecoteescapee/ByeDPIAndroid/discussions) to discuss it. This template is only for reporting bugs in the ByeDPI program. 10 | ### Если у вас не работают какие-то сайты или не получается настроить приложение, используйте [раздел дискуссий](https://github.com/dovecoteescapee/ByeDPIAndroid/discussions) для обсуждения этого. Этот шаблон предназначен только для сообщений об ошибках в программе ByeDPI. 11 | - type: textarea 12 | id: bug 13 | attributes: 14 | label: Describe the bug / Описание ошибки 15 | description: A clear and concise description of what the bug is / Четкое и краткое описание ошибки 16 | validations: 17 | required: true 18 | - type: textarea 19 | id: reproduce 20 | attributes: 21 | label: To Reproduce / Как воспроизвести 22 | description: Steps to reproduce the behavior / Шаги для воспроизведения проблемы 23 | value: | 24 | 1. Go to '...' 25 | 2. Click on '....' 26 | 3. Scroll down to '....' 27 | 4. See error 28 | validations: 29 | required: true 30 | - type: textarea 31 | id: expected 32 | attributes: 33 | label: Expected behavior / Ожидаемое поведение 34 | description: A clear and concise description of what you expected to happen / Четкое и краткое описание ожидаемого поведения 35 | validations: 36 | required: true 37 | - type: textarea 38 | id: screenshots 39 | attributes: 40 | label: Screenshots / Скриншоты 41 | description: If applicable, add screenshots to help explain your problem / При необходимости добавьте скриншоты, чтобы объяснить вашу проблему 42 | - type: textarea 43 | id: environment 44 | attributes: 45 | label: Environment / Окружение 46 | description: Please complete the following information / Пожалуйста, заполните следующую информацию 47 | value: | 48 | **Smartphone / Смартфон:** 49 | - Device: [e.g. Pixel Fold, Samsung Galaxy S24] 50 | - Android version: [e.g. 10] 51 | - ByeDPI version: [e.g. 1.0.2] 52 | validations: 53 | required: true 54 | - type: textarea 55 | id: context 56 | attributes: 57 | label: Additional context / Дополнительная информация 58 | description: Add any other context about the problem here / Добавьте любую другую информацию о проблеме здесь 59 | - type: checkboxes 60 | id: checklist 61 | attributes: 62 | label: Before you submit / Прежде чем отправить 63 | options: 64 | - label: I have searched the open and closed issues for duplicates / Я искал(а) открытые и закрытые задачи на предмет дубликатов 65 | required: true 66 | - label: This is not a question, feature request, or general feedback / Это не вопрос, предложение новой функциональности или общий отзыв 67 | required: true 68 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false 2 | contact_links: 3 | - name: Questions about the program on Github / Вопросы по програме на Github 4 | url: https://github.com/dovecoteescapee/ByeDPIAndroid/discussions 5 | about: Please do not ask questions in the issue tracker / Пожалуйста, не задавайте вопросы в трекере задач 6 | - name: Questions about the program on NTC.party / Вопросы по програме на NTC.party 7 | url: https://ntc.party/c/community-software/byedpi/39 8 | about: Please do not ask questions in the issue tracker / Пожалуйста, не задавайте вопросы в трекере задач 9 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature.yml: -------------------------------------------------------------------------------- 1 | name: Feature request / Предложить новую функциональность 2 | description: Suggest an idea for this project / Предложить идею для проекта 3 | labels: [ enhancement ] 4 | assignees: dovecoteescapee 5 | body: 6 | - type: textarea 7 | id: problem 8 | attributes: 9 | label: What problem does your proposal solve? / Какую проблему решает ваше предложение? 10 | description: A clear and concise description of what the problem is / Четкое и краткое описание проблемы 11 | validations: 12 | required: true 13 | - type: textarea 14 | id: solution 15 | attributes: 16 | label: Describe the solution you'd like / Опишите решение, которое вы предлагаете 17 | description: A clear and concise description of what you want to happen / Четкое и краткое описание того, что вы хотите, чтобы произошло 18 | validations: 19 | required: true 20 | - type: textarea 21 | id: alternatives 22 | attributes: 23 | label: Describe alternatives you've considered / Опишите альтернативные варианты, которые вы рассматривали 24 | description: A clear and concise description of any alternative solutions or features you've considered / Четкое и краткое описание любых альтернативных решений или функций, которые вы рассматривали 25 | validations: 26 | required: true 27 | - type: checkboxes 28 | id: checklist 29 | attributes: 30 | label: Before you submit / Прежде чем отправить 31 | options: 32 | - label: I have searched the open and closed issues for duplicates / Я искал(а) открытые и закрытые задачи на предмет дубликатов 33 | required: true 34 | - label: I have verified that this feature does not already exist / Я убедился(ась), что эта функция еще не существует 35 | required: true 36 | -------------------------------------------------------------------------------- /.github/images/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | /local.properties 4 | /.idea/caches 5 | /.idea/deploymentTargetDropDown.xml 6 | /.idea/libraries 7 | /.idea/modules.xml 8 | /.idea/workspace.xml 9 | /.idea/navEditor.xml 10 | /.idea/assetWizardSettings.xml 11 | .DS_Store 12 | /build 13 | /captures 14 | .externalNativeBuild 15 | .cxx 16 | local.properties 17 | /app/src/main/jniLibs 18 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "hev-socks5-tunnel"] 2 | path = app/src/main/jni/hev-socks5-tunnel 3 | url = https://github.com/heiher/hev-socks5-tunnel.git 4 | [submodule "byedpi"] 5 | path = app/src/main/cpp/byedpi 6 | url = https://github.com/hufrea/byedpi.git 7 | -------------------------------------------------------------------------------- /.idea/.gitignore: -------------------------------------------------------------------------------- 1 | # Default ignored files 2 | /shelf/ 3 | /workspace.xml 4 | -------------------------------------------------------------------------------- /.idea/.name: -------------------------------------------------------------------------------- 1 | ByeDpi -------------------------------------------------------------------------------- /.idea/codeStyles/Project.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | 119 | 120 | 122 | 123 | -------------------------------------------------------------------------------- /.idea/codeStyles/codeStyleConfig.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | -------------------------------------------------------------------------------- /.idea/compiler.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.idea/gradle.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 18 | 19 | -------------------------------------------------------------------------------- /.idea/kotlinc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | -------------------------------------------------------------------------------- /.idea/migrations.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 10 | -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 9 | -------------------------------------------------------------------------------- /.idea/render.experimental.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /README-ru.md: -------------------------------------------------------------------------------- 1 | # ByeDPI for Android 2 | 3 | [English](README.md) | **Русский** 4 | 5 |
6 | Логотип ByeDPI 7 |
8 | 9 | --- 10 | 11 | Приложение для Android, которое запускает локальный VPN-сервис для обхода DPI (Deep Packet Inspection) и цензуры. 12 | 13 | Приложение локально запускает SOCKS5-прокси [ByeDPI](https://github.com/hufrea/byedpi) и перенаправляет весь трафик через него. 14 | 15 | ## Установка 16 | 17 | [Скачать с GitHub](https://github.com/dovecoteescapee/ByeDPIAndroid/releases) 20 | [Скачать с IzzyOnDroid](https://apt.izzysoft.de/fdroid/index/apk/io.github.dovecoteescapee.byedpi) 23 | 24 | ### Или используйте Obtainium 25 | 26 | 1. Установите [Obtainium](https://github.com/ImranR98/Obtainium/blob/main/README.md#installation) 27 | 2. Добавьте приложение по URL: 28 | `https://github.com/dovecoteescapee/ByeDPIAndroid` 29 | 30 | ## Настройки 31 | 32 | Для обхода некоторых блокировок может потребоваться изменить настройки. Подробнее о различных настройках можно прочитать в [документации ByeDPI](https://github.com/hufrea/byedpi/blob/v0.13/README.md). 33 | 34 | ## FAQ 35 | 36 | ### У меня не получается настроить. Что делать? 37 | 38 | Вы можете попросить помощи в [discussion](https://github.com/dovecoteescapee/ByeDPIAndroid/discussions). 39 | 40 | ### Приложение требует root-права? 41 | 42 | Нет. Все функции приложения работают без root-прав. 43 | 44 | ### Это VPN? 45 | 46 | Нет. Приложение использует VPN-режим на Android для перенаправления трафика, но не передает ничего на удаленный сервер. Оно не шифрует трафик и не скрывает ваш IP-адрес. 47 | 48 | ### Какие данные собирает приложение? 49 | 50 | Никакие. Приложения не отправляет никакие данные на удаленный сервер. Весь трафик обрабатывается на устройстве. 51 | 52 | ### Как использовать ByeDPI вместе с AdGuard? 53 | 54 | 1. Запустите ByeDPI в режиме прокси. 55 | 2. Добавьте ByeDPI в исключения AdGuard на вкладке "Управление приложениями". 56 | 3. В настройках AdGuard укажите прокси: 57 | 58 | ```plaintext 59 | Тип прокси: SOCKS5 60 | Хост: 127.0.0.1 61 | Порт: 1080 (по умолчанию) 62 | ``` 63 | 64 | ### А есть для других платформ? 65 | 66 | [Список аналогов](https://github.com/ValdikSS/GoodbyeDPI/blob/master/README.md#similar-projects) 67 | 68 | ### Что такое DPI? 69 | 70 | DPI (Deep Packet Inspection) - это технология для анализа и фильтрации трафика. Она используется провайдерами и государственными органами для блокировки сайтов и сервисов. 71 | 72 | ## Зависимости 73 | 74 | - [ByeDPI](https://github.com/hufrea/byedpi) 75 | - [hev-socks5-tunnel](https://github.com/heiher/hev-socks5-tunnel) 76 | 77 | ## Сборка 78 | 79 | Для сборки приложения требуется: 80 | 81 | 1. JDK 8 или новее 82 | 2. Android SDK 83 | 3. Android NDK 84 | 4. CMake 3.22.1 или новее 85 | 86 | Сборка приложения: 87 | 88 | 1. Клонируйте репозиторий с подмодулями: 89 | ```bash 90 | git clone --recurse-submodules 91 | ``` 92 | 2. Запустите скрипт сборки из корня репозитория: 93 | ```bash 94 | ./gradlew assembleRelease` 95 | ``` 96 | 3. APK будет лежать в `app/build/outputs/apk/release/` 97 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ByeDPI for Android 2 | 3 | **English** | [Русский](README-ru.md) 4 | 5 |
6 | ByeDPI logo 7 |
8 | 9 | --- 10 | 11 | Android application that runs a local VPN service to bypass DPI (Deep Packet Inspection) and censorship. 12 | 13 | 14 | This application runs a SOCKS5 proxy [ByeDPI](https://github.com/hufrea/byedpi) and redirects all traffic through it. 15 | 16 | ## Installation 17 | 18 | [Get it on GitHub](https://github.com/dovecoteescapee/ByeDPIAndroid/releases) 21 | [Get it on IzzyOnDroid](https://apt.izzysoft.de/fdroid/index/apk/io.github.dovecoteescapee.byedpi) 24 | 25 | ### Or use Obtainium 26 | 27 | 1. Install [Obtainium](https://github.com/ImranR98/Obtainium/blob/main/README.md#installation) 28 | 2. Add the app by URL: 29 | `https://github.com/dovecoteescapee/ByeDPIAndroid` 30 | 31 | ## Settings 32 | 33 | To bypass some blocks, you may need to change the settings. More about the various settings can be found in the [ByeDPI documentation](https://github.com/hufrea/byedpi/blob/v0.13/README.md). 34 | 35 | ## FAQ 36 | 37 | ### I can't configure it. What to do? 38 | 39 | You can ask for help in [discussion](https://github.com/dovecoteescapee/ByeDPIAndroid/discussions). 40 | 41 | ### Does the application require root access? 42 | 43 | No. All application features work without root. 44 | 45 | ### Is this a VPN? 46 | 47 | No. The application uses the VPN mode on Android to redirect traffic, but does not send anything to a remote server. It does not encrypt traffic and does not hide your IP address. 48 | 49 | ### How to use ByeDPI with AdGuard? 50 | 51 | 1. Run ByeDPI in proxy mode. 52 | 2. Add ByeDPI to AdGuard exceptions on the "App management" tab. 53 | 3. In AdGuard settings, specify the proxy: 54 | 55 | ```plaintext 56 | Proxy type: SOCKS5 57 | Proxy host: 127.0.0.1 58 | Proxy port: 1080 (default) 59 | ``` 60 | 61 | ### What data does the application collect? 62 | 63 | None. The application does not send any data to a remote server. All traffic is processed on the device. 64 | 65 | ### Are there any for other platforms? 66 | 67 | [Similar projects](https://github.com/ValdikSS/GoodbyeDPI/blob/master/README.md#similar-projects)) 68 | 69 | ### What is DPI? 70 | 71 | DPI (Deep Packet Inspection) is a technology for analyzing and filtering traffic. It is used by providers and government agencies to block sites and services. 72 | 73 | ## Dependencies 74 | 75 | - [ByeDPI](https://github.com/hufrea/byedpi) 76 | - [hev-socks5-tunnel](https://github.com/heiher/hev-socks5-tunnel) 77 | 78 | ## Building 79 | 80 | For building the application, you need: 81 | 82 | 1. JDK 8 or later 83 | 2. Android SDK 84 | 3. Android NDK 85 | 4. CMake 3.22.1 or later 86 | 87 | To build the application: 88 | 89 | 1. Clone the repository with submodules: 90 | ```bash 91 | git clone --recurse-submodules 92 | ``` 93 | 2. Run the build script from the root of the repository: 94 | ```bash 95 | ./gradlew assembleRelease 96 | ``` 97 | 3. The APK will be in `app/build/outputs/apk/release/` 98 | -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | /debug 3 | /release 4 | *.aar 5 | *.jar -------------------------------------------------------------------------------- /app/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | id("com.android.application") 3 | id("org.jetbrains.kotlin.android") 4 | } 5 | 6 | android { 7 | namespace = "io.github.dovecoteescapee.byedpi" 8 | compileSdk = 34 9 | 10 | defaultConfig { 11 | applicationId = "io.github.dovecoteescapee.byedpi" 12 | minSdk = 21 13 | targetSdk = 34 14 | versionCode = 10 15 | versionName = "1.2.0" 16 | 17 | testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" 18 | 19 | ndk { 20 | abiFilters.add("armeabi-v7a") 21 | abiFilters.add("arm64-v8a") 22 | abiFilters.add("x86") 23 | abiFilters.add("x86_64") 24 | } 25 | } 26 | 27 | buildFeatures { 28 | buildConfig = true 29 | } 30 | 31 | buildTypes { 32 | release { 33 | buildConfigField("String", "VERSION_NAME", "\"${defaultConfig.versionName}\"") 34 | 35 | isMinifyEnabled = false 36 | proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro") 37 | } 38 | debug { 39 | buildConfigField("String", "VERSION_NAME", "\"${defaultConfig.versionName}-debug\"") 40 | } 41 | } 42 | compileOptions { 43 | sourceCompatibility = JavaVersion.VERSION_1_8 44 | targetCompatibility = JavaVersion.VERSION_1_8 45 | } 46 | kotlinOptions { 47 | jvmTarget = "1.8" 48 | } 49 | externalNativeBuild { 50 | cmake { 51 | path = file("src/main/cpp/CMakeLists.txt") 52 | version = "3.22.1" 53 | } 54 | } 55 | buildFeatures { 56 | viewBinding = true 57 | } 58 | 59 | // https://android.izzysoft.de/articles/named/iod-scan-apkchecks?lang=en#blobs 60 | dependenciesInfo { 61 | // Disables dependency metadata when building APKs. 62 | includeInApk = false 63 | // Disables dependency metadata when building Android App Bundles. 64 | includeInBundle = false 65 | } 66 | } 67 | 68 | dependencies { 69 | implementation("androidx.fragment:fragment-ktx:1.8.2") 70 | implementation("androidx.core:core-ktx:1.13.1") 71 | implementation("androidx.appcompat:appcompat:1.7.0") 72 | implementation("androidx.preference:preference-ktx:1.2.1") 73 | implementation("com.takisoft.preferencex:preferencex:1.1.0") 74 | implementation("com.google.android.material:material:1.12.0") 75 | implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.8.4") 76 | implementation("androidx.lifecycle:lifecycle-service:2.8.4") 77 | testImplementation("junit:junit:4.13.2") 78 | androidTestImplementation("androidx.test.ext:junit:1.2.1") 79 | androidTestImplementation("androidx.test.espresso:espresso-core:3.6.1") 80 | } 81 | 82 | tasks.register("runNdkBuild") { 83 | group = "build" 84 | 85 | val ndkDir = android.ndkDirectory 86 | executable = if (System.getProperty("os.name").startsWith("Windows", ignoreCase = true)) { 87 | "$ndkDir\\ndk-build.cmd" 88 | } else { 89 | "$ndkDir/ndk-build" 90 | } 91 | setArgs(listOf( 92 | "NDK_PROJECT_PATH=build/intermediates/ndkBuild", 93 | "NDK_LIBS_OUT=src/main/jniLibs", 94 | "APP_BUILD_SCRIPT=src/main/jni/Android.mk", 95 | "NDK_APPLICATION_MK=src/main/jni/Application.mk" 96 | )) 97 | 98 | println("Command: $commandLine") 99 | } 100 | 101 | tasks.preBuild { 102 | dependsOn("runNdkBuild") 103 | } -------------------------------------------------------------------------------- /app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile -------------------------------------------------------------------------------- /app/src/androidTest/java/io/github/dovecoteescapee/byedpi/ExampleInstrumentedTest.kt: -------------------------------------------------------------------------------- 1 | package io.github.dovecoteescapee.byedpi 2 | 3 | import androidx.test.platform.app.InstrumentationRegistry 4 | import androidx.test.ext.junit.runners.AndroidJUnit4 5 | 6 | import org.junit.Test 7 | import org.junit.runner.RunWith 8 | 9 | import org.junit.Assert.* 10 | 11 | /** 12 | * Instrumented test, which will execute on an Android device. 13 | * 14 | * See [testing documentation](http://d.android.com/tools/testing). 15 | */ 16 | @RunWith(AndroidJUnit4::class) 17 | class ExampleInstrumentedTest { 18 | @Test 19 | fun useAppContext() { 20 | // Context of the app under test. 21 | val appContext = InstrumentationRegistry.getInstrumentation().targetContext 22 | assertEquals("io.github.dovecoteescapee.byedpi", appContext.packageName) 23 | } 24 | } -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | 9 | 11 | 13 | 14 | 26 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 42 | 43 | 47 | 48 | 49 | 50 | 53 | 56 | 57 | 58 | 61 | 64 | 65 | 66 | 72 | 73 | 75 | 76 | 77 | 78 | 79 | -------------------------------------------------------------------------------- /app/src/main/cpp/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.22.1) 2 | 3 | project(byedpi_native) 4 | 5 | file(GLOB BYE_DPI_SRC byedpi/*.c) 6 | list(REMOVE_ITEM BYE_DPI_SRC ${CMAKE_CURRENT_SOURCE_DIR}/byedpi/win_service.c) 7 | 8 | add_library(byedpi SHARED ${BYE_DPI_SRC} native-lib.c utils.c) 9 | target_include_directories(byedpi PRIVATE byedpi) 10 | 11 | target_compile_options(byedpi PRIVATE -std=c99 -O2 -D_XOPEN_SOURCE=500) 12 | target_compile_definitions(byedpi PRIVATE ANDROID_APP) 13 | 14 | target_link_libraries(byedpi PRIVATE android log) 15 | -------------------------------------------------------------------------------- /app/src/main/cpp/main.h: -------------------------------------------------------------------------------- 1 | extern char *oob_char; 2 | extern int NOT_EXIT; 3 | 4 | struct sockaddr_ina; 5 | 6 | int get_default_ttl(); 7 | 8 | int get_addr(const char *str, struct sockaddr_ina *addr); 9 | 10 | void *add(void **root, int *n, size_t ss); 11 | 12 | void clear_params(void); 13 | 14 | char *ftob(const char *str, ssize_t *sl); 15 | 16 | char *data_from_str(const char *str, ssize_t *size); 17 | 18 | size_t parse_cform(char *buffer, size_t blen, 19 | const char *str, size_t slen); 20 | 21 | struct mphdr *parse_hosts(char *buffer, size_t size); 22 | 23 | int parse_offset(struct part *part, const char *str); 24 | -------------------------------------------------------------------------------- /app/src/main/cpp/native-lib.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | #include 5 | #include 6 | 7 | #include "byedpi/error.h" 8 | #include "byedpi/proxy.h" 9 | #include "byedpi/params.h" 10 | #include "byedpi/packets.h" 11 | #include "main.h" 12 | #include "utils.h" 13 | 14 | const enum demode DESYNC_METHODS[] = { 15 | DESYNC_NONE, 16 | DESYNC_SPLIT, 17 | DESYNC_DISORDER, 18 | DESYNC_FAKE, 19 | DESYNC_OOB, 20 | DESYNC_DISOOB, 21 | }; 22 | 23 | enum hosts_mode { 24 | HOSTS_DISABLE, 25 | HOSTS_BLACKLIST, 26 | HOSTS_WHITELIST, 27 | }; 28 | 29 | JNIEXPORT jint JNI_OnLoad( 30 | __attribute__((unused)) JavaVM *vm, 31 | __attribute__((unused)) void *reserved) { 32 | default_params = params; 33 | return JNI_VERSION_1_6; 34 | } 35 | 36 | JNIEXPORT jint JNICALL 37 | Java_io_github_dovecoteescapee_byedpi_core_ByeDpiProxy_jniCreateSocketWithCommandLine( 38 | JNIEnv *env, 39 | __attribute__((unused)) jobject thiz, 40 | jobjectArray args) { 41 | int argc = (*env)->GetArrayLength(env, args); 42 | char *argv[argc]; 43 | for (int i = 0; i < argc; i++) { 44 | jstring arg = (jstring) (*env)->GetObjectArrayElement(env, args, i); 45 | const char *arg_str = (*env)->GetStringUTFChars(env, arg, 0); 46 | argv[i] = strdup(arg_str); 47 | (*env)->ReleaseStringUTFChars(env, arg, arg_str); 48 | } 49 | 50 | int res = parse_args(argc, argv); 51 | if (res < 0) { 52 | uniperror("parse_args"); 53 | return -1; 54 | } 55 | 56 | int fd = listen_socket((struct sockaddr_ina *)¶ms.laddr); 57 | if (fd < 0) { 58 | uniperror("listen_socket"); 59 | return -1; 60 | } 61 | LOG(LOG_S, "listen_socket, fd: %d", fd); 62 | 63 | return fd; 64 | } 65 | 66 | JNIEXPORT jint JNICALL 67 | Java_io_github_dovecoteescapee_byedpi_core_ByeDpiProxy_jniCreateSocket( 68 | JNIEnv *env, 69 | __attribute__((unused)) jobject thiz, 70 | jstring ip, 71 | jint port, 72 | jint max_connections, 73 | jint buffer_size, 74 | jint default_ttl, 75 | jboolean custom_ttl, 76 | jboolean no_domain, 77 | jboolean desync_http, 78 | jboolean desync_https, 79 | jboolean desync_udp, 80 | jint desync_method, 81 | jint split_position, 82 | jboolean split_at_host, 83 | jint fake_ttl, 84 | jstring fake_sni, 85 | jbyte custom_oob_char, 86 | jboolean host_mixed_case, 87 | jboolean domain_mixed_case, 88 | jboolean host_remove_spaces, 89 | jboolean tls_record_split, 90 | jint tls_record_split_position, 91 | jboolean tls_record_split_at_sni, 92 | jint hosts_mode, 93 | jstring hosts, 94 | jboolean tfo, 95 | jint udp_fake_count, 96 | jboolean drop_sack, 97 | jint fake_offset) { 98 | struct sockaddr_ina s; 99 | 100 | const char *address = (*env)->GetStringUTFChars(env, ip, 0); 101 | int res = get_addr(address, &s); 102 | (*env)->ReleaseStringUTFChars(env, ip, address); 103 | if (res < 0) { 104 | uniperror("get_addr"); 105 | return -1; 106 | } 107 | 108 | s.in.sin_port = htons(port); 109 | 110 | params.max_open = max_connections; 111 | params.bfsize = buffer_size; 112 | params.resolve = !no_domain; 113 | params.tfo = tfo; 114 | 115 | if (custom_ttl) { 116 | params.def_ttl = default_ttl; 117 | params.custom_ttl = 1; 118 | } 119 | 120 | if (!params.def_ttl) { 121 | if ((params.def_ttl = get_default_ttl()) < 1) { 122 | uniperror("get_default_ttl"); 123 | reset_params(); 124 | return -1; 125 | } 126 | } 127 | 128 | if (hosts_mode == HOSTS_WHITELIST) { 129 | struct desync_params *dp = add( 130 | (void *) ¶ms.dp, 131 | ¶ms.dp_count, 132 | sizeof(struct desync_params) 133 | ); 134 | if (!dp) { 135 | uniperror("add"); 136 | reset_params(); 137 | return -1; 138 | } 139 | 140 | const char *str = (*env)->GetStringUTFChars(env, hosts, 0); 141 | dp->file_ptr = data_from_str(str, &dp->file_size); 142 | (*env)->ReleaseStringUTFChars(env, hosts, str); 143 | dp->hosts = parse_hosts(dp->file_ptr, dp->file_size); 144 | if (!dp->hosts) { 145 | perror("parse_hosts"); 146 | clear_params(); 147 | return -1; 148 | } 149 | } 150 | 151 | struct desync_params *dp = add( 152 | (void *) ¶ms.dp, 153 | ¶ms.dp_count, 154 | sizeof(struct desync_params) 155 | ); 156 | if (!dp) { 157 | uniperror("add"); 158 | reset_params(); 159 | return -1; 160 | } 161 | 162 | if (hosts_mode == HOSTS_BLACKLIST) { 163 | const char *str = (*env)->GetStringUTFChars(env, hosts, 0); 164 | dp->file_ptr = data_from_str(str, &dp->file_size); 165 | (*env)->ReleaseStringUTFChars(env, hosts, str); 166 | dp->hosts = parse_hosts(dp->file_ptr, dp->file_size); 167 | if (!dp->hosts) { 168 | perror("parse_hosts"); 169 | clear_params(); 170 | return -1; 171 | } 172 | } 173 | 174 | dp->ttl = fake_ttl; 175 | dp->udp_fake_count = udp_fake_count; 176 | dp->drop_sack = drop_sack; 177 | dp->proto = 178 | IS_HTTP * desync_http | 179 | IS_HTTPS * desync_https | 180 | IS_UDP * desync_udp; 181 | dp->mod_http = 182 | MH_HMIX * host_mixed_case | 183 | MH_DMIX * domain_mixed_case | 184 | MH_SPACE * host_remove_spaces; 185 | 186 | struct part *part = add( 187 | (void *) &dp->parts, 188 | &dp->parts_n, 189 | sizeof(struct part) 190 | ); 191 | if (!part) { 192 | uniperror("add"); 193 | reset_params(); 194 | return -1; 195 | } 196 | 197 | enum demode mode = DESYNC_METHODS[desync_method]; 198 | 199 | int offset_flag = dp->proto || desync_https ? OFFSET_SNI : OFFSET_HOST; 200 | 201 | part->flag = split_at_host ? offset_flag : 0; 202 | part->pos = split_position; 203 | part->m = mode; 204 | 205 | if (tls_record_split) { 206 | struct part *tlsrec_part = add( 207 | (void *) &dp->tlsrec, 208 | &dp->tlsrec_n, 209 | sizeof(struct part) 210 | ); 211 | 212 | if (!tlsrec_part) { 213 | uniperror("add"); 214 | reset_params(); 215 | return -1; 216 | } 217 | 218 | tlsrec_part->flag = tls_record_split_at_sni ? offset_flag : 0; 219 | tlsrec_part->pos = tls_record_split_position; 220 | } 221 | 222 | if (mode == DESYNC_FAKE) { 223 | dp->fake_offset = fake_offset; 224 | 225 | const char *sni = (*env)->GetStringUTFChars(env, fake_sni, 0); 226 | LOG(LOG_S, "fake_sni: %s", sni); 227 | res = change_tls_sni(sni, fake_tls.data, fake_tls.size); 228 | (*env)->ReleaseStringUTFChars(env, fake_sni, sni); 229 | if (res) { 230 | fprintf(stderr, "error chsni\n"); 231 | return -1; 232 | } 233 | } 234 | 235 | if (mode == DESYNC_OOB) { 236 | dp->oob_char[0] = custom_oob_char; 237 | dp->oob_char[1] = 1; 238 | } 239 | 240 | if (dp->proto) { 241 | dp = add((void *)¶ms.dp, 242 | ¶ms.dp_count, sizeof(struct desync_params)); 243 | if (!dp) { 244 | uniperror("add"); 245 | clear_params(); 246 | return -1; 247 | } 248 | } 249 | 250 | params.mempool = mem_pool(0); 251 | if (!params.mempool) { 252 | uniperror("mem_pool"); 253 | clear_params(); 254 | return -1; 255 | } 256 | 257 | int fd = listen_socket(&s); 258 | if (fd < 0) { 259 | uniperror("listen_socket"); 260 | return -1; 261 | } 262 | LOG(LOG_S, "listen_socket, fd: %d", fd); 263 | 264 | return fd; 265 | } 266 | 267 | JNIEXPORT jint JNICALL 268 | Java_io_github_dovecoteescapee_byedpi_core_ByeDpiProxy_jniStartProxy( 269 | __attribute__((unused)) JNIEnv *env, 270 | __attribute__((unused)) jobject thiz, 271 | jint fd) { 272 | LOG(LOG_S, "start_proxy, fd: %d", fd); 273 | NOT_EXIT = 1; 274 | if (event_loop(fd) < 0) { 275 | uniperror("event_loop"); 276 | return get_e(); 277 | } 278 | return 0; 279 | } 280 | 281 | JNIEXPORT jint JNICALL 282 | Java_io_github_dovecoteescapee_byedpi_core_ByeDpiProxy_jniStopProxy( 283 | __attribute__((unused)) JNIEnv *env, 284 | __attribute__((unused)) jobject thiz, 285 | jint fd) { 286 | LOG(LOG_S, "stop_proxy, fd: %d", fd); 287 | 288 | int res = shutdown(fd, SHUT_RDWR); 289 | reset_params(); 290 | 291 | if (res < 0) { 292 | uniperror("shutdown"); 293 | return get_e(); 294 | } 295 | return 0; 296 | } -------------------------------------------------------------------------------- /app/src/main/cpp/utils.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | 5 | #include "byedpi/params.h" 6 | #include "error.h" 7 | #include "main.h" 8 | #include "packets.h" 9 | #include "utils.h" 10 | 11 | struct params default_params; 12 | 13 | void reset_params(void) { 14 | clear_params(); 15 | params = default_params; 16 | } 17 | 18 | extern const struct option options[38]; 19 | 20 | int parse_args(int argc, char **argv) 21 | { 22 | int optc = sizeof(options)/sizeof(*options); 23 | for (int i = 0, e = optc; i < e; i++) 24 | optc += options[i].has_arg; 25 | 26 | char opt[optc + 1]; 27 | opt[optc] = 0; 28 | 29 | for (int i = 0, o = 0; o < optc; i++, o++) { 30 | opt[o] = options[i].val; 31 | for (int c = options[i].has_arg; c; c--) { 32 | o++; 33 | opt[o] = ':'; 34 | } 35 | } 36 | 37 | params.laddr.sin6_port = htons(1080); 38 | 39 | int rez; 40 | int invalid = 0; 41 | 42 | long val; 43 | char *end = 0; 44 | 45 | struct desync_params *dp = add((void *)¶ms.dp, 46 | ¶ms.dp_count, sizeof(struct desync_params)); 47 | if (!dp) { 48 | reset_params(); 49 | return -1; 50 | } 51 | 52 | optind = optreset = 1; 53 | 54 | while (!invalid && (rez = getopt_long( 55 | argc, argv, opt, options, 0)) != -1) { 56 | 57 | switch (rez) { 58 | 59 | case 'N': 60 | params.resolve = 0; 61 | break; 62 | case 'X': 63 | params.ipv6 = 0; 64 | break; 65 | case 'U': 66 | params.udp = 0; 67 | break; 68 | 69 | // case 'h': 70 | // printf(help_text); 71 | // reset_params(); 72 | // return 0; 73 | // case 'v': 74 | // printf("%s\n", VERSION); 75 | // reset_params(); 76 | // return 0; 77 | 78 | case 'i': 79 | if (get_addr(optarg, 80 | (struct sockaddr_ina *)¶ms.laddr) < 0) 81 | invalid = 1; 82 | break; 83 | 84 | case 'p': 85 | val = strtol(optarg, &end, 0); 86 | if (val <= 0 || val > 0xffff || *end) 87 | invalid = 1; 88 | else 89 | params.laddr.sin6_port = htons(val); 90 | break; 91 | 92 | case 'I': 93 | if (get_addr(optarg, 94 | (struct sockaddr_ina *)¶ms.baddr) < 0) 95 | invalid = 1; 96 | break; 97 | 98 | case 'b': 99 | val = strtol(optarg, &end, 0); 100 | if (val <= 0 || val > INT_MAX/4 || *end) 101 | invalid = 1; 102 | else 103 | params.bfsize = val; 104 | break; 105 | 106 | case 'c': 107 | val = strtol(optarg, &end, 0); 108 | if (val <= 0 || val >= (0xffff/2) || *end) 109 | invalid = 1; 110 | else 111 | params.max_open = val; 112 | break; 113 | 114 | case 'x': // 115 | params.debug = strtol(optarg, 0, 0); 116 | if (params.debug < 0) 117 | invalid = 1; 118 | break; 119 | 120 | // desync options 121 | 122 | case 'F': 123 | params.tfo = 1; 124 | break; 125 | 126 | case 'A': 127 | dp = add((void *)¶ms.dp, ¶ms.dp_count, 128 | sizeof(struct desync_params)); 129 | if (!dp) { 130 | reset_params(); 131 | return -1; 132 | } 133 | end = optarg; 134 | while (end && !invalid) { 135 | switch (*end) { 136 | case 't': 137 | dp->detect |= DETECT_TORST; 138 | break; 139 | case 'r': 140 | dp->detect |= DETECT_HTTP_LOCAT; 141 | break; 142 | case 'a': 143 | case 's': 144 | dp->detect |= DETECT_TLS_ERR; 145 | break; 146 | case 'n': 147 | break; 148 | default: 149 | invalid = 1; 150 | continue; 151 | } 152 | end = strchr(end, ','); 153 | if (end) end++; 154 | } 155 | break; 156 | 157 | case 'u': 158 | val = strtol(optarg, &end, 0); 159 | if (val <= 0 || *end) 160 | invalid = 1; 161 | else 162 | params.cache_ttl = val; 163 | break; 164 | 165 | case 'T':; 166 | #ifdef __linux__ 167 | float f = strtof(optarg, &end); 168 | val = (long)(f * 1000); 169 | #else 170 | val = strtol(optarg, &end, 0); 171 | #endif 172 | if (val <= 0 || val > UINT_MAX || *end) 173 | invalid = 1; 174 | else 175 | params.timeout = val; 176 | break; 177 | 178 | case 'K': 179 | end = optarg; 180 | while (end && !invalid) { 181 | switch (*end) { 182 | case 't': 183 | dp->proto |= IS_HTTPS; 184 | break; 185 | case 'h': 186 | dp->proto |= IS_HTTP; 187 | break; 188 | case 'u': 189 | dp->proto |= IS_UDP; 190 | break; 191 | default: 192 | invalid = 1; 193 | continue; 194 | } 195 | end = strchr(end, ','); 196 | if (end) end++; 197 | } 198 | break; 199 | 200 | case 'H':; 201 | if (dp->file_ptr) { 202 | continue; 203 | } 204 | dp->file_ptr = ftob(optarg, &dp->file_size); 205 | if (!dp->file_ptr) { 206 | uniperror("read/parse"); 207 | invalid = 1; 208 | continue; 209 | } 210 | dp->hosts = parse_hosts(dp->file_ptr, dp->file_size); 211 | if (!dp->hosts) { 212 | perror("parse_hosts"); 213 | reset_params(); 214 | return -1; 215 | } 216 | break; 217 | 218 | case 's': 219 | case 'd': 220 | case 'o': 221 | case 'q': 222 | case 'f': 223 | ; 224 | struct part *part = add((void *)&dp->parts, 225 | &dp->parts_n, sizeof(struct part)); 226 | if (!part) { 227 | reset_params(); 228 | return -1; 229 | } 230 | if (parse_offset(part, optarg)) { 231 | invalid = 1; 232 | break; 233 | } 234 | switch (rez) { 235 | case 's': part->m = DESYNC_SPLIT; 236 | break; 237 | case 'd': part->m = DESYNC_DISORDER; 238 | break; 239 | case 'o': part->m = DESYNC_OOB; 240 | break; 241 | case 'q': part->m = DESYNC_DISOOB; 242 | break; 243 | case 'f': part->m = DESYNC_FAKE; 244 | } 245 | break; 246 | 247 | case 't': 248 | val = strtol(optarg, &end, 0); 249 | if (val <= 0 || val > 255 || *end) 250 | invalid = 1; 251 | else 252 | dp->ttl = val; 253 | break; 254 | 255 | case 'k': 256 | if (dp->ip_options) { 257 | continue; 258 | } 259 | if (optarg) 260 | dp->ip_options = ftob(optarg, &dp->ip_options_len); 261 | else { 262 | dp->ip_options = ip_option; 263 | dp->ip_options_len = sizeof(ip_option); 264 | } 265 | if (!dp->ip_options) { 266 | uniperror("read/parse"); 267 | invalid = 1; 268 | } 269 | break; 270 | 271 | case 'S': 272 | dp->md5sig = 1; 273 | break; 274 | 275 | case 'O': 276 | val = strtol(optarg, &end, 0); 277 | if (val <= 0 || *end) 278 | invalid = 1; 279 | else 280 | dp->fake_offset = val; 281 | break; 282 | 283 | case 'n': 284 | if (change_tls_sni(optarg, fake_tls.data, fake_tls.size)) { 285 | perror("change_tls_sni"); 286 | reset_params(); 287 | return -1; 288 | } 289 | LOG(LOG_S, "sni: %s", optarg); 290 | break; 291 | 292 | case 'l': 293 | if (dp->fake_data.data) { 294 | continue; 295 | } 296 | dp->fake_data.data = ftob(optarg, &dp->fake_data.size); 297 | if (!dp->fake_data.data) { 298 | uniperror("read/parse"); 299 | invalid = 1; 300 | } 301 | break; 302 | 303 | case 'e': 304 | val = parse_cform(dp->oob_char, 1, optarg, strlen(optarg)); 305 | if (val != 1) { 306 | invalid = 1; 307 | } 308 | else dp->oob_char[1] = 1; 309 | break; 310 | 311 | case 'M': 312 | end = optarg; 313 | while (end && !invalid) { 314 | switch (*end) { 315 | case 'r': 316 | dp->mod_http |= MH_SPACE; 317 | break; 318 | case 'h': 319 | dp->mod_http |= MH_HMIX; 320 | break; 321 | case 'd': 322 | dp->mod_http |= MH_DMIX; 323 | break; 324 | default: 325 | invalid = 1; 326 | continue; 327 | } 328 | end = strchr(end, ','); 329 | if (end) end++; 330 | } 331 | break; 332 | 333 | case 'r': 334 | part = add((void *)&dp->tlsrec, 335 | &dp->tlsrec_n, sizeof(struct part)); 336 | if (!part) { 337 | reset_params(); 338 | return -1; 339 | } 340 | if (parse_offset(part, optarg) 341 | || part->pos > 0xffff) { 342 | invalid = 1; 343 | break; 344 | } 345 | break; 346 | 347 | case 'a': 348 | val = strtol(optarg, &end, 0); 349 | if (val < 0 || val > INT_MAX || *end) 350 | invalid = 1; 351 | else 352 | dp->udp_fake_count = val; 353 | break; 354 | 355 | case 'V': 356 | val = strtol(optarg, &end, 0); 357 | if (val <= 0 || val > USHRT_MAX) 358 | invalid = 1; 359 | else { 360 | dp->pf[0] = htons(val); 361 | if (*end == '-') { 362 | val = strtol(end + 1, &end, 0); 363 | if (val <= 0 || val > USHRT_MAX) 364 | invalid = 1; 365 | } 366 | if (*end) 367 | invalid = 1; 368 | else 369 | dp->pf[1] = htons(val); 370 | } 371 | break; 372 | 373 | case 'g': 374 | val = strtol(optarg, &end, 0); 375 | if (val <= 0 || val > 255 || *end) 376 | invalid = 1; 377 | else { 378 | params.def_ttl = val; 379 | params.custom_ttl = 1; 380 | } 381 | break; 382 | 383 | case 'Y': 384 | dp->drop_sack = 1; 385 | break; 386 | 387 | case 'w': // 388 | params.sfdelay = strtol(optarg, &end, 0); 389 | if (params.sfdelay < 0 || optarg == end 390 | || params.sfdelay >= 1000 || *end) 391 | invalid = 1; 392 | break; 393 | 394 | case 'W': 395 | params.wait_send = 0; 396 | break; 397 | #ifdef __linux__ 398 | case 'P': 399 | params.protect_path = optarg; 400 | break; 401 | #endif 402 | case 0: 403 | break; 404 | 405 | case '?': 406 | reset_params(); 407 | return -1; 408 | 409 | default: 410 | LOG(LOG_S, "Unknown option: -%c", rez); 411 | reset_params(); 412 | return -1; 413 | } 414 | } 415 | if (invalid) { 416 | LOG(LOG_S, "invalid value: -%c %s", rez, optarg); 417 | reset_params(); 418 | return -1; 419 | } 420 | if (dp->hosts || dp->proto || dp->pf[0]) { 421 | dp = add((void *)¶ms.dp, 422 | ¶ms.dp_count, sizeof(struct desync_params)); 423 | if (!dp) { 424 | reset_params(); 425 | return -1; 426 | } 427 | } 428 | 429 | if (params.baddr.sin6_family != AF_INET6) { 430 | params.ipv6 = 0; 431 | } 432 | if (!params.def_ttl) { 433 | if ((params.def_ttl = get_default_ttl()) < 1) { 434 | reset_params(); 435 | return -1; 436 | } 437 | } 438 | params.mempool = mem_pool(0); 439 | if (!params.mempool) { 440 | uniperror("mem_pool"); 441 | reset_params(); 442 | return -1; 443 | } 444 | 445 | return 0; 446 | } 447 | -------------------------------------------------------------------------------- /app/src/main/cpp/utils.h: -------------------------------------------------------------------------------- 1 | extern struct params default_params; 2 | 3 | void reset_params(void); 4 | int parse_args(int argc, char **argv); 5 | -------------------------------------------------------------------------------- /app/src/main/ic_launcher-playstore.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dovecoteescapee/ByeDPIAndroid/43db0955de687d43f9473374dc7c5548eaa55359/app/src/main/ic_launcher-playstore.png -------------------------------------------------------------------------------- /app/src/main/java/io/github/dovecoteescapee/byedpi/activities/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package io.github.dovecoteescapee.byedpi.activities 2 | 3 | import android.Manifest 4 | import android.annotation.SuppressLint 5 | import android.content.BroadcastReceiver 6 | import android.content.Context 7 | import android.content.Intent 8 | import android.content.IntentFilter 9 | import android.content.pm.PackageManager 10 | import android.net.VpnService 11 | import android.os.Build 12 | import android.os.Bundle 13 | import android.util.Log 14 | import android.view.Menu 15 | import android.view.MenuItem 16 | import android.widget.Toast 17 | import androidx.activity.result.contract.ActivityResultContracts 18 | import androidx.appcompat.app.AppCompatActivity 19 | import androidx.core.content.ContextCompat 20 | import androidx.lifecycle.lifecycleScope 21 | import io.github.dovecoteescapee.byedpi.R 22 | import io.github.dovecoteescapee.byedpi.data.* 23 | import io.github.dovecoteescapee.byedpi.fragments.MainSettingsFragment 24 | import io.github.dovecoteescapee.byedpi.databinding.ActivityMainBinding 25 | import io.github.dovecoteescapee.byedpi.services.ServiceManager 26 | import io.github.dovecoteescapee.byedpi.services.appStatus 27 | import io.github.dovecoteescapee.byedpi.utility.* 28 | import kotlinx.coroutines.Dispatchers 29 | import kotlinx.coroutines.launch 30 | import java.io.IOException 31 | 32 | class MainActivity : AppCompatActivity() { 33 | private lateinit var binding: ActivityMainBinding 34 | 35 | companion object { 36 | private val TAG: String = MainActivity::class.java.simpleName 37 | 38 | private fun collectLogs(): String? = 39 | try { 40 | Runtime.getRuntime() 41 | .exec("logcat *:D -d") 42 | .inputStream.bufferedReader() 43 | .use { it.readText() } 44 | } catch (e: Exception) { 45 | Log.e(TAG, "Failed to collect logs", e) 46 | null 47 | } 48 | } 49 | 50 | private val vpnRegister = 51 | registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { 52 | if (it.resultCode == RESULT_OK) { 53 | ServiceManager.start(this, Mode.VPN) 54 | } else { 55 | Toast.makeText(this, R.string.vpn_permission_denied, Toast.LENGTH_SHORT).show() 56 | updateStatus() 57 | } 58 | } 59 | 60 | private val logsRegister = 61 | registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { 62 | lifecycleScope.launch(Dispatchers.IO) { 63 | val logs = collectLogs() 64 | 65 | if (logs == null) { 66 | Toast.makeText( 67 | this@MainActivity, 68 | R.string.logs_failed, 69 | Toast.LENGTH_SHORT 70 | ).show() 71 | } else { 72 | val uri = it.data?.data ?: run { 73 | Log.e(TAG, "No data in result") 74 | return@launch 75 | } 76 | contentResolver.openOutputStream(uri)?.use { 77 | try { 78 | it.write(logs.toByteArray()) 79 | } catch (e: IOException) { 80 | Log.e(TAG, "Failed to save logs", e) 81 | } 82 | } ?: run { 83 | Log.e(TAG, "Failed to open output stream") 84 | } 85 | } 86 | } 87 | } 88 | 89 | private val receiver = object : BroadcastReceiver() { 90 | override fun onReceive(context: Context?, intent: Intent?) { 91 | Log.d(TAG, "Received intent: ${intent?.action}") 92 | 93 | if (intent == null) { 94 | Log.w(TAG, "Received null intent") 95 | return 96 | } 97 | 98 | val senderOrd = intent.getIntExtra(SENDER, -1) 99 | val sender = Sender.entries.getOrNull(senderOrd) 100 | if (sender == null) { 101 | Log.w(TAG, "Received intent with unknown sender: $senderOrd") 102 | return 103 | } 104 | 105 | when (val action = intent.action) { 106 | STARTED_BROADCAST, 107 | STOPPED_BROADCAST -> updateStatus() 108 | 109 | FAILED_BROADCAST -> { 110 | Toast.makeText( 111 | context, 112 | getString(R.string.failed_to_start, sender.name), 113 | Toast.LENGTH_SHORT, 114 | ).show() 115 | updateStatus() 116 | } 117 | 118 | else -> Log.w(TAG, "Unknown action: $action") 119 | } 120 | } 121 | } 122 | 123 | override fun onCreate(savedInstanceState: Bundle?) { 124 | super.onCreate(savedInstanceState) 125 | 126 | binding = ActivityMainBinding.inflate(layoutInflater) 127 | setContentView(binding.root) 128 | 129 | val intentFilter = IntentFilter().apply { 130 | addAction(STARTED_BROADCAST) 131 | addAction(STOPPED_BROADCAST) 132 | addAction(FAILED_BROADCAST) 133 | } 134 | 135 | @SuppressLint("UnspecifiedRegisterReceiverFlag") 136 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { 137 | registerReceiver(receiver, intentFilter, RECEIVER_EXPORTED) 138 | } else { 139 | registerReceiver(receiver, intentFilter) 140 | } 141 | 142 | binding.statusButton.setOnClickListener { 143 | val (status, _) = appStatus 144 | when (status) { 145 | AppStatus.Halted -> start() 146 | AppStatus.Running -> stop() 147 | } 148 | } 149 | 150 | val theme = getPreferences() 151 | .getString("app_theme", null) 152 | MainSettingsFragment.setTheme(theme ?: "system") 153 | 154 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU && 155 | ContextCompat.checkSelfPermission( 156 | this, 157 | Manifest.permission.POST_NOTIFICATIONS 158 | ) != PackageManager.PERMISSION_GRANTED 159 | ) { 160 | requestPermissions(arrayOf(Manifest.permission.POST_NOTIFICATIONS), 1) 161 | } 162 | } 163 | 164 | override fun onResume() { 165 | super.onResume() 166 | updateStatus() 167 | } 168 | 169 | override fun onDestroy() { 170 | super.onDestroy() 171 | unregisterReceiver(receiver) 172 | } 173 | 174 | override fun onCreateOptionsMenu(menu: Menu?): Boolean { 175 | menuInflater.inflate(R.menu.menu_main, menu) 176 | return true 177 | } 178 | 179 | override fun onOptionsItemSelected(item: MenuItem): Boolean { 180 | val (status, _) = appStatus 181 | 182 | return when (item.itemId) { 183 | R.id.action_settings -> { 184 | if (status == AppStatus.Halted) { 185 | val intent = Intent(this, SettingsActivity::class.java) 186 | startActivity(intent) 187 | } else { 188 | Toast.makeText(this, R.string.settings_unavailable, Toast.LENGTH_SHORT) 189 | .show() 190 | } 191 | true 192 | } 193 | 194 | R.id.action_save_logs -> { 195 | val intent = 196 | Intent(Intent.ACTION_CREATE_DOCUMENT).apply { 197 | addCategory(Intent.CATEGORY_OPENABLE) 198 | type = "text/plain" 199 | putExtra(Intent.EXTRA_TITLE, "byedpi.log") 200 | } 201 | 202 | logsRegister.launch(intent) 203 | true 204 | } 205 | 206 | else -> super.onOptionsItemSelected(item) 207 | } 208 | } 209 | 210 | private fun start() { 211 | when (getPreferences().mode()) { 212 | Mode.VPN -> { 213 | val intentPrepare = VpnService.prepare(this) 214 | if (intentPrepare != null) { 215 | vpnRegister.launch(intentPrepare) 216 | } else { 217 | ServiceManager.start(this, Mode.VPN) 218 | } 219 | } 220 | 221 | Mode.Proxy -> ServiceManager.start(this, Mode.Proxy) 222 | } 223 | } 224 | 225 | private fun stop() { 226 | ServiceManager.stop(this) 227 | } 228 | 229 | private fun updateStatus() { 230 | val (status, mode) = appStatus 231 | 232 | Log.i(TAG, "Updating status: $status, $mode") 233 | 234 | val preferences = getPreferences() 235 | val proxyIp = preferences.getStringNotNull("byedpi_proxy_ip", "127.0.0.1") 236 | val proxyPort = preferences.getStringNotNull("byedpi_proxy_port", "1080") 237 | binding.proxyAddress.text = getString(R.string.proxy_address, proxyIp, proxyPort) 238 | 239 | when (status) { 240 | AppStatus.Halted -> { 241 | when (preferences.mode()) { 242 | Mode.VPN -> { 243 | binding.statusText.setText(R.string.vpn_disconnected) 244 | binding.statusButton.setText(R.string.vpn_connect) 245 | } 246 | 247 | Mode.Proxy -> { 248 | binding.statusText.setText(R.string.proxy_down) 249 | binding.statusButton.setText(R.string.proxy_start) 250 | } 251 | } 252 | binding.statusButton.isEnabled = true 253 | } 254 | 255 | AppStatus.Running -> { 256 | when (mode) { 257 | Mode.VPN -> { 258 | binding.statusText.setText(R.string.vpn_connected) 259 | binding.statusButton.setText(R.string.vpn_disconnect) 260 | } 261 | 262 | Mode.Proxy -> { 263 | binding.statusText.setText(R.string.proxy_up) 264 | binding.statusButton.setText(R.string.proxy_stop) 265 | } 266 | } 267 | binding.statusButton.isEnabled = true 268 | } 269 | } 270 | } 271 | } -------------------------------------------------------------------------------- /app/src/main/java/io/github/dovecoteescapee/byedpi/activities/SettingsActivity.kt: -------------------------------------------------------------------------------- 1 | package io.github.dovecoteescapee.byedpi.activities 2 | 3 | import android.os.Bundle 4 | import android.view.Menu 5 | import android.view.MenuItem 6 | import androidx.appcompat.app.AppCompatActivity 7 | import androidx.fragment.app.FragmentManager 8 | import io.github.dovecoteescapee.byedpi.R 9 | import io.github.dovecoteescapee.byedpi.fragments.MainSettingsFragment 10 | import io.github.dovecoteescapee.byedpi.utility.getPreferences 11 | 12 | class SettingsActivity : AppCompatActivity() { 13 | override fun onCreate(savedInstanceState: Bundle?) { 14 | super.onCreate(savedInstanceState) 15 | setContentView(R.layout.activity_settings) 16 | 17 | supportFragmentManager 18 | .beginTransaction() 19 | .replace(R.id.settings, MainSettingsFragment()) 20 | .commit() 21 | 22 | supportActionBar?.setDisplayHomeAsUpEnabled(true) 23 | } 24 | 25 | override fun onCreateOptionsMenu(menu: Menu?): Boolean { 26 | menuInflater.inflate(R.menu.menu_settings, menu) 27 | return true 28 | } 29 | 30 | override fun onOptionsItemSelected(item: MenuItem): Boolean = when (item.itemId) { 31 | android.R.id.home -> { 32 | onBackPressedDispatcher.onBackPressed() 33 | true 34 | } 35 | 36 | R.id.action_reset_settings -> { 37 | getPreferences().edit().clear().apply() 38 | 39 | supportFragmentManager.popBackStack(null, FragmentManager.POP_BACK_STACK_INCLUSIVE) 40 | supportFragmentManager 41 | .beginTransaction() 42 | .replace(R.id.settings, MainSettingsFragment()) 43 | .commit() 44 | true 45 | } 46 | 47 | else -> super.onOptionsItemSelected(item) 48 | } 49 | } -------------------------------------------------------------------------------- /app/src/main/java/io/github/dovecoteescapee/byedpi/core/ByeDpiProxy.kt: -------------------------------------------------------------------------------- 1 | package io.github.dovecoteescapee.byedpi.core 2 | 3 | import kotlinx.coroutines.sync.Mutex 4 | import kotlinx.coroutines.sync.withLock 5 | 6 | class ByeDpiProxy { 7 | companion object { 8 | init { 9 | System.loadLibrary("byedpi") 10 | } 11 | } 12 | 13 | private val mutex = Mutex() 14 | private var fd = -1 15 | 16 | suspend fun startProxy(preferences: ByeDpiProxyPreferences): Int { 17 | val fd = createSocket(preferences) 18 | if (fd < 0) { 19 | return -1 // TODO: should be error code 20 | } 21 | return jniStartProxy(fd) 22 | } 23 | 24 | suspend fun stopProxy(): Int { 25 | mutex.withLock { 26 | if (fd < 0) { 27 | throw IllegalStateException("Proxy is not running") 28 | } 29 | 30 | val result = jniStopProxy(fd) 31 | if (result == 0) { 32 | fd = -1 33 | } 34 | return result 35 | } 36 | } 37 | 38 | private suspend fun createSocket(preferences: ByeDpiProxyPreferences): Int = 39 | mutex.withLock { 40 | if (fd >= 0) { 41 | throw IllegalStateException("Proxy is already running") 42 | } 43 | 44 | val fd = createSocketFromPreferences(preferences) 45 | if (fd < 0) { 46 | return -1 47 | } 48 | this.fd = fd 49 | fd 50 | } 51 | 52 | private fun createSocketFromPreferences(preferences: ByeDpiProxyPreferences) = 53 | when (preferences) { 54 | is ByeDpiProxyCmdPreferences -> jniCreateSocketWithCommandLine(preferences.args) 55 | 56 | is ByeDpiProxyUIPreferences -> jniCreateSocket( 57 | ip = preferences.ip, 58 | port = preferences.port, 59 | maxConnections = preferences.maxConnections, 60 | bufferSize = preferences.bufferSize, 61 | defaultTtl = preferences.defaultTtl, 62 | customTtl = preferences.customTtl, 63 | noDomain = preferences.noDomain, 64 | desyncHttp = preferences.desyncHttp, 65 | desyncHttps = preferences.desyncHttps, 66 | desyncUdp = preferences.desyncUdp, 67 | desyncMethod = preferences.desyncMethod.ordinal, 68 | splitPosition = preferences.splitPosition, 69 | splitAtHost = preferences.splitAtHost, 70 | fakeTtl = preferences.fakeTtl, 71 | fakeSni = preferences.fakeSni, 72 | oobChar = preferences.oobChar, 73 | hostMixedCase = preferences.hostMixedCase, 74 | domainMixedCase = preferences.domainMixedCase, 75 | hostRemoveSpaces = preferences.hostRemoveSpaces, 76 | tlsRecordSplit = preferences.tlsRecordSplit, 77 | tlsRecordSplitPosition = preferences.tlsRecordSplitPosition, 78 | tlsRecordSplitAtSni = preferences.tlsRecordSplitAtSni, 79 | hostsMode = preferences.hostsMode.ordinal, 80 | hosts = preferences.hosts, 81 | tcpFastOpen = preferences.tcpFastOpen, 82 | udpFakeCount = preferences.udpFakeCount, 83 | dropSack = preferences.dropSack, 84 | fakeOffset = preferences.fakeOffset, 85 | ) 86 | } 87 | 88 | private external fun jniCreateSocketWithCommandLine(args: Array): Int 89 | 90 | private external fun jniCreateSocket( 91 | ip: String, 92 | port: Int, 93 | maxConnections: Int, 94 | bufferSize: Int, 95 | defaultTtl: Int, 96 | customTtl: Boolean, 97 | noDomain: Boolean, 98 | desyncHttp: Boolean, 99 | desyncHttps: Boolean, 100 | desyncUdp: Boolean, 101 | desyncMethod: Int, 102 | splitPosition: Int, 103 | splitAtHost: Boolean, 104 | fakeTtl: Int, 105 | fakeSni: String, 106 | oobChar: Byte, 107 | hostMixedCase: Boolean, 108 | domainMixedCase: Boolean, 109 | hostRemoveSpaces: Boolean, 110 | tlsRecordSplit: Boolean, 111 | tlsRecordSplitPosition: Int, 112 | tlsRecordSplitAtSni: Boolean, 113 | hostsMode: Int, 114 | hosts: String?, 115 | tcpFastOpen: Boolean, 116 | udpFakeCount: Int, 117 | dropSack: Boolean, 118 | fakeOffset: Int, 119 | ): Int 120 | 121 | private external fun jniStartProxy(fd: Int): Int 122 | 123 | private external fun jniStopProxy(fd: Int): Int 124 | } -------------------------------------------------------------------------------- /app/src/main/java/io/github/dovecoteescapee/byedpi/core/ByeDpiProxyPreferences.kt: -------------------------------------------------------------------------------- 1 | package io.github.dovecoteescapee.byedpi.core 2 | 3 | import android.content.SharedPreferences 4 | import io.github.dovecoteescapee.byedpi.utility.getStringNotNull 5 | import io.github.dovecoteescapee.byedpi.utility.shellSplit 6 | 7 | sealed interface ByeDpiProxyPreferences { 8 | companion object { 9 | fun fromSharedPreferences(preferences: SharedPreferences): ByeDpiProxyPreferences = 10 | when (preferences.getBoolean("byedpi_enable_cmd_settings", false)) { 11 | true -> ByeDpiProxyCmdPreferences(preferences) 12 | false -> ByeDpiProxyUIPreferences(preferences) 13 | } 14 | } 15 | } 16 | 17 | class ByeDpiProxyCmdPreferences(val args: Array) : ByeDpiProxyPreferences { 18 | constructor(cmd: String) : this(cmdToArgs(cmd)) 19 | 20 | constructor(preferences: SharedPreferences) : this( 21 | preferences.getStringNotNull( 22 | "byedpi_cmd_args", 23 | "" 24 | ) 25 | ) 26 | 27 | companion object { 28 | private fun cmdToArgs(cmd: String): Array { 29 | val firstArgIndex = cmd.indexOf("-") 30 | val argsStr = (if (firstArgIndex > 0) cmd.substring(firstArgIndex) else cmd).trim() 31 | return arrayOf("ciadpi") + shellSplit(argsStr) 32 | } 33 | } 34 | } 35 | 36 | class ByeDpiProxyUIPreferences( 37 | ip: String? = null, 38 | port: Int? = null, 39 | maxConnections: Int? = null, 40 | bufferSize: Int? = null, 41 | defaultTtl: Int? = null, 42 | noDomain: Boolean? = null, 43 | desyncHttp: Boolean? = null, 44 | desyncHttps: Boolean? = null, 45 | desyncUdp: Boolean? = null, 46 | desyncMethod: DesyncMethod? = null, 47 | splitPosition: Int? = null, 48 | splitAtHost: Boolean? = null, 49 | fakeTtl: Int? = null, 50 | fakeSni: String? = null, 51 | oobChar: String? = null, 52 | hostMixedCase: Boolean? = null, 53 | domainMixedCase: Boolean? = null, 54 | hostRemoveSpaces: Boolean? = null, 55 | tlsRecordSplit: Boolean? = null, 56 | tlsRecordSplitPosition: Int? = null, 57 | tlsRecordSplitAtSni: Boolean? = null, 58 | hostsMode: HostsMode? = null, 59 | hosts: String? = null, 60 | tcpFastOpen: Boolean? = null, 61 | udpFakeCount: Int? = null, 62 | dropSack: Boolean? = null, 63 | byedpiFakeOffset: Int? = null, 64 | ) : ByeDpiProxyPreferences { 65 | val ip: String = ip ?: "127.0.0.1" 66 | val port: Int = port ?: 1080 67 | val maxConnections: Int = maxConnections ?: 512 68 | val bufferSize: Int = bufferSize ?: 16384 69 | val defaultTtl: Int = defaultTtl ?: 0 70 | val customTtl: Boolean = defaultTtl != null 71 | val noDomain: Boolean = noDomain ?: false 72 | val desyncHttp: Boolean = desyncHttp ?: true 73 | val desyncHttps: Boolean = desyncHttps ?: true 74 | val desyncUdp: Boolean = desyncUdp ?: false 75 | val desyncMethod: DesyncMethod = desyncMethod ?: DesyncMethod.Disorder 76 | val splitPosition: Int = splitPosition ?: 1 77 | val splitAtHost: Boolean = splitAtHost ?: false 78 | val fakeTtl: Int = fakeTtl ?: 8 79 | val fakeSni: String = fakeSni ?: "www.iana.org" 80 | val oobChar: Byte = (oobChar ?: "a")[0].code.toByte() 81 | val hostMixedCase: Boolean = hostMixedCase ?: false 82 | val domainMixedCase: Boolean = domainMixedCase ?: false 83 | val hostRemoveSpaces: Boolean = hostRemoveSpaces ?: false 84 | val tlsRecordSplit: Boolean = tlsRecordSplit ?: false 85 | val tlsRecordSplitPosition: Int = tlsRecordSplitPosition ?: 0 86 | val tlsRecordSplitAtSni: Boolean = tlsRecordSplitAtSni ?: false 87 | val hostsMode: HostsMode = 88 | if (hosts?.isBlank() != false) HostsMode.Disable 89 | else hostsMode ?: HostsMode.Disable 90 | val hosts: String? = 91 | if (this.hostsMode == HostsMode.Disable) null 92 | else hosts?.trim() 93 | val tcpFastOpen: Boolean = tcpFastOpen ?: false 94 | val udpFakeCount: Int = udpFakeCount ?: 0 95 | val dropSack: Boolean = dropSack ?: false 96 | val fakeOffset: Int = byedpiFakeOffset ?: 0 97 | 98 | constructor(preferences: SharedPreferences) : this( 99 | ip = preferences.getString("byedpi_proxy_ip", null), 100 | port = preferences.getString("byedpi_proxy_port", null)?.toIntOrNull(), 101 | maxConnections = preferences.getString("byedpi_max_connections", null)?.toIntOrNull(), 102 | bufferSize = preferences.getString("byedpi_buffer_size", null)?.toIntOrNull(), 103 | defaultTtl = preferences.getString("byedpi_default_ttl", null)?.toIntOrNull(), 104 | noDomain = preferences.getBoolean("byedpi_no_domain", false), 105 | desyncHttp = preferences.getBoolean("byedpi_desync_http", true), 106 | desyncHttps = preferences.getBoolean("byedpi_desync_https", true), 107 | desyncUdp = preferences.getBoolean("byedpi_desync_udp", false), 108 | desyncMethod = preferences.getString("byedpi_desync_method", null) 109 | ?.let { DesyncMethod.fromName(it) }, 110 | splitPosition = preferences.getString("byedpi_split_position", null)?.toIntOrNull(), 111 | splitAtHost = preferences.getBoolean("byedpi_split_at_host", false), 112 | fakeTtl = preferences.getString("byedpi_fake_ttl", null)?.toIntOrNull(), 113 | fakeSni = preferences.getString("byedpi_fake_sni", null), 114 | oobChar = preferences.getString("byedpi_oob_data", null), 115 | hostMixedCase = preferences.getBoolean("byedpi_host_mixed_case", false), 116 | domainMixedCase = preferences.getBoolean("byedpi_domain_mixed_case", false), 117 | hostRemoveSpaces = preferences.getBoolean("byedpi_host_remove_spaces", false), 118 | tlsRecordSplit = preferences.getBoolean("byedpi_tlsrec_enabled", false), 119 | tlsRecordSplitPosition = preferences.getString("byedpi_tlsrec_position", null) 120 | ?.toIntOrNull(), 121 | tlsRecordSplitAtSni = preferences.getBoolean("byedpi_tlsrec_at_sni", false), 122 | hostsMode = preferences.getString("byedpi_hosts_mode", null) 123 | ?.let { HostsMode.fromName(it) }, 124 | hosts = preferences.getString("byedpi_hosts_mode", null)?.let { 125 | when (HostsMode.fromName(it)) { 126 | HostsMode.Blacklist -> preferences.getString("byedpi_hosts_blacklist", null) 127 | HostsMode.Whitelist -> preferences.getString("byedpi_hosts_whitelist", null) 128 | else -> null 129 | } 130 | }, 131 | tcpFastOpen = preferences.getBoolean("byedpi_tcp_fast_open", false), 132 | udpFakeCount = preferences.getString("byedpi_udp_fake_count", null)?.toIntOrNull(), 133 | dropSack = preferences.getBoolean("byedpi_drop_sack", false), 134 | byedpiFakeOffset = preferences.getString("byedpi_fake_offset", null)?.toIntOrNull(), 135 | ) 136 | 137 | enum class DesyncMethod { 138 | None, 139 | Split, 140 | Disorder, 141 | Fake, 142 | OOB, 143 | DISOOB; 144 | 145 | companion object { 146 | fun fromName(name: String): DesyncMethod { 147 | return when (name) { 148 | "none" -> None 149 | "split" -> Split 150 | "disorder" -> Disorder 151 | "fake" -> Fake 152 | "oob" -> OOB 153 | "disoob" -> DISOOB 154 | else -> throw IllegalArgumentException("Unknown desync method: $name") 155 | } 156 | } 157 | } 158 | } 159 | 160 | enum class HostsMode { 161 | Disable, 162 | Blacklist, 163 | Whitelist; 164 | 165 | companion object { 166 | fun fromName(name: String): HostsMode { 167 | return when (name) { 168 | "disable" -> Disable 169 | "blacklist" -> Blacklist 170 | "whitelist" -> Whitelist 171 | else -> throw IllegalArgumentException("Unknown hosts mode: $name") 172 | } 173 | } 174 | } 175 | } 176 | } 177 | -------------------------------------------------------------------------------- /app/src/main/java/io/github/dovecoteescapee/byedpi/core/TProxyService.kt: -------------------------------------------------------------------------------- 1 | package io.github.dovecoteescapee.byedpi.core 2 | 3 | object TProxyService { 4 | init { 5 | System.loadLibrary("hev-socks5-tunnel") 6 | } 7 | 8 | @JvmStatic 9 | external fun TProxyStartService(configPath: String, fd: Int) 10 | 11 | @JvmStatic 12 | external fun TProxyStopService() 13 | 14 | @JvmStatic 15 | @Suppress("unused") 16 | external fun TProxyGetStats(): LongArray 17 | } 18 | -------------------------------------------------------------------------------- /app/src/main/java/io/github/dovecoteescapee/byedpi/data/Actions.kt: -------------------------------------------------------------------------------- 1 | package io.github.dovecoteescapee.byedpi.data 2 | 3 | const val START_ACTION = "start" 4 | const val STOP_ACTION = "stop" 5 | -------------------------------------------------------------------------------- /app/src/main/java/io/github/dovecoteescapee/byedpi/data/AppStatus.kt: -------------------------------------------------------------------------------- 1 | package io.github.dovecoteescapee.byedpi.data 2 | 3 | enum class AppStatus { 4 | Halted, 5 | Running, 6 | } 7 | 8 | enum class Mode { 9 | Proxy, 10 | VPN; 11 | 12 | companion object { 13 | fun fromSender(sender: Sender): Mode = when (sender) { 14 | Sender.Proxy -> Proxy 15 | Sender.VPN -> VPN 16 | } 17 | 18 | fun fromString(name: String): Mode = when (name) { 19 | "proxy" -> Proxy 20 | "vpn" -> VPN 21 | else -> throw IllegalArgumentException("Invalid mode: $name") 22 | } 23 | } 24 | } -------------------------------------------------------------------------------- /app/src/main/java/io/github/dovecoteescapee/byedpi/data/Broadcasts.kt: -------------------------------------------------------------------------------- 1 | package io.github.dovecoteescapee.byedpi.data 2 | 3 | const val STARTED_BROADCAST = "io.github.dovecoteescapee.byedpi.STARTED" 4 | const val STOPPED_BROADCAST = "io.github.dovecoteescapee.byedpi.STOPPED" 5 | const val FAILED_BROADCAST = "io.github.dovecoteescapee.byedpi.FAILED" 6 | 7 | const val SENDER = "sender" 8 | 9 | enum class Sender(val senderName: String) { 10 | Proxy("Proxy"), 11 | VPN("VPN") 12 | } 13 | -------------------------------------------------------------------------------- /app/src/main/java/io/github/dovecoteescapee/byedpi/data/ServiceStatus.kt: -------------------------------------------------------------------------------- 1 | package io.github.dovecoteescapee.byedpi.data 2 | 3 | enum class ServiceStatus { 4 | Disconnected, 5 | Connected, 6 | Failed, 7 | } 8 | -------------------------------------------------------------------------------- /app/src/main/java/io/github/dovecoteescapee/byedpi/fragments/ByeDpiCommandLineSettingsFragment.kt: -------------------------------------------------------------------------------- 1 | package io.github.dovecoteescapee.byedpi.fragments 2 | 3 | import android.os.Bundle 4 | import androidx.preference.PreferenceFragmentCompat 5 | import io.github.dovecoteescapee.byedpi.R 6 | 7 | class ByeDpiCommandLineSettingsFragment : PreferenceFragmentCompat() { 8 | override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { 9 | setPreferencesFromResource(R.xml.byedpi_cmd_settings, rootKey) 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /app/src/main/java/io/github/dovecoteescapee/byedpi/fragments/ByeDpiUISettingsFragment.kt: -------------------------------------------------------------------------------- 1 | package io.github.dovecoteescapee.byedpi.fragments 2 | 3 | import android.content.SharedPreferences 4 | import android.os.Bundle 5 | import androidx.preference.* 6 | import io.github.dovecoteescapee.byedpi.R 7 | import io.github.dovecoteescapee.byedpi.core.ByeDpiProxyUIPreferences 8 | import io.github.dovecoteescapee.byedpi.core.ByeDpiProxyUIPreferences.DesyncMethod.* 9 | import io.github.dovecoteescapee.byedpi.core.ByeDpiProxyUIPreferences.HostsMode.* 10 | import io.github.dovecoteescapee.byedpi.utility.* 11 | 12 | class ByeDpiUISettingsFragment : PreferenceFragmentCompat() { 13 | 14 | private val preferenceListener = 15 | SharedPreferences.OnSharedPreferenceChangeListener { _, _ -> 16 | updatePreferences() 17 | } 18 | 19 | override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { 20 | setPreferencesFromResource(R.xml.byedpi_ui_settings, rootKey) 21 | 22 | setEditTextPreferenceListener("byedpi_proxy_ip") { checkIp(it) } 23 | setEditTestPreferenceListenerPort("byedpi_proxy_port") 24 | setEditTestPreferenceListenerInt( 25 | "byedpi_max_connections", 26 | 1, 27 | Short.MAX_VALUE.toInt() 28 | ) 29 | setEditTestPreferenceListenerInt( 30 | "byedpi_buffer_size", 31 | 1, 32 | Int.MAX_VALUE / 4 33 | ) 34 | setEditTestPreferenceListenerInt("byedpi_default_ttl", 0, 255) 35 | setEditTestPreferenceListenerInt( 36 | "byedpi_split_position", 37 | Int.MIN_VALUE, 38 | Int.MAX_VALUE 39 | ) 40 | setEditTestPreferenceListenerInt("byedpi_fake_ttl", 1, 255) 41 | setEditTestPreferenceListenerInt( 42 | "byedpi_tlsrec_position", 43 | 2 * Short.MIN_VALUE, 44 | 2 * Short.MAX_VALUE, 45 | ) 46 | 47 | findPreferenceNotNull("byedpi_oob_data") 48 | .setOnBindEditTextListener { 49 | it.filters = arrayOf(android.text.InputFilter.LengthFilter(1)) 50 | } 51 | 52 | updatePreferences() 53 | } 54 | 55 | override fun onResume() { 56 | super.onResume() 57 | sharedPreferences?.registerOnSharedPreferenceChangeListener(preferenceListener) 58 | } 59 | 60 | override fun onPause() { 61 | super.onPause() 62 | sharedPreferences?.unregisterOnSharedPreferenceChangeListener(preferenceListener) 63 | } 64 | 65 | private fun updatePreferences() { 66 | val desyncMethod = 67 | findPreferenceNotNull("byedpi_desync_method") 68 | .value.let { ByeDpiProxyUIPreferences.DesyncMethod.fromName(it) } 69 | val hostsMode = findPreferenceNotNull("byedpi_hosts_mode") 70 | .value.let { ByeDpiProxyUIPreferences.HostsMode.fromName(it) } 71 | 72 | val hostsBlacklist = findPreferenceNotNull("byedpi_hosts_blacklist") 73 | val hostsWhitelist = findPreferenceNotNull("byedpi_hosts_whitelist") 74 | val desyncHttp = findPreferenceNotNull("byedpi_desync_http") 75 | val desyncHttps = findPreferenceNotNull("byedpi_desync_https") 76 | val desyncUdp = findPreferenceNotNull("byedpi_desync_udp") 77 | val splitPosition = findPreferenceNotNull("byedpi_split_position") 78 | val splitAtHost = findPreferenceNotNull("byedpi_split_at_host") 79 | val ttlFake = findPreferenceNotNull("byedpi_fake_ttl") 80 | val fakeSni = findPreferenceNotNull("byedpi_fake_sni") 81 | val fakeOffset = findPreferenceNotNull("byedpi_fake_offset") 82 | val oobChar = findPreferenceNotNull("byedpi_oob_data") 83 | val udpFakeCount = findPreferenceNotNull("byedpi_udp_fake_count") 84 | val hostMixedCase = findPreferenceNotNull("byedpi_host_mixed_case") 85 | val domainMixedCase = findPreferenceNotNull("byedpi_domain_mixed_case") 86 | val hostRemoveSpaces = 87 | findPreferenceNotNull("byedpi_host_remove_spaces") 88 | val splitTlsRec = findPreferenceNotNull("byedpi_tlsrec_enabled") 89 | val splitTlsRecPosition = 90 | findPreferenceNotNull("byedpi_tlsrec_position") 91 | val splitTlsRecAtSni = findPreferenceNotNull("byedpi_tlsrec_at_sni") 92 | 93 | hostsBlacklist.isVisible = hostsMode == Blacklist 94 | hostsWhitelist.isVisible = hostsMode == Whitelist 95 | 96 | val desyncEnabled = desyncMethod != None 97 | splitPosition.isVisible = desyncEnabled 98 | splitAtHost.isVisible = desyncEnabled 99 | 100 | val isFake = desyncMethod == Fake 101 | ttlFake.isVisible = isFake 102 | fakeSni.isVisible = isFake 103 | fakeOffset.isVisible = isFake 104 | 105 | val isOob = desyncMethod == OOB || desyncMethod == DISOOB 106 | oobChar.isVisible = isOob 107 | 108 | val desyncAllProtocols = 109 | !desyncHttp.isChecked && !desyncHttps.isChecked && !desyncUdp.isChecked 110 | 111 | val desyncHttpEnabled = desyncAllProtocols || desyncHttp.isChecked 112 | hostMixedCase.isEnabled = desyncHttpEnabled 113 | domainMixedCase.isEnabled = desyncHttpEnabled 114 | hostRemoveSpaces.isEnabled = desyncHttpEnabled 115 | 116 | val desyncUdpEnabled = desyncAllProtocols || desyncUdp.isChecked 117 | udpFakeCount.isEnabled = desyncUdpEnabled 118 | 119 | val desyncHttpsEnabled = desyncAllProtocols || desyncHttps.isChecked 120 | splitTlsRec.isEnabled = desyncHttpsEnabled 121 | val tlsRecEnabled = desyncHttpsEnabled && splitTlsRec.isChecked 122 | splitTlsRecPosition.isEnabled = tlsRecEnabled 123 | splitTlsRecAtSni.isEnabled = tlsRecEnabled 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /app/src/main/java/io/github/dovecoteescapee/byedpi/fragments/MainSettingsFragment.kt: -------------------------------------------------------------------------------- 1 | package io.github.dovecoteescapee.byedpi.fragments 2 | 3 | import android.content.SharedPreferences 4 | import android.os.Bundle 5 | import android.util.Log 6 | import androidx.appcompat.app.AppCompatDelegate 7 | import androidx.preference.* 8 | import io.github.dovecoteescapee.byedpi.BuildConfig 9 | import io.github.dovecoteescapee.byedpi.R 10 | import io.github.dovecoteescapee.byedpi.data.Mode 11 | import io.github.dovecoteescapee.byedpi.utility.* 12 | 13 | class MainSettingsFragment : PreferenceFragmentCompat() { 14 | companion object { 15 | private val TAG: String = MainSettingsFragment::class.java.simpleName 16 | 17 | fun setTheme(name: String) = 18 | themeByName(name)?.let { 19 | AppCompatDelegate.setDefaultNightMode(it) 20 | } ?: throw IllegalStateException("Invalid value for app_theme: $name") 21 | 22 | private fun themeByName(name: String): Int? = when (name) { 23 | "system" -> AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM 24 | "light" -> AppCompatDelegate.MODE_NIGHT_NO 25 | "dark" -> AppCompatDelegate.MODE_NIGHT_YES 26 | else -> { 27 | Log.w(TAG, "Invalid value for app_theme: $name") 28 | null 29 | } 30 | } 31 | } 32 | 33 | private val preferenceListener = 34 | SharedPreferences.OnSharedPreferenceChangeListener { _, _ -> 35 | updatePreferences() 36 | } 37 | 38 | override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { 39 | setPreferencesFromResource(R.xml.main_settings, rootKey) 40 | 41 | setEditTextPreferenceListener("dns_ip") { 42 | it.isBlank() || checkNotLocalIp(it) 43 | } 44 | 45 | findPreferenceNotNull("app_theme") 46 | .setOnPreferenceChangeListener { _, newValue -> 47 | setTheme(newValue as String) 48 | true 49 | } 50 | 51 | val switchCommandLineSettings = findPreferenceNotNull( 52 | "byedpi_enable_cmd_settings" 53 | ) 54 | val uiSettings = findPreferenceNotNull("byedpi_ui_settings") 55 | val cmdSettings = findPreferenceNotNull("byedpi_cmd_settings") 56 | 57 | val setByeDpiSettingsMode = { enable: Boolean -> 58 | uiSettings.isEnabled = !enable 59 | cmdSettings.isEnabled = enable 60 | } 61 | 62 | setByeDpiSettingsMode(switchCommandLineSettings.isChecked) 63 | 64 | switchCommandLineSettings.setOnPreferenceChangeListener { _, newValue -> 65 | setByeDpiSettingsMode(newValue as Boolean) 66 | true 67 | } 68 | 69 | findPreferenceNotNull("version").summary = BuildConfig.VERSION_NAME 70 | 71 | updatePreferences() 72 | } 73 | 74 | override fun onResume() { 75 | super.onResume() 76 | sharedPreferences?.registerOnSharedPreferenceChangeListener(preferenceListener) 77 | } 78 | 79 | override fun onPause() { 80 | super.onPause() 81 | sharedPreferences?.unregisterOnSharedPreferenceChangeListener(preferenceListener) 82 | } 83 | 84 | private fun updatePreferences() { 85 | val mode = findPreferenceNotNull("byedpi_mode") 86 | .value.let { Mode.fromString(it) } 87 | val dns = findPreferenceNotNull("dns_ip") 88 | val ipv6 = findPreferenceNotNull("ipv6_enable") 89 | 90 | when (mode) { 91 | Mode.VPN -> { 92 | dns.isVisible = true 93 | ipv6.isVisible = true 94 | } 95 | 96 | Mode.Proxy -> { 97 | dns.isVisible = false 98 | ipv6.isVisible = false 99 | } 100 | } 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /app/src/main/java/io/github/dovecoteescapee/byedpi/services/ByeDpiProxyService.kt: -------------------------------------------------------------------------------- 1 | package io.github.dovecoteescapee.byedpi.services 2 | 3 | import android.app.Notification 4 | import android.content.Intent 5 | import android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_SPECIAL_USE 6 | import android.os.Build 7 | import android.util.Log 8 | import androidx.lifecycle.LifecycleService 9 | import androidx.lifecycle.lifecycleScope 10 | import io.github.dovecoteescapee.byedpi.R 11 | import io.github.dovecoteescapee.byedpi.core.ByeDpiProxy 12 | import io.github.dovecoteescapee.byedpi.core.ByeDpiProxyPreferences 13 | import io.github.dovecoteescapee.byedpi.data.* 14 | import io.github.dovecoteescapee.byedpi.utility.* 15 | import kotlinx.coroutines.Dispatchers 16 | import kotlinx.coroutines.Job 17 | import kotlinx.coroutines.launch 18 | import kotlinx.coroutines.sync.Mutex 19 | import kotlinx.coroutines.sync.withLock 20 | import kotlinx.coroutines.withContext 21 | 22 | class ByeDpiProxyService : LifecycleService() { 23 | private var proxy = ByeDpiProxy() 24 | private var proxyJob: Job? = null 25 | private val mutex = Mutex() 26 | 27 | companion object { 28 | private val TAG: String = ByeDpiProxyService::class.java.simpleName 29 | private const val FOREGROUND_SERVICE_ID: Int = 2 30 | private const val NOTIFICATION_CHANNEL_ID: String = "ByeDPI Proxy" 31 | 32 | private var status: ServiceStatus = ServiceStatus.Disconnected 33 | } 34 | 35 | override fun onCreate() { 36 | super.onCreate() 37 | registerNotificationChannel( 38 | this, 39 | NOTIFICATION_CHANNEL_ID, 40 | R.string.proxy_channel_name, 41 | ) 42 | } 43 | 44 | override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { 45 | super.onStartCommand(intent, flags, startId) 46 | return when (val action = intent?.action) { 47 | START_ACTION -> { 48 | lifecycleScope.launch { start() } 49 | START_STICKY 50 | } 51 | 52 | STOP_ACTION -> { 53 | lifecycleScope.launch { stop() } 54 | START_NOT_STICKY 55 | } 56 | 57 | else -> { 58 | Log.w(TAG, "Unknown action: $action") 59 | START_NOT_STICKY 60 | } 61 | } 62 | } 63 | 64 | private suspend fun start() { 65 | Log.i(TAG, "Starting") 66 | 67 | if (status == ServiceStatus.Connected) { 68 | Log.w(TAG, "Proxy already connected") 69 | return 70 | } 71 | 72 | try { 73 | mutex.withLock { 74 | startProxy() 75 | } 76 | updateStatus(ServiceStatus.Connected) 77 | startForeground() 78 | } catch (e: Exception) { 79 | Log.e(TAG, "Failed to start proxy", e) 80 | updateStatus(ServiceStatus.Failed) 81 | stop() 82 | } 83 | } 84 | 85 | private fun startForeground() { 86 | val notification: Notification = createNotification() 87 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) { 88 | startForeground( 89 | FOREGROUND_SERVICE_ID, 90 | notification, 91 | FOREGROUND_SERVICE_TYPE_SPECIAL_USE, 92 | ) 93 | } else { 94 | startForeground(FOREGROUND_SERVICE_ID, notification) 95 | } 96 | } 97 | 98 | private suspend fun stop() { 99 | Log.i(TAG, "Stopping VPN") 100 | 101 | mutex.withLock { 102 | stopProxy() 103 | } 104 | updateStatus(ServiceStatus.Disconnected) 105 | stopSelf() 106 | } 107 | 108 | private suspend fun startProxy() { 109 | Log.i(TAG, "Starting proxy") 110 | 111 | if (proxyJob != null) { 112 | Log.w(TAG, "Proxy fields not null") 113 | throw IllegalStateException("Proxy fields not null") 114 | } 115 | 116 | proxy = ByeDpiProxy() 117 | val preferences = getByeDpiPreferences() 118 | 119 | proxyJob = lifecycleScope.launch(Dispatchers.IO) { 120 | val code = proxy.startProxy(preferences) 121 | 122 | withContext(Dispatchers.Main) { 123 | if (code != 0) { 124 | Log.e(TAG, "Proxy stopped with code $code") 125 | updateStatus(ServiceStatus.Failed) 126 | } else { 127 | updateStatus(ServiceStatus.Disconnected) 128 | } 129 | } 130 | } 131 | 132 | Log.i(TAG, "Proxy started") 133 | } 134 | 135 | private suspend fun stopProxy() { 136 | Log.i(TAG, "Stopping proxy") 137 | 138 | if (status == ServiceStatus.Disconnected) { 139 | Log.w(TAG, "Proxy already disconnected") 140 | return 141 | } 142 | 143 | proxy.stopProxy() 144 | proxyJob?.join() 145 | proxyJob = null 146 | 147 | Log.i(TAG, "Proxy stopped") 148 | } 149 | 150 | private fun getByeDpiPreferences(): ByeDpiProxyPreferences = 151 | ByeDpiProxyPreferences.fromSharedPreferences(getPreferences()) 152 | 153 | private fun updateStatus(newStatus: ServiceStatus) { 154 | Log.d(TAG, "Proxy status changed from $status to $newStatus") 155 | 156 | status = newStatus 157 | 158 | setStatus( 159 | when (newStatus) { 160 | ServiceStatus.Connected -> AppStatus.Running 161 | ServiceStatus.Disconnected, 162 | ServiceStatus.Failed -> { 163 | proxyJob = null 164 | AppStatus.Halted 165 | } 166 | }, 167 | Mode.Proxy 168 | ) 169 | 170 | val intent = Intent( 171 | when (newStatus) { 172 | ServiceStatus.Connected -> STARTED_BROADCAST 173 | ServiceStatus.Disconnected -> STOPPED_BROADCAST 174 | ServiceStatus.Failed -> FAILED_BROADCAST 175 | } 176 | ) 177 | intent.putExtra(SENDER, Sender.Proxy.ordinal) 178 | sendBroadcast(intent) 179 | } 180 | 181 | private fun createNotification(): Notification = 182 | createConnectionNotification( 183 | this, 184 | NOTIFICATION_CHANNEL_ID, 185 | R.string.notification_title, 186 | R.string.proxy_notification_content, 187 | ByeDpiProxyService::class.java, 188 | ) 189 | } 190 | -------------------------------------------------------------------------------- /app/src/main/java/io/github/dovecoteescapee/byedpi/services/ByeDpiStatus.kt: -------------------------------------------------------------------------------- 1 | package io.github.dovecoteescapee.byedpi.services 2 | 3 | import io.github.dovecoteescapee.byedpi.data.AppStatus 4 | import io.github.dovecoteescapee.byedpi.data.Mode 5 | 6 | var appStatus = AppStatus.Halted to Mode.VPN 7 | private set 8 | 9 | fun setStatus(status: AppStatus, mode: Mode) { 10 | appStatus = status to mode 11 | } 12 | -------------------------------------------------------------------------------- /app/src/main/java/io/github/dovecoteescapee/byedpi/services/ByeDpiVpnService.kt: -------------------------------------------------------------------------------- 1 | package io.github.dovecoteescapee.byedpi.services 2 | 3 | import android.app.Notification 4 | import android.app.PendingIntent 5 | import android.content.Intent 6 | import android.content.pm.ServiceInfo 7 | import android.os.Build 8 | import android.os.ParcelFileDescriptor 9 | import android.util.Log 10 | import androidx.lifecycle.lifecycleScope 11 | import io.github.dovecoteescapee.byedpi.R 12 | import io.github.dovecoteescapee.byedpi.activities.MainActivity 13 | import io.github.dovecoteescapee.byedpi.core.ByeDpiProxy 14 | import io.github.dovecoteescapee.byedpi.core.ByeDpiProxyPreferences 15 | import io.github.dovecoteescapee.byedpi.core.TProxyService 16 | import io.github.dovecoteescapee.byedpi.data.* 17 | import io.github.dovecoteescapee.byedpi.utility.* 18 | import kotlinx.coroutines.Dispatchers 19 | import kotlinx.coroutines.Job 20 | import kotlinx.coroutines.launch 21 | import kotlinx.coroutines.sync.Mutex 22 | import kotlinx.coroutines.sync.withLock 23 | import kotlinx.coroutines.withContext 24 | import java.io.File 25 | 26 | class ByeDpiVpnService : LifecycleVpnService() { 27 | private val byeDpiProxy = ByeDpiProxy() 28 | private var proxyJob: Job? = null 29 | private var tunFd: ParcelFileDescriptor? = null 30 | private val mutex = Mutex() 31 | private var stopping: Boolean = false 32 | 33 | companion object { 34 | private val TAG: String = ByeDpiVpnService::class.java.simpleName 35 | private const val FOREGROUND_SERVICE_ID: Int = 1 36 | private const val NOTIFICATION_CHANNEL_ID: String = "ByeDPIVpn" 37 | 38 | private var status: ServiceStatus = ServiceStatus.Disconnected 39 | } 40 | 41 | override fun onCreate() { 42 | super.onCreate() 43 | registerNotificationChannel( 44 | this, 45 | NOTIFICATION_CHANNEL_ID, 46 | R.string.vpn_channel_name, 47 | ) 48 | } 49 | 50 | override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { 51 | super.onStartCommand(intent, flags, startId) 52 | return when (val action = intent?.action) { 53 | START_ACTION -> { 54 | lifecycleScope.launch { start() } 55 | START_STICKY 56 | } 57 | 58 | STOP_ACTION -> { 59 | lifecycleScope.launch { stop() } 60 | START_NOT_STICKY 61 | } 62 | 63 | else -> { 64 | Log.w(TAG, "Unknown action: $action") 65 | START_NOT_STICKY 66 | } 67 | } 68 | } 69 | 70 | override fun onRevoke() { 71 | Log.i(TAG, "VPN revoked") 72 | lifecycleScope.launch { stop() } 73 | } 74 | 75 | private suspend fun start() { 76 | Log.i(TAG, "Starting") 77 | 78 | if (status == ServiceStatus.Connected) { 79 | Log.w(TAG, "VPN already connected") 80 | return 81 | } 82 | 83 | try { 84 | mutex.withLock { 85 | startProxy() 86 | startTun2Socks() 87 | } 88 | updateStatus(ServiceStatus.Connected) 89 | startForeground() 90 | } catch (e: Exception) { 91 | Log.e(TAG, "Failed to start VPN", e) 92 | updateStatus(ServiceStatus.Failed) 93 | stop() 94 | } 95 | } 96 | 97 | private fun startForeground() { 98 | val notification: Notification = createNotification() 99 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) { 100 | startForeground( 101 | FOREGROUND_SERVICE_ID, 102 | notification, 103 | ServiceInfo.FOREGROUND_SERVICE_TYPE_SPECIAL_USE, 104 | ) 105 | } else { 106 | startForeground(FOREGROUND_SERVICE_ID, notification) 107 | } 108 | } 109 | 110 | private suspend fun stop() { 111 | Log.i(TAG, "Stopping") 112 | 113 | mutex.withLock { 114 | stopping = true 115 | try { 116 | stopTun2Socks() 117 | stopProxy() 118 | } catch (e: Exception) { 119 | Log.e(TAG, "Failed to stop VPN", e) 120 | } finally { 121 | stopping = false 122 | } 123 | } 124 | 125 | updateStatus(ServiceStatus.Disconnected) 126 | stopSelf() 127 | } 128 | 129 | private suspend fun startProxy() { 130 | Log.i(TAG, "Starting proxy") 131 | 132 | if (proxyJob != null) { 133 | Log.w(TAG, "Proxy fields not null") 134 | throw IllegalStateException("Proxy fields not null") 135 | } 136 | 137 | val preferences = getByeDpiPreferences() 138 | 139 | proxyJob = lifecycleScope.launch(Dispatchers.IO) { 140 | val code = byeDpiProxy.startProxy(preferences) 141 | 142 | withContext(Dispatchers.Main) { 143 | if (code != 0) { 144 | Log.e(TAG, "Proxy stopped with code $code") 145 | updateStatus(ServiceStatus.Failed) 146 | } else { 147 | if (!stopping) { 148 | stop() 149 | updateStatus(ServiceStatus.Disconnected) 150 | } 151 | } 152 | } 153 | } 154 | 155 | Log.i(TAG, "Proxy started") 156 | } 157 | 158 | private suspend fun stopProxy() { 159 | Log.i(TAG, "Stopping proxy") 160 | 161 | if (status == ServiceStatus.Disconnected) { 162 | Log.w(TAG, "Proxy already disconnected") 163 | return 164 | } 165 | 166 | byeDpiProxy.stopProxy() 167 | proxyJob?.join() ?: throw IllegalStateException("ProxyJob field null") 168 | proxyJob = null 169 | 170 | Log.i(TAG, "Proxy stopped") 171 | } 172 | 173 | private fun startTun2Socks() { 174 | Log.i(TAG, "Starting tun2socks") 175 | 176 | if (tunFd != null) { 177 | throw IllegalStateException("VPN field not null") 178 | } 179 | 180 | val sharedPreferences = getPreferences() 181 | val port = sharedPreferences.getString("byedpi_proxy_port", null)?.toInt() ?: 1080 182 | val dns = sharedPreferences.getStringNotNull("dns_ip", "1.1.1.1") 183 | val ipv6 = sharedPreferences.getBoolean("ipv6_enable", false) 184 | 185 | val tun2socksConfig = """ 186 | | misc: 187 | | task-stack-size: 81920 188 | | socks5: 189 | | mtu: 8500 190 | | address: 127.0.0.1 191 | | port: $port 192 | | udp: udp 193 | """.trimMargin("| ") 194 | 195 | val configPath = try { 196 | File.createTempFile("config", "tmp", cacheDir).apply { 197 | writeText(tun2socksConfig) 198 | } 199 | } catch (e: Exception) { 200 | Log.e(TAG, "Failed to create config file", e) 201 | throw e 202 | } 203 | 204 | val fd = createBuilder(dns, ipv6).establish() 205 | ?: throw IllegalStateException("VPN connection failed") 206 | 207 | this.tunFd = fd 208 | 209 | TProxyService.TProxyStartService(configPath.absolutePath, fd.fd) 210 | 211 | Log.i(TAG, "Tun2Socks started") 212 | } 213 | 214 | private fun stopTun2Socks() { 215 | Log.i(TAG, "Stopping tun2socks") 216 | 217 | TProxyService.TProxyStopService() 218 | 219 | try { 220 | File(cacheDir, "config.tmp").delete() 221 | } catch (e: SecurityException) { 222 | Log.e(TAG, "Failed to delete config file", e) 223 | } 224 | 225 | tunFd?.close() ?: Log.w(TAG, "VPN not running") 226 | tunFd = null 227 | 228 | Log.i(TAG, "Tun2socks stopped") 229 | } 230 | 231 | private fun getByeDpiPreferences(): ByeDpiProxyPreferences = 232 | ByeDpiProxyPreferences.fromSharedPreferences(getPreferences()) 233 | 234 | private fun updateStatus(newStatus: ServiceStatus) { 235 | Log.d(TAG, "VPN status changed from $status to $newStatus") 236 | 237 | status = newStatus 238 | 239 | setStatus( 240 | when (newStatus) { 241 | ServiceStatus.Connected -> AppStatus.Running 242 | 243 | ServiceStatus.Disconnected, 244 | ServiceStatus.Failed -> { 245 | proxyJob = null 246 | AppStatus.Halted 247 | } 248 | }, 249 | Mode.VPN 250 | ) 251 | 252 | val intent = Intent( 253 | when (newStatus) { 254 | ServiceStatus.Connected -> STARTED_BROADCAST 255 | ServiceStatus.Disconnected -> STOPPED_BROADCAST 256 | ServiceStatus.Failed -> FAILED_BROADCAST 257 | } 258 | ) 259 | intent.putExtra(SENDER, Sender.VPN.ordinal) 260 | sendBroadcast(intent) 261 | } 262 | 263 | private fun createNotification(): Notification = 264 | createConnectionNotification( 265 | this, 266 | NOTIFICATION_CHANNEL_ID, 267 | R.string.notification_title, 268 | R.string.vpn_notification_content, 269 | ByeDpiVpnService::class.java, 270 | ) 271 | 272 | private fun createBuilder(dns: String, ipv6: Boolean): Builder { 273 | Log.d(TAG, "DNS: $dns") 274 | val builder = Builder() 275 | builder.setSession("ByeDPI") 276 | builder.setConfigureIntent( 277 | PendingIntent.getActivity( 278 | this, 279 | 0, 280 | Intent(this, MainActivity::class.java), 281 | PendingIntent.FLAG_IMMUTABLE, 282 | ) 283 | ) 284 | 285 | builder.addAddress("10.10.10.10", 32) 286 | .addRoute("0.0.0.0", 0) 287 | 288 | if (ipv6) { 289 | builder.addAddress("fd00::1", 128) 290 | .addRoute("::", 0) 291 | } 292 | 293 | if (dns.isNotBlank()) { 294 | builder.addDnsServer(dns) 295 | } 296 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { 297 | builder.setMetered(false) 298 | } 299 | 300 | builder.addDisallowedApplication(applicationContext.packageName) 301 | 302 | return builder 303 | } 304 | } 305 | -------------------------------------------------------------------------------- /app/src/main/java/io/github/dovecoteescapee/byedpi/services/LifecycleVpnService.kt: -------------------------------------------------------------------------------- 1 | package io.github.dovecoteescapee.byedpi.services 2 | 3 | import android.content.Intent 4 | import android.net.VpnService 5 | import android.os.IBinder 6 | import androidx.annotation.CallSuper 7 | import androidx.lifecycle.Lifecycle 8 | import androidx.lifecycle.LifecycleOwner 9 | import androidx.lifecycle.ServiceLifecycleDispatcher 10 | 11 | /** 12 | * Based on [androidx.lifecycle.LifecycleService] 13 | */ 14 | open class LifecycleVpnService : VpnService(), LifecycleOwner { 15 | @Suppress("LeakingThis") 16 | private val dispatcher = ServiceLifecycleDispatcher(this) 17 | 18 | @CallSuper 19 | override fun onCreate() { 20 | dispatcher.onServicePreSuperOnCreate() 21 | super.onCreate() 22 | } 23 | 24 | @CallSuper 25 | override fun onBind(intent: Intent): IBinder? { 26 | dispatcher.onServicePreSuperOnBind() 27 | return super.onBind(intent) 28 | } 29 | 30 | @Deprecated("Deprecated in Java") 31 | @CallSuper 32 | override fun onStart(intent: Intent?, startId: Int) { 33 | dispatcher.onServicePreSuperOnStart() 34 | @Suppress("DEPRECATION") 35 | super.onStart(intent, startId) 36 | } 37 | 38 | // this method is added only to annotate it with @CallSuper. 39 | // In usual Service, super.onStartCommand is no-op, but in LifecycleService 40 | // it results in dispatcher.onServicePreSuperOnStart() call, because 41 | // super.onStartCommand calls onStart(). 42 | @CallSuper 43 | override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { 44 | return super.onStartCommand(intent, flags, startId) 45 | } 46 | 47 | @CallSuper 48 | override fun onDestroy() { 49 | dispatcher.onServicePreSuperOnDestroy() 50 | super.onDestroy() 51 | } 52 | 53 | override val lifecycle: Lifecycle 54 | get() = dispatcher.lifecycle 55 | } 56 | -------------------------------------------------------------------------------- /app/src/main/java/io/github/dovecoteescapee/byedpi/services/QuickTileService.kt: -------------------------------------------------------------------------------- 1 | package io.github.dovecoteescapee.byedpi.services 2 | 3 | import android.app.PendingIntent 4 | import android.content.BroadcastReceiver 5 | import android.content.Context 6 | import android.content.Intent 7 | import android.content.IntentFilter 8 | import android.net.VpnService 9 | import android.os.Build 10 | import android.service.quicksettings.Tile 11 | import android.service.quicksettings.TileService 12 | import android.util.Log 13 | import android.widget.Toast 14 | import androidx.annotation.RequiresApi 15 | import androidx.core.content.ContextCompat 16 | import androidx.core.service.quicksettings.PendingIntentActivityWrapper 17 | import androidx.core.service.quicksettings.TileServiceCompat 18 | import io.github.dovecoteescapee.byedpi.R 19 | import io.github.dovecoteescapee.byedpi.activities.MainActivity 20 | import io.github.dovecoteescapee.byedpi.data.* 21 | import io.github.dovecoteescapee.byedpi.utility.getPreferences 22 | import io.github.dovecoteescapee.byedpi.utility.mode 23 | 24 | 25 | @RequiresApi(Build.VERSION_CODES.N) 26 | class QuickTileService : TileService() { 27 | 28 | companion object { 29 | private val TAG: String = QuickTileService::class.java.simpleName 30 | } 31 | 32 | private val receiver: BroadcastReceiver = object : BroadcastReceiver() { 33 | override fun onReceive(context: Context, intent: Intent) { 34 | val senderOrd = intent.getIntExtra(SENDER, -1) 35 | val sender = Sender.entries.getOrNull(senderOrd) 36 | if (sender == null) { 37 | Log.w(TAG, "Received intent with unknown sender: $senderOrd") 38 | return 39 | } 40 | 41 | when (val action = intent.action) { 42 | STARTED_BROADCAST, 43 | STOPPED_BROADCAST -> updateStatus() 44 | 45 | FAILED_BROADCAST -> { 46 | Toast.makeText( 47 | context, 48 | getString(R.string.failed_to_start, sender.name), 49 | Toast.LENGTH_SHORT, 50 | ).show() 51 | updateStatus() 52 | } 53 | 54 | else -> Log.w(TAG, "Unknown action: $action") 55 | } 56 | } 57 | } 58 | 59 | override fun onStartListening() { 60 | updateStatus() 61 | ContextCompat.registerReceiver( 62 | this, 63 | receiver, 64 | IntentFilter().apply { 65 | addAction(STARTED_BROADCAST) 66 | addAction(STOPPED_BROADCAST) 67 | addAction(FAILED_BROADCAST) 68 | }, 69 | ContextCompat.RECEIVER_EXPORTED, 70 | ) 71 | } 72 | 73 | override fun onStopListening() { 74 | unregisterReceiver(receiver) 75 | } 76 | 77 | private fun launchActivity() { 78 | TileServiceCompat.startActivityAndCollapse( 79 | this, PendingIntentActivityWrapper( 80 | this, 0, Intent(this, MainActivity::class.java), 81 | PendingIntent.FLAG_UPDATE_CURRENT, false 82 | ) 83 | ) 84 | } 85 | 86 | override fun onClick() { 87 | if (qsTile.state == Tile.STATE_UNAVAILABLE) { 88 | return 89 | } 90 | 91 | unlockAndRun(this::handleClick) 92 | } 93 | 94 | private fun setState(newState: Int) { 95 | qsTile.apply { 96 | state = newState 97 | updateTile() 98 | } 99 | } 100 | 101 | private fun updateStatus() { 102 | val (status) = appStatus 103 | setState(if (status == AppStatus.Halted) Tile.STATE_INACTIVE else Tile.STATE_ACTIVE) 104 | } 105 | 106 | private fun handleClick() { 107 | setState(Tile.STATE_ACTIVE) 108 | setState(Tile.STATE_UNAVAILABLE) 109 | 110 | val (status) = appStatus 111 | when (status) { 112 | AppStatus.Halted -> { 113 | val mode = getPreferences().mode() 114 | 115 | if (mode == Mode.VPN && VpnService.prepare(this) != null) { 116 | updateStatus() 117 | launchActivity() 118 | return 119 | } 120 | 121 | ServiceManager.start(this, mode) 122 | } 123 | 124 | AppStatus.Running -> ServiceManager.stop(this) 125 | } 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /app/src/main/java/io/github/dovecoteescapee/byedpi/services/ServiceManager.kt: -------------------------------------------------------------------------------- 1 | package io.github.dovecoteescapee.byedpi.services 2 | 3 | import android.content.Context 4 | import android.content.Intent 5 | import android.util.Log 6 | import androidx.core.content.ContextCompat 7 | import io.github.dovecoteescapee.byedpi.data.Mode 8 | import io.github.dovecoteescapee.byedpi.data.START_ACTION 9 | import io.github.dovecoteescapee.byedpi.data.STOP_ACTION 10 | 11 | object ServiceManager { 12 | private val TAG: String = ServiceManager::class.java.simpleName 13 | 14 | fun start(context: Context, mode: Mode) { 15 | when (mode) { 16 | Mode.VPN -> { 17 | Log.i(TAG, "Starting VPN") 18 | val intent = Intent(context, ByeDpiVpnService::class.java) 19 | intent.action = START_ACTION 20 | ContextCompat.startForegroundService(context, intent) 21 | } 22 | 23 | Mode.Proxy -> { 24 | Log.i(TAG, "Starting proxy") 25 | val intent = Intent(context, ByeDpiProxyService::class.java) 26 | intent.action = START_ACTION 27 | ContextCompat.startForegroundService(context, intent) 28 | } 29 | } 30 | } 31 | 32 | fun stop(context: Context) { 33 | val (_, mode) = appStatus 34 | when (mode) { 35 | Mode.VPN -> { 36 | Log.i(TAG, "Stopping VPN") 37 | val intent = Intent(context, ByeDpiVpnService::class.java) 38 | intent.action = STOP_ACTION 39 | ContextCompat.startForegroundService(context, intent) 40 | } 41 | 42 | Mode.Proxy -> { 43 | Log.i(TAG, "Stopping proxy") 44 | val intent = Intent(context, ByeDpiProxyService::class.java) 45 | intent.action = STOP_ACTION 46 | ContextCompat.startForegroundService(context, intent) 47 | } 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /app/src/main/java/io/github/dovecoteescapee/byedpi/utility/ArgumentsUtils.kt: -------------------------------------------------------------------------------- 1 | package io.github.dovecoteescapee.byedpi.utility 2 | 3 | // Based on https://gist.github.com/raymyers/8077031 4 | fun shellSplit(string: CharSequence): List { 5 | val tokens: MutableList = ArrayList() 6 | var escaping = false 7 | var quoteChar = ' ' 8 | var quoting = false 9 | var lastCloseQuoteIndex = Int.MIN_VALUE 10 | var current = StringBuilder() 11 | 12 | for (i in string.indices) { 13 | val c = string[i] 14 | 15 | if (escaping) { 16 | current.append(c) 17 | escaping = false 18 | } else if (c == '\\' && !(quoting && quoteChar == '\'')) { 19 | escaping = true 20 | } else if (quoting && c == quoteChar) { 21 | quoting = false 22 | lastCloseQuoteIndex = i 23 | } else if (!quoting && (c == '\'' || c == '"')) { 24 | quoting = true 25 | quoteChar = c 26 | } else if (!quoting && Character.isWhitespace(c)) { 27 | if (current.isNotEmpty() || lastCloseQuoteIndex == i - 1) { 28 | tokens.add(current.toString()) 29 | current = StringBuilder() 30 | } 31 | } else { 32 | current.append(c) 33 | } 34 | } 35 | 36 | if (current.isNotEmpty() || lastCloseQuoteIndex == string.length - 1) { 37 | tokens.add(current.toString()) 38 | } 39 | 40 | return tokens 41 | } 42 | -------------------------------------------------------------------------------- /app/src/main/java/io/github/dovecoteescapee/byedpi/utility/NotificationUtils.kt: -------------------------------------------------------------------------------- 1 | package io.github.dovecoteescapee.byedpi.utility 2 | 3 | import android.app.Notification 4 | import android.app.NotificationChannel 5 | import android.app.NotificationManager 6 | import android.app.PendingIntent 7 | import android.content.Context 8 | import android.content.Intent 9 | import android.os.Build 10 | import androidx.annotation.StringRes 11 | import androidx.core.app.NotificationCompat 12 | import io.github.dovecoteescapee.byedpi.R 13 | import io.github.dovecoteescapee.byedpi.activities.MainActivity 14 | import io.github.dovecoteescapee.byedpi.data.STOP_ACTION 15 | 16 | fun registerNotificationChannel(context: Context, id: String, @StringRes name: Int) { 17 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { 18 | val manager = context.getSystemService(NotificationManager::class.java) ?: return 19 | 20 | val channel = NotificationChannel( 21 | id, 22 | context.getString(name), 23 | NotificationManager.IMPORTANCE_DEFAULT 24 | ) 25 | channel.enableLights(false) 26 | channel.enableVibration(false) 27 | channel.setShowBadge(false) 28 | 29 | manager.createNotificationChannel(channel) 30 | } 31 | } 32 | 33 | fun createConnectionNotification( 34 | context: Context, 35 | channelId: String, 36 | @StringRes title: Int, 37 | @StringRes content: Int, 38 | service: Class<*>, 39 | ): Notification = 40 | NotificationCompat.Builder(context, channelId) 41 | .setSmallIcon(R.drawable.ic_notification) 42 | .setSilent(true) 43 | .setContentTitle(context.getString(title)) 44 | .setContentText(context.getString(content)) 45 | .addAction(0, "Stop", 46 | PendingIntent.getService( 47 | context, 48 | 0, 49 | Intent(context, service).setAction(STOP_ACTION), 50 | PendingIntent.FLAG_IMMUTABLE, 51 | ) 52 | ) 53 | .setContentIntent( 54 | PendingIntent.getActivity( 55 | context, 56 | 0, 57 | Intent(context, MainActivity::class.java), 58 | PendingIntent.FLAG_IMMUTABLE, 59 | ) 60 | ) 61 | .build() 62 | -------------------------------------------------------------------------------- /app/src/main/java/io/github/dovecoteescapee/byedpi/utility/PreferencesUtils.kt: -------------------------------------------------------------------------------- 1 | package io.github.dovecoteescapee.byedpi.utility 2 | 3 | import android.content.Context 4 | import android.content.SharedPreferences 5 | import androidx.preference.Preference 6 | import androidx.preference.PreferenceFragmentCompat 7 | import androidx.preference.PreferenceManager 8 | import io.github.dovecoteescapee.byedpi.data.Mode 9 | 10 | val PreferenceFragmentCompat.sharedPreferences 11 | get() = preferenceScreen.sharedPreferences 12 | 13 | fun Context.getPreferences(): SharedPreferences = 14 | PreferenceManager.getDefaultSharedPreferences(this) 15 | 16 | fun SharedPreferences.getStringNotNull(key: String, defValue: String): String = 17 | getString(key, defValue) ?: defValue 18 | 19 | fun SharedPreferences.mode(): Mode = 20 | Mode.fromString(getStringNotNull("byedpi_mode", "vpn")) 21 | 22 | fun PreferenceFragmentCompat.findPreferenceNotNull(key: CharSequence): T = 23 | findPreference(key) ?: throw IllegalStateException("Preference $key not found") 24 | -------------------------------------------------------------------------------- /app/src/main/java/io/github/dovecoteescapee/byedpi/utility/ValidateUtils.kt: -------------------------------------------------------------------------------- 1 | package io.github.dovecoteescapee.byedpi.utility 2 | 3 | import android.net.InetAddresses 4 | import android.os.Build 5 | import android.util.Log 6 | import android.widget.Toast 7 | import androidx.preference.EditTextPreference 8 | import androidx.preference.PreferenceFragmentCompat 9 | 10 | private const val TAG = "ValidateUtils" 11 | 12 | fun checkIp(ip: String): Boolean = 13 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { 14 | InetAddresses.isNumericAddress(ip) 15 | } else { 16 | // This pattern doesn't not support IPv6 17 | // @Suppress("DEPRECATION") 18 | // Patterns.IP_ADDRESS.matcher(ip).matches() 19 | true 20 | } 21 | 22 | fun checkNotLocalIp(ip: String): Boolean = 23 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { 24 | InetAddresses.isNumericAddress(ip) && InetAddresses.parseNumericAddress(ip).let { 25 | !it.isAnyLocalAddress && !it.isLoopbackAddress 26 | } 27 | } else { 28 | // This pattern doesn't not support IPv6 29 | // @Suppress("DEPRECATION") 30 | // Patterns.IP_ADDRESS.matcher(ip).matches() 31 | true 32 | } 33 | 34 | fun PreferenceFragmentCompat.setEditTestPreferenceListenerPort(key: String) { 35 | setEditTestPreferenceListenerInt(key, 1, 65535) 36 | } 37 | 38 | fun PreferenceFragmentCompat.setEditTestPreferenceListenerInt( 39 | key: String, 40 | min: Int = Int.MIN_VALUE, 41 | max: Int = Int.MAX_VALUE 42 | ) { 43 | setEditTextPreferenceListener(key) { value -> 44 | value.toIntOrNull()?.let { it in min..max } ?: false 45 | } 46 | } 47 | 48 | fun PreferenceFragmentCompat.setEditTextPreferenceListener( 49 | key: String, 50 | check: (String) -> Boolean 51 | ) { 52 | findPreferenceNotNull(key) 53 | .setOnPreferenceChangeListener { preference, newValue -> 54 | when (newValue) { 55 | is String -> { 56 | val valid = check(newValue) 57 | if (!valid) { 58 | Toast.makeText( 59 | requireContext(), 60 | "Invalid value for ${preference.title}: $newValue", 61 | Toast.LENGTH_SHORT 62 | ).show() 63 | } 64 | valid 65 | } 66 | 67 | else -> { 68 | Log.w( 69 | TAG, 70 | "Invalid type for ${preference.key}: " + 71 | "$newValue has type ${newValue::class.java}" 72 | ) 73 | false 74 | } 75 | } 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /app/src/main/jni/Android.mk: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2023 The Android Open Source Project 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | # 15 | 16 | include $(call all-subdir-makefiles) 17 | -------------------------------------------------------------------------------- /app/src/main/jni/Application.mk: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2023 The Android Open Source Project 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | # 15 | 16 | APP_OPTIM := release 17 | APP_PLATFORM := android-21 18 | APP_ABI := armeabi-v7a arm64-v8a x86 x86_64 19 | APP_CFLAGS := -O3 -DPKGNAME=io/github/dovecoteescapee/byedpi/core 20 | APP_CPPFLAGS := -O3 -std=c++11 21 | NDK_TOOLCHAIN_VERSION := clang 22 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/baseline_settings_24.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_github_36.xml: -------------------------------------------------------------------------------- 1 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_notification.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 12 | 15 | 18 | 21 | 24 | 25 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_main.xml: -------------------------------------------------------------------------------- 1 | 2 | 9 | 10 | 23 | 24 | 32 | 33 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_settings.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/menu/menu_main.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 13 | 14 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /app/src/main/res/menu/menu_settings.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dovecoteescapee/ByeDPIAndroid/43db0955de687d43f9473374dc7c5548eaa55359/app/src/main/res/mipmap-hdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher_foreground.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dovecoteescapee/ByeDPIAndroid/43db0955de687d43f9473374dc7c5548eaa55359/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dovecoteescapee/ByeDPIAndroid/43db0955de687d43f9473374dc7c5548eaa55359/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dovecoteescapee/ByeDPIAndroid/43db0955de687d43f9473374dc7c5548eaa55359/app/src/main/res/mipmap-mdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher_foreground.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dovecoteescapee/ByeDPIAndroid/43db0955de687d43f9473374dc7c5548eaa55359/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dovecoteescapee/ByeDPIAndroid/43db0955de687d43f9473374dc7c5548eaa55359/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dovecoteescapee/ByeDPIAndroid/43db0955de687d43f9473374dc7c5548eaa55359/app/src/main/res/mipmap-xhdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dovecoteescapee/ByeDPIAndroid/43db0955de687d43f9473374dc7c5548eaa55359/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dovecoteescapee/ByeDPIAndroid/43db0955de687d43f9473374dc7c5548eaa55359/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dovecoteescapee/ByeDPIAndroid/43db0955de687d43f9473374dc7c5548eaa55359/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dovecoteescapee/ByeDPIAndroid/43db0955de687d43f9473374dc7c5548eaa55359/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dovecoteescapee/ByeDPIAndroid/43db0955de687d43f9473374dc7c5548eaa55359/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dovecoteescapee/ByeDPIAndroid/43db0955de687d43f9473374dc7c5548eaa55359/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dovecoteescapee/ByeDPIAndroid/43db0955de687d43f9473374dc7c5548eaa55359/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dovecoteescapee/ByeDPIAndroid/43db0955de687d43f9473374dc7c5548eaa55359/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /app/src/main/res/values-night/themes.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 13 | 14 | -------------------------------------------------------------------------------- /app/src/main/res/values/arrays.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | System 5 | Light 6 | Dark 7 | 8 | 9 | system 10 | light 11 | dark 12 | 13 | 14 | 15 | VPN 16 | Proxy 17 | 18 | 19 | vpn 20 | proxy 21 | 22 | 23 | 24 | None 25 | Split 26 | Disorder 27 | Fake 28 | Out-of-band 29 | Disordered out-of-band 30 | 31 | 32 | none 33 | split 34 | disorder 35 | fake 36 | oob 37 | disoob 38 | 39 | 40 | 41 | Disable 42 | Blacklist 43 | Whitelist 44 | 45 | 46 | disable 47 | blacklist 48 | whitelist 49 | 50 | 51 | -------------------------------------------------------------------------------- /app/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #FF000000 4 | #FFFFFFFF 5 | -------------------------------------------------------------------------------- /app/src/main/res/values/ic_launcher_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #061A23 4 | -------------------------------------------------------------------------------- /app/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | https://github.com/hufrea/byedpi/blob/v0.13/README.md 3 | ByeDPI 4 | Connect 5 | Disconnect 6 | Connected 7 | Disconnected 8 | Proxy is up 9 | Proxy is down 10 | Start 11 | Stop 12 | Settings 13 | VPN permission denied 14 | Please stop the VPN service before changing settings 15 | Settings 16 | Save logs 17 | Reset 18 | Failed to collect logs 19 | %1$s:%2$s 20 | Theme 21 | Mode 22 | DNS 23 | Listen address 24 | Port 25 | Maximum number of connections 26 | Buffer size 27 | Default TTL 28 | No domain 29 | Desync method 30 | Split position 31 | Split at host 32 | TTL of fake packets 33 | Host mixed case 34 | Domain mixed case 35 | Host remove spaces 36 | Split TLS record 37 | TLS record split position 38 | Split TLS record at SNI 39 | Version 40 | Source code 41 | What does all of this mean? 42 | Failed to start %1$s 43 | VPN 44 | Proxy 45 | ByeDPI 46 | VPN is running 47 | Proxy is running 48 | General 49 | ByeDPI 50 | About 51 | Command line editor 52 | UI editor 53 | Use command line settings 54 | Desync HTTP 55 | Desync HTTPS 56 | Desync UDP 57 | OOB Data 58 | SNI of fake packet 59 | Proxy 60 | Desync 61 | Command line arguments 62 | Hosts 63 | Hosts blacklist 64 | Hosts whitelist 65 | TCP Fast Open 66 | UDP fake count 67 | IPv6 68 | Fake offset 69 | Drop SACK 70 | UDP 71 | HTTP 72 | HTTPS 73 | Protocols 74 | Uncheck all to desync all traffic 75 | 76 | -------------------------------------------------------------------------------- /app/src/main/res/values/themes.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 16 | 17 | -------------------------------------------------------------------------------- /app/src/main/res/xml/backup_rules.xml: -------------------------------------------------------------------------------- 1 | 8 | 9 | 13 | -------------------------------------------------------------------------------- /app/src/main/res/xml/byedpi_cmd_settings.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 10 | 13 | 14 | 15 | 16 | 17 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /app/src/main/res/xml/byedpi_ui_settings.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 10 | 13 | 14 | 15 | 17 | 18 | 23 | 24 | 30 | 31 | 37 | 38 | 44 | 45 | 49 | 50 | 54 | 55 | 56 | 57 | 59 | 60 | 67 | 68 | 74 | 75 | 81 | 82 | 88 | 89 | 96 | 97 | 103 | 104 | 108 | 109 | 113 | 114 | 120 | 121 | 127 | 128 | 133 | 134 | 139 | 140 | 141 | 142 | 145 | 146 | 150 | 151 | 155 | 156 | 160 | 161 | 162 | 163 | 165 | 166 | 170 | 171 | 175 | 176 | 180 | 181 | 182 | 183 | 185 | 186 | 190 | 191 | 197 | 198 | 202 | 203 | 204 | 205 | 207 | 208 | 214 | 215 | 216 | 217 | 218 | -------------------------------------------------------------------------------- /app/src/main/res/xml/data_extraction_rules.xml: -------------------------------------------------------------------------------- 1 | 6 | 7 | 8 | 12 | 13 | 19 | -------------------------------------------------------------------------------- /app/src/main/res/xml/main_settings.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 9 | 10 | 17 | 18 | 25 | 26 | 31 | 32 | 36 | 37 | 38 | 39 | 41 | 42 | 46 | 47 | 52 | 53 | 58 | 59 | 60 | 61 | 63 | 64 | 69 | 70 | 74 | 77 | 78 | 79 | 80 | 81 | 82 | -------------------------------------------------------------------------------- /app/src/test/java/io/github/dovecoteescapee/byedpi/ExampleUnitTest.kt: -------------------------------------------------------------------------------- 1 | package io.github.dovecoteescapee.byedpi 2 | 3 | import org.junit.Test 4 | 5 | import org.junit.Assert.* 6 | 7 | /** 8 | * Example local unit test, which will execute on the development machine (host). 9 | * 10 | * See [testing documentation](http://d.android.com/tools/testing). 11 | */ 12 | class ExampleUnitTest { 13 | @Test 14 | fun addition_isCorrect() { 15 | assertEquals(4, 2 + 2) 16 | } 17 | } -------------------------------------------------------------------------------- /build.gradle.kts: -------------------------------------------------------------------------------- 1 | // Top-level build file where you can add configuration options common to all sub-projects/modules. 2 | plugins { 3 | id("com.android.application") version "8.3.0" apply false 4 | id("org.jetbrains.kotlin.android") version "1.9.22" apply false 5 | } 6 | -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/full_description.txt: -------------------------------------------------------------------------------- 1 |

ByeDPI runs a local VPN service to bypass DPI (Deep Packet Inspection) and censorship. It runs a SOCKS5 proxy ByeDPI and redirects all traffic through it.

To bypass some blocks, you may need to change the settings. More about the various settings can be found in the ByeDPI documentation.

The application uses the VPN mode on Android to redirect traffic, but does not send anything to a remote server. It does not encrypt traffic and does not hide your IP address.

-------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/images/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dovecoteescapee/ByeDPIAndroid/43db0955de687d43f9473374dc7c5548eaa55359/fastlane/metadata/android/en-US/images/icon.png -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/images/phoneScreenshots/screenshot1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dovecoteescapee/ByeDPIAndroid/43db0955de687d43f9473374dc7c5548eaa55359/fastlane/metadata/android/en-US/images/phoneScreenshots/screenshot1.png -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/images/phoneScreenshots/screenshot2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dovecoteescapee/ByeDPIAndroid/43db0955de687d43f9473374dc7c5548eaa55359/fastlane/metadata/android/en-US/images/phoneScreenshots/screenshot2.png -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/images/phoneScreenshots/screenshot3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dovecoteescapee/ByeDPIAndroid/43db0955de687d43f9473374dc7c5548eaa55359/fastlane/metadata/android/en-US/images/phoneScreenshots/screenshot3.png -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/short_description.txt: -------------------------------------------------------------------------------- 1 | Bypass censorship on Android -------------------------------------------------------------------------------- /fastlane/metadata/android/ru-RU/full_description.txt: -------------------------------------------------------------------------------- 1 |

ByeDPI запускает локальный VPN-сервис для обхода DPI (Deep Packet Inspection) и цензуры. Приложение локально запускает SOCKS5-прокси ByeDPI и перенаправляет весь трафик через него.

Для обхода некоторых блокировок может потребоваться изменить настройки. Подробнее о различных настройках можно прочитать в документации ByeDPI.

Приложение использует VPN-режим на Android для перенаправления трафика, но не передает ничего на удаленный сервер. Оно не шифрует трафик и не скрывает ваш IP-адрес.

-------------------------------------------------------------------------------- /fastlane/metadata/android/ru-RU/short_description.txt: -------------------------------------------------------------------------------- 1 | Обход цензуры на Android -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | # Project-wide Gradle settings. 2 | # IDE (e.g. Android Studio) users: 3 | # Gradle settings configured through the IDE *will override* 4 | # any settings specified in this file. 5 | # For more details on how to configure your build environment visit 6 | # http://www.gradle.org/docs/current/userguide/build_environment.html 7 | # Specifies the JVM arguments used for the daemon process. 8 | # The setting is particularly useful for tweaking memory settings. 9 | org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8 10 | # When configured, Gradle will run in incubating parallel mode. 11 | # This option should only be used with decoupled projects. More details, visit 12 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects 13 | # org.gradle.parallel=true 14 | # AndroidX package structure to make it clearer which packages are bundled with the 15 | # Android operating system, and which are packaged with your app's APK 16 | # https://developer.android.com/topic/libraries/support-library/androidx-rn 17 | android.useAndroidX=true 18 | # Kotlin code style for this project: "official" or "obsolete": 19 | kotlin.code.style=official 20 | # Enables namespacing of each library's R class so that its R class includes only the 21 | # resources declared in the library itself and none from the library's dependencies, 22 | # thereby reducing the size of the R class for that library 23 | android.nonTransitiveRClass=true -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dovecoteescapee/ByeDPIAndroid/43db0955de687d43f9473374dc7c5548eaa55359/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Mon Feb 19 00:00:21 MSK 2024 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.4-bin.zip 5 | zipStoreBase=GRADLE_USER_HOME 6 | zipStorePath=wrapper/dists 7 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | # 4 | # Copyright 2015 the original author or authors. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # https://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | ############################################################################## 20 | ## 21 | ## Gradle start up script for UN*X 22 | ## 23 | ############################################################################## 24 | 25 | # Attempt to set APP_HOME 26 | # Resolve links: $0 may be a link 27 | PRG="$0" 28 | # Need this for relative symlinks. 29 | while [ -h "$PRG" ] ; do 30 | ls=`ls -ld "$PRG"` 31 | link=`expr "$ls" : '.*-> \(.*\)$'` 32 | if expr "$link" : '/.*' > /dev/null; then 33 | PRG="$link" 34 | else 35 | PRG=`dirname "$PRG"`"/$link" 36 | fi 37 | done 38 | SAVED="`pwd`" 39 | cd "`dirname \"$PRG\"`/" >/dev/null 40 | APP_HOME="`pwd -P`" 41 | cd "$SAVED" >/dev/null 42 | 43 | APP_NAME="Gradle" 44 | APP_BASE_NAME=`basename "$0"` 45 | 46 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 47 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 48 | 49 | # Use the maximum available, or set MAX_FD != -1 to use that value. 50 | MAX_FD="maximum" 51 | 52 | warn () { 53 | echo "$*" 54 | } 55 | 56 | die () { 57 | echo 58 | echo "$*" 59 | echo 60 | exit 1 61 | } 62 | 63 | # OS specific support (must be 'true' or 'false'). 64 | cygwin=false 65 | msys=false 66 | darwin=false 67 | nonstop=false 68 | case "`uname`" in 69 | CYGWIN* ) 70 | cygwin=true 71 | ;; 72 | Darwin* ) 73 | darwin=true 74 | ;; 75 | MINGW* ) 76 | msys=true 77 | ;; 78 | NONSTOP* ) 79 | nonstop=true 80 | ;; 81 | esac 82 | 83 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 84 | 85 | 86 | # Determine the Java command to use to start the JVM. 87 | if [ -n "$JAVA_HOME" ] ; then 88 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 89 | # IBM's JDK on AIX uses strange locations for the executables 90 | JAVACMD="$JAVA_HOME/jre/sh/java" 91 | else 92 | JAVACMD="$JAVA_HOME/bin/java" 93 | fi 94 | if [ ! -x "$JAVACMD" ] ; then 95 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 96 | 97 | Please set the JAVA_HOME variable in your environment to match the 98 | location of your Java installation." 99 | fi 100 | else 101 | JAVACMD="java" 102 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 103 | 104 | Please set the JAVA_HOME variable in your environment to match the 105 | location of your Java installation." 106 | fi 107 | 108 | # Increase the maximum file descriptors if we can. 109 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 110 | MAX_FD_LIMIT=`ulimit -H -n` 111 | if [ $? -eq 0 ] ; then 112 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 113 | MAX_FD="$MAX_FD_LIMIT" 114 | fi 115 | ulimit -n $MAX_FD 116 | if [ $? -ne 0 ] ; then 117 | warn "Could not set maximum file descriptor limit: $MAX_FD" 118 | fi 119 | else 120 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 121 | fi 122 | fi 123 | 124 | # For Darwin, add options to specify how the application appears in the dock 125 | if $darwin; then 126 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 127 | fi 128 | 129 | # For Cygwin or MSYS, switch paths to Windows format before running java 130 | if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then 131 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 132 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 133 | 134 | JAVACMD=`cygpath --unix "$JAVACMD"` 135 | 136 | # We build the pattern for arguments to be converted via cygpath 137 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 138 | SEP="" 139 | for dir in $ROOTDIRSRAW ; do 140 | ROOTDIRS="$ROOTDIRS$SEP$dir" 141 | SEP="|" 142 | done 143 | OURCYGPATTERN="(^($ROOTDIRS))" 144 | # Add a user-defined pattern to the cygpath arguments 145 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 146 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 147 | fi 148 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 149 | i=0 150 | for arg in "$@" ; do 151 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 152 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 153 | 154 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 155 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 156 | else 157 | eval `echo args$i`="\"$arg\"" 158 | fi 159 | i=`expr $i + 1` 160 | done 161 | case $i in 162 | 0) set -- ;; 163 | 1) set -- "$args0" ;; 164 | 2) set -- "$args0" "$args1" ;; 165 | 3) set -- "$args0" "$args1" "$args2" ;; 166 | 4) set -- "$args0" "$args1" "$args2" "$args3" ;; 167 | 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 168 | 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 169 | 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 170 | 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 171 | 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 172 | esac 173 | fi 174 | 175 | # Escape application args 176 | save () { 177 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done 178 | echo " " 179 | } 180 | APP_ARGS=`save "$@"` 181 | 182 | # Collect all arguments for the java command, following the shell quoting and substitution rules 183 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" 184 | 185 | exec "$JAVACMD" "$@" 186 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%" == "" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%" == "" set DIRNAME=. 29 | set APP_BASE_NAME=%~n0 30 | set APP_HOME=%DIRNAME% 31 | 32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 34 | 35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 37 | 38 | @rem Find java.exe 39 | if defined JAVA_HOME goto findJavaFromJavaHome 40 | 41 | set JAVA_EXE=java.exe 42 | %JAVA_EXE% -version >NUL 2>&1 43 | if "%ERRORLEVEL%" == "0" goto execute 44 | 45 | echo. 46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 47 | echo. 48 | echo Please set the JAVA_HOME variable in your environment to match the 49 | echo location of your Java installation. 50 | 51 | goto fail 52 | 53 | :findJavaFromJavaHome 54 | set JAVA_HOME=%JAVA_HOME:"=% 55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 56 | 57 | if exist "%JAVA_EXE%" goto execute 58 | 59 | echo. 60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 61 | echo. 62 | echo Please set the JAVA_HOME variable in your environment to match the 63 | echo location of your Java installation. 64 | 65 | goto fail 66 | 67 | :execute 68 | @rem Setup the command line 69 | 70 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 71 | 72 | 73 | @rem Execute Gradle 74 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 75 | 76 | :end 77 | @rem End local scope for the variables with windows NT shell 78 | if "%ERRORLEVEL%"=="0" goto mainEnd 79 | 80 | :fail 81 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 82 | rem the _cmd.exe /c_ return code! 83 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 84 | exit /b 1 85 | 86 | :mainEnd 87 | if "%OS%"=="Windows_NT" endlocal 88 | 89 | :omega 90 | -------------------------------------------------------------------------------- /settings.gradle.kts: -------------------------------------------------------------------------------- 1 | pluginManagement { 2 | repositories { 3 | google() 4 | mavenCentral() 5 | gradlePluginPortal() 6 | } 7 | } 8 | dependencyResolutionManagement { 9 | repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) 10 | repositories { 11 | google() 12 | mavenCentral() 13 | } 14 | } 15 | 16 | rootProject.name = "ByeDpi" 17 | include(":app") 18 | --------------------------------------------------------------------------------