├── .github ├── ISSUE_TEMPLATE │ ├── bug.yml │ ├── config.yml │ └── feature.yml └── images │ ├── app.svg │ └── logo.svg ├── .gitignore ├── .gitmodules ├── .idea ├── .gitignore ├── .name ├── androidTestResultsUserPreferences.xml ├── appInsightsSettings.xml ├── codeStyles │ ├── Project.xml │ └── codeStyleConfig.xml ├── compiler.xml ├── deploymentTargetSelector.xml ├── gradle.xml ├── kotlinc.xml ├── migrations.xml ├── misc.xml ├── render.experimental.xml └── vcs.xml ├── LICENSE ├── README-en.md ├── README-tr.md ├── README.md ├── app ├── .gitignore ├── build.gradle.kts ├── proguard-rules.pro └── src │ ├── androidTest │ └── java │ │ └── io │ │ └── github │ │ └── dovecoteescapee │ │ └── byedpi │ │ └── ExampleInstrumentedTest.kt │ ├── main │ ├── AndroidManifest.xml │ ├── assets │ │ ├── proxytest_cmds.txt │ │ └── proxytest_sites.txt │ ├── cpp │ │ ├── CMakeLists.txt │ │ ├── main.h │ │ ├── native-lib.c │ │ ├── utils.c │ │ └── utils.h │ ├── java │ │ └── io │ │ │ └── github │ │ │ └── dovecoteescapee │ │ │ └── byedpi │ │ │ ├── activities │ │ │ ├── MainActivity.kt │ │ │ ├── SettingsActivity.kt │ │ │ ├── TestActivity.kt │ │ │ ├── TestSettingsActivity.kt │ │ │ └── ToggleActivity.kt │ │ │ ├── adapters │ │ │ └── AppSelectionAdapter.kt │ │ │ ├── core │ │ │ ├── ByeDpiProxy.kt │ │ │ ├── ByeDpiProxyPreferences.kt │ │ │ └── TProxyService.kt │ │ │ ├── data │ │ │ ├── Actions.kt │ │ │ ├── AppInfo.kt │ │ │ ├── AppSettings.kt │ │ │ ├── AppStatus.kt │ │ │ ├── Broadcasts.kt │ │ │ ├── Command.kt │ │ │ └── ServiceStatus.kt │ │ │ ├── fragments │ │ │ ├── AppSelectionFragment.kt │ │ │ ├── ByeDpiCommandLineSettingsFragment.kt │ │ │ ├── ByeDpiUISettingsFragment.kt │ │ │ ├── MainSettingsFragment.kt │ │ │ └── ProxyTestSettingsFragment.kt │ │ │ ├── receiver │ │ │ └── BootReceiver.kt │ │ │ ├── services │ │ │ ├── ByeDpiProxyService.kt │ │ │ ├── ByeDpiStatus.kt │ │ │ ├── ByeDpiVpnService.kt │ │ │ ├── LifecycleVpnService.kt │ │ │ ├── QuickTileService.kt │ │ │ └── ServiceManager.kt │ │ │ └── utility │ │ │ ├── ArgumentsUtils.kt │ │ │ ├── GoogleVideoUtils.kt │ │ │ ├── HistoryUtils.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 │ │ ├── ic_telegram.xml │ │ └── ic_toggle.xml │ │ ├── layout │ │ ├── activity_main.xml │ │ ├── activity_proxy_test.xml │ │ ├── activity_settings.xml │ │ ├── activity_test_settings.xml │ │ ├── app_selection.xml │ │ └── item_app.xml │ │ ├── menu │ │ ├── menu_main.xml │ │ ├── menu_settings.xml │ │ └── menu_test.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_banner.png │ │ ├── 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-en │ │ ├── arrays.xml │ │ └── strings.xml │ │ ├── values-night │ │ └── themes.xml │ │ ├── values-tr │ │ ├── arrays.xml │ │ └── strings.xml │ │ ├── values │ │ ├── arrays.xml │ │ ├── colors.xml │ │ ├── strings.xml │ │ └── themes.xml │ │ └── xml │ │ ├── backup_rules.xml │ │ ├── byedpi_cmd_settings.xml │ │ ├── byedpi_ui_settings.xml │ │ ├── data_extraction_rules.xml │ │ ├── main_settings.xml │ │ ├── proxy_test_settings.xml │ │ └── shortcuts.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 │ │ │ └── screenshot4.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 ├── sbox.md └── 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 | -------------------------------------------------------------------------------- /.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/androidTestResultsUserPreferences.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 21 | 22 | -------------------------------------------------------------------------------- /.idea/appInsightsSettings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 25 | 26 | -------------------------------------------------------------------------------- /.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/deploymentTargetSelector.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 8 | 9 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /.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-en.md: -------------------------------------------------------------------------------- 1 | # ByeByeDPI Android 2 | [Русский](README.md) | English | [Türkçe](README-tr.md) 3 | 4 |
5 | ByeDPI Logo 6 |
7 | 8 | --- 9 | 10 | An Android application that locally runs ByeDPI and routes all traffic through it. 11 | 12 | For stable operation, you may need to adjust the settings. You can read more about different settings in the [ByeDPI documentation](https://github.com/hufrea/byedpi/blob/v0.13/README.md). 13 | 14 | This application is **not** a VPN. It uses Android's VPN mode to route traffic but does not transmit anything to a remote server. It does not encrypt traffic or hide your IP address. 15 | 16 | This application is a fork of [ByeDPIAndroid](https://github.com/dovecoteescapee/ByeDPIAndroid). 17 | 18 | --- 19 | 20 | ### Features 21 | * Autostart service on device boot 22 | * Saving lists of command-line parameters 23 | * Improved compatibility with Android TV/BOX 24 | * Per-app split tunneling 25 | * Import/export settings 26 | 27 | ### Usage 28 | * To enable auto-start, activate the option in settings. 29 | * It is recommended to connect to the VPN once to accept the request. 30 | * After that, upon device startup, the application will automatically launch the service based on settings (VPN/Proxy). 31 | * If you are using an Android TV/BOX and your Ethernet connection drops when connecting, enable whitelist mode and specify the apps that should work through the VPN (e.g., YouTube). 32 | * Guide for [SberBOX](sbox.md) 33 | 34 | ### How to use ByeByeDPI with AdGuard? 35 | * Start ByeByeDPI in proxy mode. 36 | * Add ByeByeDPI to AdGuard exclusions on the "App Management" tab. 37 | * In AdGuard settings, specify the proxy: 38 | ```plaintext 39 | Proxy Type: SOCKS5 40 | Host: 127.0.0.1 41 | Port: 1080 (default) 42 | ``` 43 | 44 | ### Dependencies 45 | - [ByeDPI](https://github.com/hufrea/byedpi) 46 | - [hev-socks5-tunnel](https://github.com/heiher/hev-socks5-tunnel) 47 | -------------------------------------------------------------------------------- /README-tr.md: -------------------------------------------------------------------------------- 1 | # ByeByeDPI Android 2 | [Русский](README.md) | [English](README-en.md) | Türkçe 3 | 4 |
5 | ByeDPI Logo 6 |
7 | 8 | --- 9 | 10 | ByeDPI'yi yerel olarak çalıştıran ve tüm trafiği bunun üzerinden yönlendiren bir Android uygulaması. 11 | 12 | Kararlı bir çalışma için ayarları yapmanız gerekebilir. Farklı ayarlar hakkında daha fazla bilgiye [ByeDPI dökümantasyonundan](https://github.com/hufrea/byedpi/blob/v0.13/README.md) ulaşabilirsiniz. 13 | 14 | Bu uygulama **VPN** değildir. Trafiği yönlendirmek için Android'in VPN modunu kullanır ancak herhangi bir veriyi uzak bir sunucuya iletmez. Trafiği şifrelemez veya IP adresinizi gizlemez. 15 | 16 | Bu uygulama, [ByeDPIAndroid](https://github.com/dovecoteescapee/ByeDPIAndroid) uygulamasının bir çatallamasıdır. 17 | 18 | --- 19 | 20 | ### Özellikler 21 | * Cihaz başlatıldığında hizmetin otomatik başlatılması 22 | * Komut satırı parametrelerinin listelerinin kaydedilmesi 23 | * Android TV/BOX ile geliştirilmiş uyumluluk 24 | * Uygulama başına bölünmüş tünelleme 25 | * Ayarları içe/dışa aktarma 26 | 27 | ### Kullanım 28 | * Otomatik başlatmayı etkinleştirmek için ayarlarda seçeneği aktifleştirin. 29 | * İlk başta VPN'e bağlanarak isteği kabul etmeniz önerilir. 30 | * Bundan sonra, cihaz başlatıldığında, uygulama ayarlara göre (VPN/Proxy) hizmeti otomatik olarak başlatacaktır. 31 | * Android TV/BOX kullanıyorsanız ve Ethernet bağlantınız VPN'e bağlanırken kopuyorsa, beyaz liste modunu etkinleştirip VPN üzerinden çalışması gereken uygulamaları belirleyin (örneğin, YouTube). 32 | * [SberBOX](sbox.md) için kılavuz 33 | 34 | ### Türkiye İle İlgili Destek İçin 35 | * Discord: [nyaex](https://github.com/nyaexx) veya [shouyuma](https://github.com/Hamzahsl) 36 | 37 | 38 | ### ByeByeDPI'yi AdGuard ile nasıl kullanırım? 39 | * ByeByeDPI'yi proxy modunda başlatın. 40 | * ByeByeDPI'yi AdGuard dışlamalarına "Uygulama Yönetimi" sekmesinde ekleyin. 41 | * AdGuard ayarlarında, proxy'i belirtin: 42 | ```plaintext 43 | Proxy Türü: SOCKS5 44 | Host: 127.0.0.1 45 | Port: 1080 (varsayılan) 46 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ByeByeDPI Android 2 | Русский | [English](README-en.md) | [Türkçe](README-tr.md) 3 | 4 |
5 | Логотип ByeDPI 6 |
7 | 8 | --- 9 | 10 | Приложение для Android, которое локально запускает ByeDPI и перенаправляет весь трафик через него. 11 | 12 | Для стабильной работы может потребоваться изменить настройки. Подробнее о различных настройках можно прочитать в [документации ByeDPI](https://github.com/hufrea/byedpi/blob/v0.13/README.md). 13 | 14 | Приложение не является VPN. Оно использует VPN-режим на Android для перенаправления трафика, но не передает ничего на удаленный сервер. Оно не шифрует трафик и не скрывает ваш IP-адрес. 15 | 16 | Приложения является форком [ByeDPIAndroid](https://github.com/dovecoteescapee/ByeDPIAndroid) 17 | 18 | --- 19 | 20 | ### Возможности 21 | * Автозапуск сервиса при старте устройства 22 | * Сохранение списков параметров командной строки 23 | * Улучшена совместимость с Android TV/BOX 24 | * Раздельное туннелирование приложений 25 | * Импорт/экспорт настроек 26 | 27 | ### Использование 28 | * Для работы автозапуска активируйте пункт в настройках. 29 | * Рекомендуется подключится один раз к VPN, чтобы принять запрос. 30 | * После этого, при загрузке устройства, приложение автоматически запустит сервис в зависимости от настроек (VPN/Proxy) 31 | * Если у вас Android TV/BOX, и при подключении пропадает соединение по Ethernet, активируйте режим белого списка и укажите нужные приложения, которые должны работать через VPN (например, YouTube) 32 | * Комплексная инструкция от комьюнити [ByeByeDPI-Manual](https://github.com/HideakiTaiki/ByeByeDPI-Manual) 33 | 34 | ### Как использовать ByeByeDPI вместе с AdGuard? 35 | * Запустите ByeByeDPI в режиме прокси. 36 | * Добавьте ByeByeDPI в исключения AdGuard на вкладке "Управление приложениями". 37 | * В настройках AdGuard укажите прокси: 38 | ```plaintext 39 | Тип прокси: SOCKS5 40 | Хост: 127.0.0.1 41 | Порт: 1080 (по умолчанию) 42 | ``` 43 | 44 | ### Зависимости 45 | - [ByeDPI](https://github.com/hufrea/byedpi) 46 | - [hev-socks5-tunnel](https://github.com/heiher/hev-socks5-tunnel) 47 | -------------------------------------------------------------------------------- /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.romanvht.byedpi" 12 | minSdk = 21 13 | //noinspection OldTargetApi 14 | targetSdk = 34 15 | versionCode = 1410 16 | versionName = "1.4.11" 17 | 18 | testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" 19 | 20 | ndk { 21 | abiFilters.add("armeabi-v7a") 22 | abiFilters.add("arm64-v8a") 23 | abiFilters.add("x86") 24 | abiFilters.add("x86_64") 25 | } 26 | } 27 | 28 | buildFeatures { 29 | buildConfig = true 30 | } 31 | 32 | buildTypes { 33 | release { 34 | buildConfigField("String", "VERSION_NAME", "\"${defaultConfig.versionName}\"") 35 | 36 | isMinifyEnabled = false 37 | proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro") 38 | signingConfig = signingConfigs.getByName("debug") 39 | } 40 | debug { 41 | buildConfigField("String", "VERSION_NAME", "\"${defaultConfig.versionName}-debug\"") 42 | } 43 | } 44 | compileOptions { 45 | sourceCompatibility = JavaVersion.VERSION_1_8 46 | targetCompatibility = JavaVersion.VERSION_1_8 47 | } 48 | kotlinOptions { 49 | jvmTarget = "1.8" 50 | } 51 | externalNativeBuild { 52 | cmake { 53 | path = file("src/main/cpp/CMakeLists.txt") 54 | version = "3.22.1" 55 | } 56 | } 57 | buildFeatures { 58 | viewBinding = true 59 | } 60 | 61 | // https://android.izzysoft.de/articles/named/iod-scan-apkchecks?lang=en#blobs 62 | dependenciesInfo { 63 | // Disables dependency metadata when building APKs. 64 | includeInApk = false 65 | // Disables dependency metadata when building Android App Bundles. 66 | includeInBundle = false 67 | } 68 | } 69 | 70 | dependencies { 71 | //noinspection GradleDependency 72 | implementation("androidx.fragment:fragment-ktx:1.8.4") 73 | implementation("androidx.core:core-ktx:1.13.1") 74 | implementation("androidx.appcompat:appcompat:1.7.0") 75 | implementation("androidx.preference:preference-ktx:1.2.1") 76 | implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.8.6") 77 | implementation("androidx.lifecycle:lifecycle-service:2.8.6") 78 | implementation("com.google.android.material:material:1.12.0") 79 | implementation("com.google.code.gson:gson:2.8.9") 80 | implementation("com.takisoft.preferencex:preferencex:1.1.0") 81 | testImplementation("junit:junit:4.13.2") 82 | androidTestImplementation("androidx.test.ext:junit:1.2.1") 83 | androidTestImplementation("androidx.test.espresso:espresso-core:3.6.1") 84 | } 85 | 86 | tasks.register("runNdkBuild") { 87 | group = "build" 88 | 89 | val ndkDir = android.ndkDirectory 90 | executable = if (System.getProperty("os.name").startsWith("Windows", ignoreCase = true)) { 91 | "$ndkDir\\ndk-build.cmd" 92 | } else { 93 | "$ndkDir/ndk-build" 94 | } 95 | setArgs(listOf( 96 | "NDK_PROJECT_PATH=build/intermediates/ndkBuild", 97 | "NDK_LIBS_OUT=src/main/jniLibs", 98 | "APP_BUILD_SCRIPT=src/main/jni/Android.mk", 99 | "NDK_APPLICATION_MK=src/main/jni/Application.mk" 100 | )) 101 | 102 | println("Command: $commandLine") 103 | } 104 | 105 | tasks.preBuild { 106 | dependsOn("runNdkBuild") 107 | } -------------------------------------------------------------------------------- /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 | 10 | 11 | 13 | 15 | 17 | 18 | 30 | 31 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 44 | 45 | 46 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 64 | 65 | 69 | 70 | 74 | 75 | 79 | 80 | 81 | 82 | 85 | 88 | 89 | 90 | 93 | 96 | 97 | 98 | 104 | 105 | 107 | 108 | 109 | 110 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | -------------------------------------------------------------------------------- /app/src/main/assets/proxytest_cmds.txt: -------------------------------------------------------------------------------- 1 | -q1+s -s25+s -o5+s -f-1 -S 2 | -q1+s -s29+s -o5+s -f-1 -S 3 | -Ku -a1 -An -Kt,h -d7 -s2 -An 4 | -s1 -q1 -Y -At -f-1 -r1+s -As 5 | -q1+s -s25+s -o5+s -f-1 -S -As 6 | -q1+s -s29+s -o5+s -f-1 -S -As 7 | -Ku -a1 -An -Kt,h -q1 -r25+s -An 8 | -n vk.com -q 1+s -O 1 -s 25+s -t 5 9 | -n vk.com -q 1+s -O 1 -s 29+s -t 5 10 | -s0 -o1 -Ar -o1 -At -f-1 -r1+s -As 11 | -s1 -o1 -Ar -o1 -At -f-1 -r1+s -As 12 | -s1 -q1 -a2 -Art -f-1 -r1+s -a3 -Asn 13 | -s1 -d1 -o1 -Ar -o3 -At -f-1 -r1+s -As 14 | -Ku -a5 -An -Kt,h -s1 -f-1 -S -b661 -An 15 | -s1 -q1 -Y -a2 -Art -f-1 -r1+s -a3 -Asn 16 | --split 2 --disorder 3 --fake -1 --ttl 5 17 | -n www.google.com -q1+s -O1 -s25+s -t5 18 | -n www.google.com -q1+s -O1 -s29+s -t5 19 | -n www.google.com -q2+s -O1 -s25+s -t5 20 | -n www.google.com -q2+s -O1 -s29+s -t5 21 | -s1 -q1 -Y -At -T5 -b1000 -S -f-1 -r1+s -As 22 | -s0 -o1 -d1 -r1+s -Ar -o1 -At -f-1 -r1+s -As 23 | --disoob 3+sni --split -1+host --tlsrec 1+sni 24 | -Ku -a1 -An -Kt,h -q1+s -s29+s -o5+s -f-1 -S -As 25 | --split 2 --fake 1 --ttl 5 --tls-sni=www.google.com 26 | -Ku -a1 -An -Kt,h -s1 -d3+s -Mh,d -At -r1+s -r3+s -An 27 | -Ku -a1 -An -Kt,h -s1 -q1 -a2 -Art -f-1 -r1+s -a3 -An 28 | -Ku -a1 -An -Kt,h -s1 -q2 -a2 -Art -f-1 -r2+s -a3 -An 29 | -Ku -a1 -An -Kt,h -s1 -q1 -Art -f-1 --md5sig -r1+s -An 30 | -Ku -a3 -An -Kt,h -s0 -o1 -Ar -o1 -At -f-1 -r1+s -As -An 31 | -Ku -a5 -An -Kt,h -o0 -d1 -r1+s -t10 -b1500 -S -s0+s -d3+s -An 32 | -Ku -a1 -An -Kt,h -d1 -s4 -d8 -s1+s -d5+s -s10+s -d20+s -An 33 | -s1 -q1 -Y -Ar -s5 -o1+s -At -f-1 -r1+s -As -s1 -o1+s -s-1 -An 34 | --split 1 --disorder 3+s --mod-http=h,d --auto=torst --tlsrec 1+s 35 | -Ku -a3 -An -Kt,h -s0 -o1 -d1 -r1+s -Ar -o1 -At -f-1 -r1+s -As -An 36 | -Ku -a3 -An -Kt,h -q1+s -s29+s -s30+s -s14+s -o5+s -f-1 --md5sig -Asn 37 | -s1 -q1 -Y -Ar -s5 -o25000+s -At -f-1 -r1+s -As -s1 -o1+s -s-1 -An -b+500 38 | -Ku -a1 -An -Kt,h -s1 -q1 -Ar -s5 -o1+s -At -f-1 -r1+s -As -s1 -o1+s -s-1 -An 39 | -Ku -a3 -An -Kt,h -d1 -s0+s -s3+s -s6+s -s9+s -s12+s -s15+s -s20+s -s30+s -An 40 | --split 1 --disorder 3+s --mod-http=h,d --auto=torst --tlsrec 1+s --tlsrec 3+sni 41 | -Ku -a3 -An -Kt,h -s1 -q1 -Y -Ar -s5 -o1+s -At -f-1 -r1+s -As -s1 -o1+s -s-1 -An 42 | -Ku -a3 -An -Kt,h -d1 -s0+s -d3+s -s6+s -d9+s -s12+s -d15+s -s20+s -d25+s -s30+s -An 43 | -Ku -a3 -An -Kt,h -s0 -o1 -Ar -o1 -At -f-1 -r1+s -As -b+500 --tls-sni=www.google.com -An 44 | -Ku -a3 -An -Kt,h -d1 -s0+s -d1+s -s3+s -d6+s -s12+s -d14+s -s20+s -d24+s -s30+s -d34+s -An 45 | -Ku -a3 -An -Kt,h -d1 -s0+s -d3+s -s6+s -d9+s -s12+s -d15+s -s20+s -d25+s -s30+s -d35+s -An 46 | -Ku -a3 -An -Kt,h -s1 -q1 -Ar -s5 -o25000+s -At -f-1 -r1+s -As -s1 -o1+s -s-1 -An -b+500 -An 47 | -Ku -a3 -An -Kt,h -s1 -q1 -Y -At -T5 -b1000 -S -f-1 -r1+s -As -n www.google.com -d1+s -O1 -s29+s -t 5 -An 48 | -Ku -a3 -An -Kt,h -s0 -o1 -d1 -r1+s -Ar -o1 -At -f-1 -r1+s -As -b+500 --tls-sni=www.google.com --ttl 3 -An 49 | -Ku -a3 -An -Kt,h -d1 -s1 -q1 -Y -Ar -s5 -o1+s -d3+s -s6+s -d9+s -s12+s -d15+s -s20+s -d25+s -s30+s -d35+s -An 50 | -Ku -l':\x16\x03\x01\x02\x87\x01\x00\x02\x83\x03\x03\x5f\x15\x63\xcb\x06' -a1 -An -s1 -q1 -Y -At -f-1 -r1+s -As 51 | -Ku -a3 -An -Kt,h -s1 -q1 -Y -Ar -s5 -o1+s -At -f-1 -r1+s -As -s1 -o1 +s -s-1 -An -b+500 --tls-sni=www.google.com --ttl 3 52 | -Ku -a1 -An -d1 -s0+s -d3+s -s6+s -d9+s -s12+s -d15+s -s20+s -d25+s -s30+s -d35+s -At,r,s -s1 -q1 -At,r,s -s5 -o25000+s -At,r,s -o1 -d1 -r1+s -t10 -b1500 -s0+s -d3+s -At,r,s -f-1 -r1+s -At,r,s -s1 -o1+s -s-1 -------------------------------------------------------------------------------- /app/src/main/assets/proxytest_sites.txt: -------------------------------------------------------------------------------- 1 | youtube.com 2 | manifest.googlevideo.com 3 | i.ytimg.com 4 | yt3.ggpht.com 5 | yt4.ggpht.com 6 | signaler-pa.youtube.com 7 | jnn-pa.googleapis.com 8 | rr1---sn-4axm-n8vs.googlevideo.com 9 | rr1---sn-gvnuxaxjvh-o8ge.googlevideo.com 10 | rr1---sn-ug5onuxaxjvh-p3ul.googlevideo.com 11 | rr1---sn-ug5onuxaxjvh-n8v6.googlevideo.com 12 | rr4---sn-q4flrnsl.googlevideo.com 13 | rr10---sn-gvnuxaxjvh-304z.googlevideo.com 14 | rr14---sn-n8v7kn7r.googlevideo.com 15 | rr16---sn-axq7sn76.googlevideo.com 16 | rr1---sn-8ph2xajvh-5xge.googlevideo.com 17 | rr1---sn-gvnuxaxjvh-5gie.googlevideo.com 18 | rr12---sn-gvnuxaxjvh-bvwz.googlevideo.com 19 | rr5---sn-n8v7knez.googlevideo.com 20 | rr1---sn-u5uuxaxjvhg0-ocje.googlevideo.com 21 | rr2---sn-q4fl6ndl.googlevideo.com 22 | rr5---sn-gvnuxaxjvh-n8vk.googlevideo.com 23 | rr4---sn-jvhnu5g-c35d.googlevideo.com 24 | rr1---sn-q4fl6n6y.googlevideo.com 25 | rr2---sn-hgn7ynek.googlevideo.com -------------------------------------------------------------------------------- /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 -Wall -Wno-unused -Wextra -Wno-unused-parameter -pedantic) 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 | union sockaddr_u; 2 | 3 | int get_default_ttl(); 4 | 5 | int get_addr(const char *str, union sockaddr_u *addr); 6 | 7 | void *add(void **root, int *n, size_t ss); 8 | 9 | void clear_params(void); 10 | 11 | char *ftob(const char *str, ssize_t *sl); 12 | 13 | char *data_from_str(const char *str, ssize_t *size); 14 | 15 | size_t parse_cform(char *buffer, size_t blen, const char *str, size_t slen); 16 | 17 | struct mphdr *parse_hosts(char *buffer, size_t size); 18 | 19 | struct mphdr *parse_ipset(char *buffer, size_t size); 20 | 21 | int parse_offset(struct part *part, const char *str); 22 | -------------------------------------------------------------------------------- /app/src/main/cpp/native-lib.c: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | #include 4 | #include 5 | 6 | #include "byedpi/error.h" 7 | #include "byedpi/proxy.h" 8 | #include "utils.h" 9 | 10 | static int g_proxy_fd = -1; 11 | 12 | JNIEXPORT jint JNI_OnLoad( 13 | __attribute__((unused)) JavaVM *vm, 14 | __attribute__((unused)) void *reserved) { 15 | default_params = params; 16 | return JNI_VERSION_1_6; 17 | } 18 | 19 | JNIEXPORT jint JNICALL 20 | Java_io_github_dovecoteescapee_byedpi_core_ByeDpiProxy_jniCreateSocket( 21 | JNIEnv *env, 22 | __attribute__((unused)) jobject thiz, 23 | jobjectArray args) { 24 | 25 | if (g_proxy_fd != -1) { 26 | LOG(LOG_S, "proxy already running, fd: %d", g_proxy_fd); 27 | return -1; 28 | } 29 | 30 | int argc = (*env)->GetArrayLength(env, args); 31 | char *argv[argc]; 32 | for (int i = 0; i < argc; i++) { 33 | jstring arg = (jstring) (*env)->GetObjectArrayElement(env, args, i); 34 | const char *arg_str = (*env)->GetStringUTFChars(env, arg, 0); 35 | argv[i] = strdup(arg_str); 36 | (*env)->ReleaseStringUTFChars(env, arg, arg_str); 37 | } 38 | 39 | int res = parse_args(argc, argv); 40 | 41 | if (res < 0) { 42 | uniperror("parse_args"); 43 | return -1; 44 | } 45 | 46 | int fd = listen_socket((union sockaddr_u *)¶ms.laddr); 47 | 48 | if (fd < 0) { 49 | uniperror("listen_socket"); 50 | return -1; 51 | } 52 | 53 | g_proxy_fd = fd; 54 | LOG(LOG_S, "listen_socket, fd: %d", fd); 55 | return fd; 56 | } 57 | 58 | JNIEXPORT jint JNICALL 59 | Java_io_github_dovecoteescapee_byedpi_core_ByeDpiProxy_jniStartProxy( 60 | __attribute__((unused)) JNIEnv *env, 61 | __attribute__((unused)) jobject thiz) { 62 | 63 | LOG(LOG_S, "start_proxy, fd: %d", g_proxy_fd); 64 | 65 | if (start_event_loop(g_proxy_fd) < 0) { 66 | uniperror("event_loop"); 67 | return get_e(); 68 | } 69 | 70 | return 0; 71 | } 72 | 73 | JNIEXPORT jint JNICALL 74 | Java_io_github_dovecoteescapee_byedpi_core_ByeDpiProxy_jniStopProxy( 75 | __attribute__((unused)) JNIEnv *env, 76 | __attribute__((unused)) jobject thiz) { 77 | 78 | LOG(LOG_S, "stop_proxy, fd: %d", g_proxy_fd); 79 | 80 | if (g_proxy_fd < 0) { 81 | LOG(LOG_S, "proxy is not running, fd: %d", g_proxy_fd); 82 | return 0; 83 | } 84 | 85 | reset_params(); 86 | int res = shutdown(g_proxy_fd, SHUT_RDWR); 87 | g_proxy_fd = -1; 88 | 89 | if (res < 0) { 90 | uniperror("shutdown"); 91 | return get_e(); 92 | } 93 | 94 | return 0; 95 | } 96 | -------------------------------------------------------------------------------- /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 | bool ipv6_support(void); 6 | -------------------------------------------------------------------------------- /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 android.widget.Toast 7 | import androidx.activity.result.contract.ActivityResultContracts 8 | import androidx.appcompat.app.AppCompatActivity 9 | import com.google.gson.Gson 10 | import io.github.dovecoteescapee.byedpi.BuildConfig 11 | import io.github.dovecoteescapee.byedpi.R 12 | import io.github.dovecoteescapee.byedpi.data.AppSettings 13 | import io.github.dovecoteescapee.byedpi.fragments.ByeDpiCommandLineSettingsFragment 14 | import io.github.dovecoteescapee.byedpi.fragments.ByeDpiUISettingsFragment 15 | import io.github.dovecoteescapee.byedpi.fragments.MainSettingsFragment 16 | import io.github.dovecoteescapee.byedpi.utility.HistoryUtils 17 | import io.github.dovecoteescapee.byedpi.utility.getPreferences 18 | import io.github.dovecoteescapee.byedpi.utility.getSelectedApps 19 | 20 | class SettingsActivity : AppCompatActivity() { 21 | 22 | override fun onCreate(savedInstanceState: Bundle?) { 23 | super.onCreate(savedInstanceState) 24 | setContentView(R.layout.activity_settings) 25 | 26 | val openFragment = intent.getStringExtra("open_fragment") 27 | 28 | when (openFragment) { 29 | "cmd" -> { 30 | supportFragmentManager 31 | .beginTransaction() 32 | .replace(R.id.settings, ByeDpiCommandLineSettingsFragment()) 33 | .commit() 34 | } 35 | "ui" -> { 36 | supportFragmentManager 37 | .beginTransaction() 38 | .replace(R.id.settings, ByeDpiUISettingsFragment()) 39 | .commit() 40 | } 41 | else -> { 42 | supportFragmentManager 43 | .beginTransaction() 44 | .replace(R.id.settings, MainSettingsFragment()) 45 | .commit() 46 | } 47 | } 48 | 49 | supportActionBar?.setDisplayHomeAsUpEnabled(true) 50 | } 51 | 52 | override fun onCreateOptionsMenu(menu: Menu?): Boolean { 53 | menuInflater.inflate(R.menu.menu_settings, menu) 54 | return true 55 | } 56 | 57 | override fun onOptionsItemSelected(item: MenuItem): Boolean = when (item.itemId) { 58 | android.R.id.home -> { 59 | onBackPressedDispatcher.onBackPressed() 60 | true 61 | } 62 | 63 | R.id.action_reset_settings -> { 64 | val prefs = getPreferences() 65 | val editor = prefs.edit() 66 | 67 | editor.clear() 68 | editor.apply() 69 | 70 | recreate() 71 | true 72 | } 73 | 74 | R.id.action_export_settings -> { 75 | val fileName = "bbd_${System.currentTimeMillis().toReadableDateTime()}.json" 76 | exportSettingsLauncher.launch(fileName) 77 | true 78 | } 79 | 80 | R.id.action_import_settings -> { 81 | importSettingsLauncher.launch(arrayOf("application/json")) 82 | true 83 | } 84 | 85 | else -> super.onOptionsItemSelected(item) 86 | } 87 | 88 | private val exportSettingsLauncher = registerForActivityResult( 89 | ActivityResultContracts.CreateDocument("application/json") 90 | ) { uri -> 91 | uri?.let { 92 | val prefs = getPreferences() 93 | val history = HistoryUtils(this).getHistory() 94 | val apps = prefs.getSelectedApps() 95 | 96 | val settings = prefs.all.filterKeys { key -> 97 | key !in setOf("byedpi_command_history", "selected_apps") 98 | } 99 | 100 | val export = AppSettings( 101 | app = BuildConfig.APPLICATION_ID, 102 | version = BuildConfig.VERSION_NAME, 103 | history = history, 104 | apps = apps, 105 | settings = settings 106 | ) 107 | 108 | val json = Gson().toJson(export) 109 | 110 | contentResolver.openOutputStream(it)?.use { outputStream -> 111 | outputStream.write(json.toByteArray()) 112 | } 113 | } 114 | } 115 | 116 | private val importSettingsLauncher = registerForActivityResult( 117 | ActivityResultContracts.OpenDocument() 118 | ) { uri -> 119 | uri?.let { 120 | contentResolver.openInputStream(it)?.use { inputStream -> 121 | val json = inputStream.bufferedReader().readText() 122 | 123 | val import = Gson().fromJson(json, AppSettings::class.java) 124 | 125 | if (import.app != BuildConfig.APPLICATION_ID) { 126 | Toast.makeText(this, "Invalid config", Toast.LENGTH_LONG).show() 127 | return@use 128 | } 129 | 130 | val prefs = getPreferences() 131 | val editor = prefs.edit() 132 | 133 | editor.clear() 134 | import.settings.forEach { (key, value) -> 135 | when (value) { 136 | is Int -> editor.putInt(key, value) 137 | is Boolean -> editor.putBoolean(key, value) 138 | is String -> editor.putString(key, value) 139 | is Float -> editor.putFloat(key, value) 140 | is Long -> editor.putLong(key, value) 141 | } 142 | } 143 | editor.putStringSet("selected_apps", import.apps.toSet()) 144 | editor.apply() 145 | HistoryUtils(this).saveHistory(import.history) 146 | 147 | recreate() 148 | } 149 | } 150 | } 151 | 152 | private fun Long.toReadableDateTime(): String { 153 | val format = java.text.SimpleDateFormat("yyyyMMdd_HHmm", java.util.Locale.getDefault()) 154 | return format.format(this) 155 | } 156 | } -------------------------------------------------------------------------------- /app/src/main/java/io/github/dovecoteescapee/byedpi/activities/TestSettingsActivity.kt: -------------------------------------------------------------------------------- 1 | package io.github.dovecoteescapee.byedpi.activities 2 | 3 | import android.os.Bundle 4 | import android.view.MenuItem 5 | import androidx.appcompat.app.AppCompatActivity 6 | import io.github.dovecoteescapee.byedpi.R 7 | import io.github.dovecoteescapee.byedpi.fragments.ProxyTestSettingsFragment 8 | 9 | class TestSettingsActivity : AppCompatActivity() { 10 | override fun onCreate(savedInstanceState: Bundle?) { 11 | super.onCreate(savedInstanceState) 12 | setContentView(R.layout.activity_test_settings) 13 | 14 | supportFragmentManager 15 | .beginTransaction() 16 | .replace(R.id.test_settings, ProxyTestSettingsFragment()) 17 | .commit() 18 | 19 | supportActionBar?.setDisplayHomeAsUpEnabled(true) 20 | } 21 | 22 | override fun onOptionsItemSelected(item: MenuItem): Boolean = when (item.itemId) { 23 | android.R.id.home -> { 24 | onBackPressedDispatcher.onBackPressed() 25 | true 26 | } 27 | else -> super.onOptionsItemSelected(item) 28 | } 29 | } -------------------------------------------------------------------------------- /app/src/main/java/io/github/dovecoteescapee/byedpi/activities/ToggleActivity.kt: -------------------------------------------------------------------------------- 1 | package io.github.dovecoteescapee.byedpi.activities 2 | 3 | import android.net.VpnService 4 | import android.os.Bundle 5 | import android.util.Log 6 | import androidx.appcompat.app.AppCompatActivity 7 | import io.github.dovecoteescapee.byedpi.data.AppStatus 8 | import io.github.dovecoteescapee.byedpi.data.Mode 9 | import io.github.dovecoteescapee.byedpi.services.ServiceManager 10 | import io.github.dovecoteescapee.byedpi.services.appStatus 11 | import io.github.dovecoteescapee.byedpi.utility.getPreferences 12 | import io.github.dovecoteescapee.byedpi.utility.mode 13 | 14 | class ToggleActivity : AppCompatActivity() { 15 | 16 | companion object { 17 | private const val TAG = "ToggleServiceActivity" 18 | } 19 | 20 | override fun onCreate(savedInstanceState: Bundle?) { 21 | super.onCreate(savedInstanceState) 22 | toggleService() 23 | finish() 24 | } 25 | 26 | private fun toggleService() { 27 | val (status) = appStatus 28 | when (status) { 29 | AppStatus.Halted -> { 30 | val mode = getPreferences().mode() 31 | 32 | if (mode == Mode.VPN && VpnService.prepare(this) != null) { 33 | return 34 | } 35 | 36 | ServiceManager.start(this, mode) 37 | Log.i(TAG, "Toggle start") 38 | } 39 | AppStatus.Running -> { 40 | ServiceManager.stop(this) 41 | Log.i(TAG, "Toggle stop") 42 | } 43 | } 44 | } 45 | } -------------------------------------------------------------------------------- /app/src/main/java/io/github/dovecoteescapee/byedpi/adapters/AppSelectionAdapter.kt: -------------------------------------------------------------------------------- 1 | package io.github.dovecoteescapee.byedpi.adapters 2 | 3 | import android.view.LayoutInflater 4 | import android.view.View 5 | import android.view.ViewGroup 6 | import android.widget.CheckBox 7 | import android.widget.Filter 8 | import android.widget.Filterable 9 | import android.widget.ImageView 10 | import android.widget.TextView 11 | import androidx.recyclerview.widget.RecyclerView 12 | import io.github.dovecoteescapee.byedpi.R 13 | import io.github.dovecoteescapee.byedpi.data.AppInfo 14 | 15 | class AppSelectionAdapter( 16 | private val allApps: List, 17 | private val onAppSelected: (AppInfo, Boolean) -> Unit 18 | ) : RecyclerView.Adapter(), Filterable { 19 | 20 | private val filteredApps: MutableList = allApps 21 | .filter { !it.appName.contains("com.") } 22 | .toMutableList() 23 | 24 | private val originalApps: List = allApps 25 | .filter { !it.appName.contains("com.") } 26 | 27 | class ViewHolder(view: View) : RecyclerView.ViewHolder(view) { 28 | val appIcon: ImageView = view.findViewById(R.id.appIcon) 29 | val appName: TextView = view.findViewById(R.id.appName) 30 | val appCheckBox: CheckBox = view.findViewById(R.id.appCheckBox) 31 | } 32 | 33 | override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { 34 | val view = LayoutInflater.from(parent.context).inflate(R.layout.item_app, parent, false) 35 | return ViewHolder(view) 36 | } 37 | 38 | override fun onBindViewHolder(holder: ViewHolder, position: Int) { 39 | val app = filteredApps[position] 40 | holder.appIcon.setImageDrawable(app.icon) 41 | holder.appName.text = app.appName 42 | holder.appCheckBox.isChecked = app.isSelected 43 | 44 | holder.itemView.setOnClickListener { 45 | app.isSelected = !app.isSelected 46 | holder.appCheckBox.isChecked = app.isSelected 47 | onAppSelected(app, app.isSelected) 48 | } 49 | } 50 | 51 | override fun getItemCount() = filteredApps.size 52 | 53 | override fun getFilter(): Filter { 54 | return object : Filter() { 55 | override fun performFiltering(constraint: CharSequence?): FilterResults { 56 | val query = constraint?.toString()?.lowercase() ?: "" 57 | val filteredList = if (query.isEmpty()) { 58 | originalApps 59 | } else { 60 | originalApps.filter { it.appName.lowercase().contains(query) } 61 | } 62 | return FilterResults().apply { values = filteredList } 63 | } 64 | 65 | override fun publishResults(constraint: CharSequence?, results: FilterResults?) { 66 | val newFilteredApps = (results?.values as? List) ?: originalApps 67 | filteredApps.clear() 68 | filteredApps.addAll(newFilteredApps) 69 | notifyDataSetChanged() 70 | } 71 | } 72 | } 73 | } -------------------------------------------------------------------------------- /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 | 15 | suspend fun startProxy(preferences: ByeDpiProxyPreferences): Int { 16 | val result = createSocket(preferences) 17 | 18 | if (result < 0) { 19 | return -1 20 | } 21 | 22 | return jniStartProxy() 23 | } 24 | 25 | suspend fun stopProxy(): Int { 26 | mutex.withLock { 27 | val result = jniStopProxy() 28 | 29 | if (result < 0) { 30 | return -1 31 | } 32 | 33 | return result 34 | } 35 | } 36 | 37 | private suspend fun createSocket(preferences: ByeDpiProxyPreferences): Int = 38 | mutex.withLock { 39 | val result = createSocketFromPreferences(preferences) 40 | 41 | if (result < 0) { 42 | return -1 43 | } 44 | 45 | return result 46 | } 47 | 48 | private fun createSocketFromPreferences(preferences: ByeDpiProxyPreferences) = 49 | when (preferences) { 50 | is ByeDpiProxyCmdPreferences -> jniCreateSocket(preferences.args) 51 | is ByeDpiProxyUIPreferences -> jniCreateSocket(preferences.uiargs) 52 | } 53 | 54 | private external fun jniCreateSocket(args: Array): Int 55 | private external fun jniStartProxy(): Int 56 | private external fun jniStopProxy(): Int 57 | } -------------------------------------------------------------------------------- /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/AppInfo.kt: -------------------------------------------------------------------------------- 1 | package io.github.dovecoteescapee.byedpi.data 2 | 3 | import android.graphics.drawable.Drawable 4 | 5 | data class AppInfo( 6 | val appName: String, 7 | val packageName: String, 8 | val icon: Drawable, 9 | var isSelected: Boolean 10 | ) -------------------------------------------------------------------------------- /app/src/main/java/io/github/dovecoteescapee/byedpi/data/AppSettings.kt: -------------------------------------------------------------------------------- 1 | package io.github.dovecoteescapee.byedpi.data 2 | 3 | data class AppSettings( 4 | val app: String, 5 | val version: String, 6 | val history: List, 7 | val apps: List, 8 | val settings: Map 9 | ) -------------------------------------------------------------------------------- /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/Command.kt: -------------------------------------------------------------------------------- 1 | package io.github.dovecoteescapee.byedpi.data 2 | 3 | data class Command( 4 | val text: String, 5 | var pinned: Boolean = false, 6 | var name: String? = null 7 | ) -------------------------------------------------------------------------------- /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/AppSelectionFragment.kt: -------------------------------------------------------------------------------- 1 | package io.github.dovecoteescapee.byedpi.fragments 2 | 3 | import android.os.Bundle 4 | import android.view.LayoutInflater 5 | import android.view.View 6 | import android.view.ViewGroup 7 | import android.widget.ProgressBar 8 | import android.widget.SearchView 9 | import androidx.fragment.app.Fragment 10 | import androidx.lifecycle.lifecycleScope 11 | import androidx.preference.PreferenceManager 12 | import androidx.recyclerview.widget.LinearLayoutManager 13 | import androidx.recyclerview.widget.RecyclerView 14 | import io.github.dovecoteescapee.byedpi.R 15 | import io.github.dovecoteescapee.byedpi.adapters.AppSelectionAdapter 16 | import io.github.dovecoteescapee.byedpi.data.AppInfo 17 | import kotlinx.coroutines.Dispatchers 18 | import kotlinx.coroutines.launch 19 | import kotlinx.coroutines.withContext 20 | 21 | class AppSelectionFragment : Fragment() { 22 | private lateinit var recyclerView: RecyclerView 23 | private lateinit var searchView: SearchView 24 | private lateinit var progressBar: ProgressBar 25 | private lateinit var adapter: AppSelectionAdapter 26 | 27 | override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { 28 | val view = inflater.inflate(R.layout.app_selection, container, false) 29 | 30 | recyclerView = view.findViewById(R.id.recyclerView) 31 | searchView = view.findViewById(R.id.searchView) 32 | progressBar = view.findViewById(R.id.progressBar) 33 | 34 | setupRecyclerView() 35 | setupSearchView() 36 | 37 | loadApps() 38 | 39 | return view 40 | } 41 | 42 | override fun onDestroyView() { 43 | super.onDestroyView() 44 | recyclerView.adapter = null 45 | searchView.setOnQueryTextListener(null) 46 | } 47 | 48 | private fun setupRecyclerView() { 49 | recyclerView.layoutManager = LinearLayoutManager(context) 50 | } 51 | 52 | private fun setupSearchView() { 53 | searchView.setOnQueryTextListener(object : SearchView.OnQueryTextListener { 54 | override fun onQueryTextSubmit(query: String?): Boolean = false 55 | 56 | override fun onQueryTextChange(newText: String?): Boolean { 57 | adapter.filter.filter(newText) 58 | return true 59 | } 60 | }) 61 | } 62 | 63 | private fun loadApps() { 64 | progressBar.visibility = View.VISIBLE 65 | 66 | lifecycleScope.launch { 67 | val apps = withContext(Dispatchers.IO) { 68 | getInstalledApps() 69 | } 70 | adapter = AppSelectionAdapter(apps) { app, isChecked -> 71 | updateSelectedApps(app.packageName, isChecked) 72 | } 73 | recyclerView.adapter = adapter 74 | progressBar.visibility = View.GONE 75 | searchView.visibility = View.VISIBLE 76 | } 77 | } 78 | 79 | private fun getInstalledApps(): List { 80 | val pm = requireContext().packageManager 81 | val installedApps = pm.getInstalledApplications(0) 82 | val selectedApps = PreferenceManager.getDefaultSharedPreferences(requireContext()) 83 | .getStringSet("selected_apps", setOf()) ?: setOf() 84 | 85 | return installedApps 86 | .filter { it.packageName != requireContext().packageName } 87 | .map { 88 | AppInfo( 89 | pm.getApplicationLabel(it).toString(), 90 | it.packageName, 91 | pm.getApplicationIcon(it.packageName), 92 | selectedApps.contains(it.packageName) 93 | ) 94 | } 95 | .sortedWith(compareBy({ !it.isSelected }, { it.appName.lowercase() })) 96 | } 97 | 98 | private fun updateSelectedApps(packageName: String, isSelected: Boolean) { 99 | val prefs = PreferenceManager.getDefaultSharedPreferences(requireContext()) 100 | val selectedApps = prefs.getStringSet("selected_apps", mutableSetOf())?.toMutableSet() ?: mutableSetOf() 101 | 102 | if (isSelected) { 103 | selectedApps.add(packageName) 104 | } else { 105 | selectedApps.remove(packageName) 106 | } 107 | 108 | prefs.edit().putStringSet("selected_apps", selectedApps).apply() 109 | } 110 | } -------------------------------------------------------------------------------- /app/src/main/java/io/github/dovecoteescapee/byedpi/fragments/ByeDpiCommandLineSettingsFragment.kt: -------------------------------------------------------------------------------- 1 | package io.github.dovecoteescapee.byedpi.fragments 2 | 3 | import android.content.ClipData 4 | import android.content.ClipboardManager 5 | import android.content.Context 6 | import android.os.Bundle 7 | import android.widget.EditText 8 | import android.widget.LinearLayout 9 | import androidx.preference.* 10 | import io.github.dovecoteescapee.byedpi.R 11 | import io.github.dovecoteescapee.byedpi.utility.findPreferenceNotNull 12 | import androidx.appcompat.app.AlertDialog 13 | import io.github.dovecoteescapee.byedpi.data.Command 14 | import io.github.dovecoteescapee.byedpi.utility.HistoryUtils 15 | 16 | class ByeDpiCommandLineSettingsFragment : PreferenceFragmentCompat() { 17 | 18 | private lateinit var cmdHistoryUtils: HistoryUtils 19 | private lateinit var editTextPreference: EditTextPreference 20 | private lateinit var historyCategory: PreferenceCategory 21 | 22 | override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { 23 | setPreferencesFromResource(R.xml.byedpi_cmd_settings, rootKey) 24 | 25 | cmdHistoryUtils = HistoryUtils(requireContext()) 26 | 27 | editTextPreference = findPreferenceNotNull("byedpi_cmd_args") 28 | historyCategory = findPreferenceNotNull("cmd_history_category") 29 | 30 | editTextPreference.setOnPreferenceChangeListener { _, newValue -> 31 | val newCommand = newValue.toString() 32 | if (newCommand.isNotBlank()) cmdHistoryUtils.addCommand(newCommand) 33 | updateHistoryCategory() 34 | true 35 | } 36 | 37 | updateHistoryCategory() 38 | } 39 | 40 | private fun updateHistoryCategory() { 41 | historyCategory.removeAll() 42 | val history = cmdHistoryUtils.getHistory() 43 | 44 | history.sortedWith(compareByDescending { it.pinned }.thenBy { history.indexOf(it) }) 45 | .forEach { command -> 46 | val preference = createPreference(command) 47 | historyCategory.addPreference(preference) 48 | } 49 | } 50 | 51 | private fun createPreference(command: Command) = 52 | Preference(requireContext()).apply { 53 | title = command.text 54 | summary = buildSummary(command) 55 | setOnPreferenceClickListener { 56 | showActionDialog(command) 57 | true 58 | } 59 | } 60 | 61 | private fun buildSummary(command: Command): String { 62 | val summary = StringBuilder() 63 | if (command.name != null) { 64 | summary.append(command.name) 65 | } 66 | if (command.pinned) { 67 | if (summary.isNotEmpty()) summary.append(" - ") 68 | summary.append(context?.getString(R.string.cmd_history_pinned)) 69 | } 70 | return summary.toString() 71 | } 72 | 73 | private fun showActionDialog(command: Command) { 74 | val options = arrayOf( 75 | getString(R.string.cmd_history_apply), 76 | if (command.pinned) getString(R.string.cmd_history_unpin) else getString(R.string.cmd_history_pin), 77 | getString(R.string.cmd_history_rename), 78 | getString(R.string.cmd_history_copy), 79 | getString(R.string.cmd_history_delete) 80 | ) 81 | 82 | AlertDialog.Builder(requireContext()) 83 | .setTitle(getString(R.string.cmd_history_menu)) 84 | .setItems(options) { _, which -> 85 | when (which) { 86 | 0 -> applyCommand(command.text) 87 | 1 -> if (command.pinned) unpinCommand(command.text) else pinCommand(command.text) 88 | 2 -> showRenameDialog(command) 89 | 3 -> copyToClipboard(command.text) 90 | 4 -> deleteCommand(command.text) 91 | } 92 | } 93 | .show() 94 | } 95 | 96 | private fun showRenameDialog(command: Command) { 97 | val input = EditText(requireContext()).apply { 98 | setText(command.name) 99 | } 100 | 101 | val container = LinearLayout(context).apply { 102 | orientation = LinearLayout.VERTICAL 103 | setPadding(50, 20, 50, 20) 104 | addView(input) 105 | } 106 | 107 | AlertDialog.Builder(requireContext()) 108 | .setTitle(getString(R.string.cmd_history_rename)) 109 | .setView(container) 110 | .setPositiveButton(getString(android.R.string.ok)) { _, _ -> 111 | val newName = input.text.toString() 112 | cmdHistoryUtils.renameCommand(command.text, newName) 113 | updateHistoryCategory() 114 | } 115 | .setNegativeButton(getString(android.R.string.cancel), null) 116 | .show() 117 | } 118 | 119 | private fun applyCommand(command: String) { 120 | editTextPreference.text = command 121 | } 122 | 123 | private fun pinCommand(command: String) { 124 | cmdHistoryUtils.pinCommand(command) 125 | updateHistoryCategory() 126 | } 127 | 128 | private fun unpinCommand(command: String) { 129 | cmdHistoryUtils.unpinCommand(command) 130 | updateHistoryCategory() 131 | } 132 | 133 | private fun deleteCommand(command: String) { 134 | cmdHistoryUtils.deleteCommand(command) 135 | updateHistoryCategory() 136 | } 137 | 138 | private fun copyToClipboard(command: String) { 139 | val clipboard = requireContext().getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager 140 | val clip = ClipData.newPlainText("Command", command) 141 | clipboard.setPrimaryClip(clip) 142 | } 143 | } -------------------------------------------------------------------------------- /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 | setEditTestPreferenceListenerInt( 23 | "byedpi_max_connections", 24 | 1, 25 | Short.MAX_VALUE.toInt() 26 | ) 27 | setEditTestPreferenceListenerInt( 28 | "byedpi_buffer_size", 29 | 1, 30 | Int.MAX_VALUE / 4 31 | ) 32 | setEditTestPreferenceListenerInt("byedpi_default_ttl", 0, 255) 33 | setEditTestPreferenceListenerInt( 34 | "byedpi_split_position", 35 | Int.MIN_VALUE, 36 | Int.MAX_VALUE 37 | ) 38 | setEditTestPreferenceListenerInt("byedpi_fake_ttl", 1, 255) 39 | setEditTestPreferenceListenerInt( 40 | "byedpi_tlsrec_position", 41 | 2 * Short.MIN_VALUE, 42 | 2 * Short.MAX_VALUE, 43 | ) 44 | 45 | findPreferenceNotNull("byedpi_oob_data") 46 | .setOnBindEditTextListener { 47 | it.filters = arrayOf(android.text.InputFilter.LengthFilter(1)) 48 | } 49 | 50 | updatePreferences() 51 | } 52 | 53 | override fun onResume() { 54 | super.onResume() 55 | sharedPreferences?.registerOnSharedPreferenceChangeListener(preferenceListener) 56 | } 57 | 58 | override fun onPause() { 59 | super.onPause() 60 | sharedPreferences?.unregisterOnSharedPreferenceChangeListener(preferenceListener) 61 | } 62 | 63 | private fun updatePreferences() { 64 | val desyncMethod = 65 | findPreferenceNotNull("byedpi_desync_method") 66 | .value.let { ByeDpiProxyUIPreferences.DesyncMethod.fromName(it) } 67 | val hostsMode = findPreferenceNotNull("byedpi_hosts_mode") 68 | .value.let { ByeDpiProxyUIPreferences.HostsMode.fromName(it) } 69 | 70 | val hostsBlacklist = findPreferenceNotNull("byedpi_hosts_blacklist") 71 | val hostsWhitelist = findPreferenceNotNull("byedpi_hosts_whitelist") 72 | val desyncHttp = findPreferenceNotNull("byedpi_desync_http") 73 | val desyncHttps = findPreferenceNotNull("byedpi_desync_https") 74 | val desyncUdp = findPreferenceNotNull("byedpi_desync_udp") 75 | val splitPosition = findPreferenceNotNull("byedpi_split_position") 76 | val splitAtHost = findPreferenceNotNull("byedpi_split_at_host") 77 | val ttlFake = findPreferenceNotNull("byedpi_fake_ttl") 78 | val fakeSni = findPreferenceNotNull("byedpi_fake_sni") 79 | val fakeOffset = findPreferenceNotNull("byedpi_fake_offset") 80 | val oobChar = findPreferenceNotNull("byedpi_oob_data") 81 | val udpFakeCount = findPreferenceNotNull("byedpi_udp_fake_count") 82 | val hostMixedCase = findPreferenceNotNull("byedpi_host_mixed_case") 83 | val domainMixedCase = findPreferenceNotNull("byedpi_domain_mixed_case") 84 | val hostRemoveSpaces = 85 | findPreferenceNotNull("byedpi_host_remove_spaces") 86 | val splitTlsRec = findPreferenceNotNull("byedpi_tlsrec_enabled") 87 | val splitTlsRecPosition = 88 | findPreferenceNotNull("byedpi_tlsrec_position") 89 | val splitTlsRecAtSni = findPreferenceNotNull("byedpi_tlsrec_at_sni") 90 | 91 | hostsBlacklist.isVisible = hostsMode == Blacklist 92 | hostsWhitelist.isVisible = hostsMode == Whitelist 93 | 94 | val desyncEnabled = desyncMethod != None 95 | splitPosition.isVisible = desyncEnabled 96 | splitAtHost.isVisible = desyncEnabled 97 | 98 | val isFake = desyncMethod == Fake 99 | ttlFake.isVisible = isFake 100 | fakeSni.isVisible = isFake 101 | fakeOffset.isVisible = isFake 102 | 103 | val isOob = desyncMethod == OOB || desyncMethod == DISOOB 104 | oobChar.isVisible = isOob 105 | 106 | val desyncAllProtocols = 107 | !desyncHttp.isChecked && !desyncHttps.isChecked && !desyncUdp.isChecked 108 | 109 | val desyncHttpEnabled = desyncAllProtocols || desyncHttp.isChecked 110 | hostMixedCase.isEnabled = desyncHttpEnabled 111 | domainMixedCase.isEnabled = desyncHttpEnabled 112 | hostRemoveSpaces.isEnabled = desyncHttpEnabled 113 | 114 | val desyncUdpEnabled = desyncAllProtocols || desyncUdp.isChecked 115 | udpFakeCount.isEnabled = desyncUdpEnabled 116 | 117 | val desyncHttpsEnabled = desyncAllProtocols || desyncHttps.isChecked 118 | splitTlsRec.isEnabled = desyncHttpsEnabled 119 | val tlsRecEnabled = desyncHttpsEnabled && splitTlsRec.isChecked 120 | splitTlsRecPosition.isEnabled = tlsRecEnabled 121 | splitTlsRecAtSni.isEnabled = tlsRecEnabled 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /app/src/main/java/io/github/dovecoteescapee/byedpi/fragments/MainSettingsFragment.kt: -------------------------------------------------------------------------------- 1 | package io.github.dovecoteescapee.byedpi.fragments 2 | 3 | import android.content.Intent 4 | import android.content.SharedPreferences 5 | import android.os.Bundle 6 | import android.util.Log 7 | import androidx.appcompat.app.AppCompatDelegate 8 | import androidx.core.os.LocaleListCompat 9 | import androidx.preference.* 10 | import io.github.dovecoteescapee.byedpi.BuildConfig 11 | import io.github.dovecoteescapee.byedpi.R 12 | import io.github.dovecoteescapee.byedpi.activities.TestActivity 13 | import io.github.dovecoteescapee.byedpi.data.Mode 14 | import io.github.dovecoteescapee.byedpi.utility.* 15 | 16 | class MainSettingsFragment : PreferenceFragmentCompat() { 17 | companion object { 18 | private val TAG: String = MainSettingsFragment::class.java.simpleName 19 | 20 | fun setLang(lang: String) { 21 | val appLocale = localeByName(lang) ?: throw IllegalStateException("Invalid value for language: $lang") 22 | AppCompatDelegate.setApplicationLocales(appLocale) 23 | } 24 | 25 | private fun localeByName(lang: String): LocaleListCompat? = when (lang) { 26 | "system" -> LocaleListCompat.getEmptyLocaleList() 27 | "ru" -> LocaleListCompat.forLanguageTags("ru") 28 | "en" -> LocaleListCompat.forLanguageTags("en") 29 | "tr" -> LocaleListCompat.forLanguageTags("tr") 30 | else -> { 31 | Log.w(TAG, "Invalid value for language: $lang") 32 | null 33 | } 34 | } 35 | 36 | fun setTheme(name: String) = 37 | themeByName(name)?.let { 38 | AppCompatDelegate.setDefaultNightMode(it) 39 | } ?: throw IllegalStateException("Invalid value for app_theme: $name") 40 | 41 | private fun themeByName(name: String): Int? = when (name) { 42 | "system" -> AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM 43 | "light" -> AppCompatDelegate.MODE_NIGHT_NO 44 | "dark" -> AppCompatDelegate.MODE_NIGHT_YES 45 | else -> { 46 | Log.w(TAG, "Invalid value for app_theme: $name") 47 | null 48 | } 49 | } 50 | } 51 | 52 | private val preferenceListener = 53 | SharedPreferences.OnSharedPreferenceChangeListener { _, _ -> 54 | updatePreferences() 55 | } 56 | 57 | override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { 58 | setPreferencesFromResource(R.xml.main_settings, rootKey) 59 | 60 | setEditTextPreferenceListener("byedpi_proxy_ip") { checkIp(it) } 61 | setEditTestPreferenceListenerPort("byedpi_proxy_port") 62 | 63 | setEditTextPreferenceListener("dns_ip") { 64 | it.isBlank() || checkNotLocalIp(it) 65 | } 66 | 67 | findPreferenceNotNull("language") 68 | .setOnPreferenceChangeListener { _, newValue -> 69 | setLang(newValue as String) 70 | true 71 | } 72 | 73 | findPreferenceNotNull("app_theme") 74 | .setOnPreferenceChangeListener { _, newValue -> 75 | setTheme(newValue as String) 76 | true 77 | } 78 | 79 | val switchCmdSettings = findPreferenceNotNull("byedpi_enable_cmd_settings") 80 | val uiSettings = findPreferenceNotNull("byedpi_ui_settings") 81 | val cmdSettings = findPreferenceNotNull("byedpi_cmd_settings") 82 | val proxyTest = findPreferenceNotNull("proxy_test") 83 | 84 | val setByeDpiSettingsMode = { enable: Boolean -> 85 | uiSettings.isEnabled = !enable 86 | cmdSettings.isEnabled = enable 87 | proxyTest.isEnabled = enable 88 | } 89 | 90 | setByeDpiSettingsMode(switchCmdSettings.isChecked) 91 | 92 | switchCmdSettings.setOnPreferenceChangeListener { _, newValue -> 93 | setByeDpiSettingsMode(newValue as Boolean) 94 | updatePreferences() 95 | true 96 | } 97 | 98 | findPreferenceNotNull("proxy_test") 99 | .setOnPreferenceClickListener { 100 | val intent = Intent(context, TestActivity::class.java) 101 | startActivity(intent) 102 | true 103 | } 104 | 105 | findPreferenceNotNull("version").summary = BuildConfig.VERSION_NAME 106 | 107 | updatePreferences() 108 | } 109 | 110 | override fun onResume() { 111 | super.onResume() 112 | sharedPreferences?.registerOnSharedPreferenceChangeListener(preferenceListener) 113 | updatePreferences() 114 | } 115 | 116 | override fun onPause() { 117 | super.onPause() 118 | sharedPreferences?.unregisterOnSharedPreferenceChangeListener(preferenceListener) 119 | } 120 | 121 | private fun updatePreferences() { 122 | val mode = findPreferenceNotNull("byedpi_mode").value.let { Mode.fromString(it) } 123 | val dns = findPreferenceNotNull("dns_ip") 124 | val ipv6 = findPreferenceNotNull("ipv6_enable") 125 | val proxy = findPreferenceNotNull("byedpi_proxy_category") 126 | 127 | val applistType = findPreferenceNotNull("applist_type") 128 | val selectedApps = findPreferenceNotNull("selected_apps") 129 | 130 | if (sharedPreferences?.getBoolean("byedpi_enable_cmd_settings", false) == true) { 131 | val (cmdIp, cmdPort) = sharedPreferences?.checkIpAndPortInCmd() ?: Pair(null, null) 132 | proxy.isVisible = cmdIp == null && cmdPort == null 133 | } else { 134 | proxy.isVisible = true 135 | } 136 | 137 | when (mode) { 138 | Mode.VPN -> { 139 | dns.isVisible = true 140 | ipv6.isVisible = true 141 | 142 | when (applistType.value) { 143 | "disable" -> { 144 | applistType.isVisible = true 145 | selectedApps.isVisible = false 146 | } 147 | "blacklist", "whitelist" -> { 148 | applistType.isVisible = true 149 | selectedApps.isVisible = true 150 | } 151 | else -> { 152 | applistType.isVisible = true 153 | selectedApps.isVisible = false 154 | Log.w(TAG, "Unexpected applistType value: ${applistType.value}") 155 | } 156 | } 157 | } 158 | 159 | Mode.Proxy -> { 160 | dns.isVisible = false 161 | ipv6.isVisible = false 162 | applistType.isVisible = false 163 | selectedApps.isVisible = false 164 | } 165 | } 166 | } 167 | } 168 | -------------------------------------------------------------------------------- /app/src/main/java/io/github/dovecoteescapee/byedpi/fragments/ProxyTestSettingsFragment.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.utility.* 8 | 9 | class ProxyTestSettingsFragment : PreferenceFragmentCompat() { 10 | 11 | private val preferenceListener = 12 | SharedPreferences.OnSharedPreferenceChangeListener { _, _ -> 13 | updatePreferences() 14 | } 15 | 16 | override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { 17 | setPreferencesFromResource(R.xml.proxy_test_settings, rootKey) 18 | updatePreferences() 19 | } 20 | 21 | override fun onResume() { 22 | super.onResume() 23 | sharedPreferences?.registerOnSharedPreferenceChangeListener(preferenceListener) 24 | } 25 | 26 | override fun onPause() { 27 | super.onPause() 28 | sharedPreferences?.unregisterOnSharedPreferenceChangeListener(preferenceListener) 29 | } 30 | 31 | private fun updatePreferences() { 32 | val switchUserDomains = findPreferenceNotNull("byedpi_proxytest_userdomains") 33 | val switchUserCommands = findPreferenceNotNull("byedpi_proxytest_usercommands") 34 | val textUserDomains = findPreferenceNotNull("byedpi_proxytest_domains") 35 | val textUserCommands = findPreferenceNotNull("byedpi_proxytest_commands") 36 | 37 | val setUserDomains = { enable: Boolean -> textUserDomains.isEnabled = enable } 38 | val setUserCommands = { enable: Boolean -> textUserCommands.isEnabled = enable } 39 | 40 | setUserDomains(switchUserDomains.isChecked) 41 | setUserCommands(switchUserCommands.isChecked) 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /app/src/main/java/io/github/dovecoteescapee/byedpi/receiver/BootReceiver.kt: -------------------------------------------------------------------------------- 1 | package io.github.dovecoteescapee.byedpi.receiver 2 | 3 | import android.content.BroadcastReceiver 4 | import android.content.Context 5 | import android.content.Intent 6 | import android.net.VpnService 7 | import android.os.SystemClock 8 | import io.github.dovecoteescapee.byedpi.data.Mode 9 | import io.github.dovecoteescapee.byedpi.services.ServiceManager 10 | import io.github.dovecoteescapee.byedpi.utility.getPreferences 11 | import io.github.dovecoteescapee.byedpi.utility.mode 12 | 13 | class BootReceiver : BroadcastReceiver() { 14 | override fun onReceive(context: Context, intent: Intent) { 15 | if (intent.action == Intent.ACTION_BOOT_COMPLETED || 16 | intent.action == Intent.ACTION_REBOOT || 17 | intent.action == "android.intent.action.QUICKBOOT_POWERON") { 18 | 19 | // for A15, todo: use wasForceStopped 20 | if (SystemClock.elapsedRealtime() > 5 * 60 * 1000) { 21 | return 22 | } 23 | 24 | val preferences = context.getPreferences() 25 | val autorunEnabled = preferences.getBoolean("autostart", false) 26 | 27 | if(autorunEnabled) { 28 | when (preferences.mode()) { 29 | Mode.VPN -> { 30 | if (VpnService.prepare(context) == null) { 31 | ServiceManager.start(context, Mode.VPN) 32 | } 33 | } 34 | 35 | Mode.Proxy -> ServiceManager.start(context, Mode.Proxy) 36 | } 37 | } 38 | } 39 | } 40 | } -------------------------------------------------------------------------------- /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 | fun getStatus(): ServiceStatus { 35 | return status 36 | } 37 | } 38 | 39 | override fun onCreate() { 40 | super.onCreate() 41 | registerNotificationChannel( 42 | this, 43 | NOTIFICATION_CHANNEL_ID, 44 | R.string.proxy_channel_name, 45 | ) 46 | } 47 | 48 | override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { 49 | super.onStartCommand(intent, flags, startId) 50 | return when (val action = intent?.action) { 51 | START_ACTION -> { 52 | lifecycleScope.launch { start() } 53 | START_STICKY 54 | } 55 | 56 | STOP_ACTION -> { 57 | lifecycleScope.launch { stop() } 58 | START_NOT_STICKY 59 | } 60 | 61 | else -> { 62 | Log.w(TAG, "Unknown action: $action") 63 | START_NOT_STICKY 64 | } 65 | } 66 | } 67 | 68 | private suspend fun start() { 69 | Log.i(TAG, "Starting") 70 | 71 | if (status == ServiceStatus.Connected) { 72 | Log.w(TAG, "Proxy already connected") 73 | return 74 | } 75 | 76 | try { 77 | startForeground() 78 | mutex.withLock { 79 | startProxy() 80 | } 81 | updateStatus(ServiceStatus.Connected) 82 | } catch (e: Exception) { 83 | Log.e(TAG, "Failed to start proxy", e) 84 | updateStatus(ServiceStatus.Failed) 85 | stop() 86 | } 87 | } 88 | 89 | private fun startForeground() { 90 | val notification: Notification = createNotification() 91 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) { 92 | startForeground( 93 | FOREGROUND_SERVICE_ID, 94 | notification, 95 | FOREGROUND_SERVICE_TYPE_SPECIAL_USE, 96 | ) 97 | } else { 98 | startForeground(FOREGROUND_SERVICE_ID, notification) 99 | } 100 | } 101 | 102 | private suspend fun stop() { 103 | Log.i(TAG, "Stopping") 104 | 105 | mutex.withLock { 106 | stopProxy() 107 | } 108 | 109 | updateStatus(ServiceStatus.Disconnected) 110 | stopSelf() 111 | } 112 | 113 | private suspend fun startProxy() { 114 | Log.i(TAG, "Starting proxy") 115 | 116 | if (proxyJob != null) { 117 | Log.w(TAG, "Proxy fields not null") 118 | throw IllegalStateException("Proxy fields not null") 119 | } 120 | 121 | proxy = ByeDpiProxy() 122 | val preferences = getByeDpiPreferences() 123 | 124 | proxyJob = lifecycleScope.launch(Dispatchers.IO) { 125 | val code = proxy.startProxy(preferences) 126 | 127 | withContext(Dispatchers.Main) { 128 | if (code != 0) { 129 | Log.e(TAG, "Proxy stopped with code $code") 130 | updateStatus(ServiceStatus.Failed) 131 | } else { 132 | updateStatus(ServiceStatus.Disconnected) 133 | } 134 | } 135 | } 136 | 137 | Log.i(TAG, "Proxy started") 138 | } 139 | 140 | private suspend fun stopProxy() { 141 | Log.i(TAG, "Stopping proxy") 142 | 143 | if (status == ServiceStatus.Disconnected) { 144 | Log.w(TAG, "Proxy already disconnected") 145 | return 146 | } 147 | 148 | try { 149 | proxy.stopProxy() 150 | proxyJob?.join() 151 | proxyJob = null 152 | } catch (e: Exception) { 153 | Log.e(TAG, "Failed to close proxyJob", e) 154 | } 155 | 156 | Log.i(TAG, "Proxy stopped") 157 | } 158 | 159 | private fun getByeDpiPreferences(): ByeDpiProxyPreferences = 160 | ByeDpiProxyPreferences.fromSharedPreferences(getPreferences()) 161 | 162 | private fun updateStatus(newStatus: ServiceStatus) { 163 | Log.d(TAG, "Proxy status changed from $status to $newStatus") 164 | 165 | status = newStatus 166 | 167 | setStatus( 168 | when (newStatus) { 169 | ServiceStatus.Connected -> AppStatus.Running 170 | ServiceStatus.Disconnected, 171 | ServiceStatus.Failed -> { 172 | proxyJob = null 173 | AppStatus.Halted 174 | } 175 | }, 176 | Mode.Proxy 177 | ) 178 | 179 | val intent = Intent( 180 | when (newStatus) { 181 | ServiceStatus.Connected -> STARTED_BROADCAST 182 | ServiceStatus.Disconnected -> STOPPED_BROADCAST 183 | ServiceStatus.Failed -> FAILED_BROADCAST 184 | } 185 | ) 186 | intent.putExtra(SENDER, Sender.Proxy.ordinal) 187 | sendBroadcast(intent) 188 | } 189 | 190 | private fun createNotification(): Notification = 191 | createConnectionNotification( 192 | this, 193 | NOTIFICATION_CHANNEL_ID, 194 | R.string.notification_title, 195 | R.string.proxy_notification_content, 196 | ByeDpiProxyService::class.java, 197 | ) 198 | } 199 | -------------------------------------------------------------------------------- /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/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.content.ComponentName 4 | import android.content.Context 5 | import android.net.VpnService 6 | import android.os.Build 7 | import android.service.quicksettings.Tile 8 | import android.service.quicksettings.TileService 9 | import android.util.Log 10 | import androidx.annotation.RequiresApi 11 | import io.github.dovecoteescapee.byedpi.data.* 12 | import io.github.dovecoteescapee.byedpi.utility.getPreferences 13 | import io.github.dovecoteescapee.byedpi.utility.mode 14 | 15 | @RequiresApi(Build.VERSION_CODES.N) 16 | class QuickTileService : TileService() { 17 | 18 | companion object { 19 | private const val TAG = "QuickTileService" 20 | 21 | fun updateTile(context: Context) { 22 | requestListeningState(context, ComponentName(context, QuickTileService::class.java)) 23 | } 24 | } 25 | 26 | override fun onStartListening() { 27 | super.onStartListening() 28 | updateStatus() 29 | } 30 | 31 | override fun onClick() { 32 | if (qsTile.state == Tile.STATE_UNAVAILABLE) return 33 | 34 | unlockAndRun { handleClick() } 35 | } 36 | 37 | private fun handleClick() { 38 | val (status) = appStatus 39 | when (status) { 40 | AppStatus.Halted -> { 41 | val mode = getPreferences().mode() 42 | 43 | if (mode == Mode.VPN && VpnService.prepare(this) != null) { 44 | return 45 | } 46 | 47 | ServiceManager.start(this, mode) 48 | setState(Tile.STATE_ACTIVE) 49 | } 50 | AppStatus.Running -> { 51 | ServiceManager.stop(this) 52 | setState(Tile.STATE_INACTIVE) 53 | } 54 | } 55 | 56 | Log.i(TAG, "Toggle tile") 57 | updateTile(this) 58 | } 59 | 60 | private fun updateStatus() { 61 | val (status) = appStatus 62 | 63 | if (status == AppStatus.Running) { 64 | setState(Tile.STATE_ACTIVE) 65 | } else { 66 | setState(Tile.STATE_INACTIVE) 67 | } 68 | } 69 | 70 | private fun setState(newState: Int) { 71 | qsTile.apply { 72 | state = newState 73 | updateTile() 74 | } 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /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 quoteChar = ' ' 7 | var escaping = false 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) { 19 | if (i + 1 < string.length && string[i + 1] == quoteChar) { 20 | escaping = true 21 | } else { 22 | current.append(c) 23 | } 24 | } else if (quoting && c == quoteChar) { 25 | quoting = false 26 | lastCloseQuoteIndex = i 27 | } else if (!quoting && (c == '\'' || c == '"')) { 28 | quoting = true 29 | quoteChar = c 30 | } else if (!quoting && Character.isWhitespace(c)) { 31 | if (current.isNotEmpty() || lastCloseQuoteIndex == i - 1) { 32 | tokens.add(current.toString()) 33 | current = StringBuilder() 34 | } 35 | } else { 36 | current.append(c) 37 | } 38 | } 39 | 40 | if (current.isNotEmpty() || lastCloseQuoteIndex == string.length - 1) { 41 | tokens.add(current.toString()) 42 | } 43 | 44 | return tokens 45 | } 46 | -------------------------------------------------------------------------------- /app/src/main/java/io/github/dovecoteescapee/byedpi/utility/GoogleVideoUtils.kt: -------------------------------------------------------------------------------- 1 | package io.github.dovecoteescapee.byedpi.utility 2 | 3 | import android.util.Log 4 | import kotlinx.coroutines.Dispatchers 5 | import kotlinx.coroutines.withContext 6 | import java.io.BufferedReader 7 | import java.io.InputStreamReader 8 | import java.net.HttpURLConnection 9 | import java.net.URL 10 | 11 | class GoogleVideoUtils { 12 | 13 | companion object { 14 | private val lettersListA = listOf( 15 | 'u', 'z', 'p', 'k', 'f', 'a', '5', '0', 'v', 'q', 'l', 'g', 16 | 'b', '6', '1', 'w', 'r', 'm', 'h', 'c', '7', '2', 'x', 's', 17 | 'n', 'i', 'd', '8', '3', 'y', 't', 'o', 'j', 'e', '9', '4', '-' 18 | ) 19 | private val lettersListB = listOf( 20 | '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 21 | 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 22 | 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', '-' 23 | ) 24 | private val lettersMap = lettersListA.zip(lettersListB).toMap() 25 | } 26 | 27 | suspend fun generateGoogleVideoDomain(): String? { 28 | val clusterCodename = getClusterCodename() 29 | if (clusterCodename == null) { 30 | Log.e("AutoGCS", "Failed to obtain cluster codename") 31 | return null 32 | } 33 | Log.i("AutoGCS", "Cluster codename: $clusterCodename") 34 | val clusterName = convertClusterCodename(clusterCodename) 35 | Log.i("AutoGCS", "Cluster name: $clusterName") 36 | val autoGCS = buildAutoGCS(clusterName) 37 | Log.i("AutoGCS", "Generated domain: $autoGCS") 38 | return autoGCS 39 | } 40 | 41 | private suspend fun getClusterCodename(): String? { 42 | val urls = listOf( 43 | "https://redirector.gvt1.com/report_mapping?di=no", 44 | "https://redirector.googlevideo.com/report_mapping?di=no" 45 | ) 46 | for (url in urls) { 47 | try { 48 | val responseBody = withContext(Dispatchers.IO) { httpGet(url) } 49 | if (responseBody != null) { 50 | val clusterCodename = extractClusterCodenameFromBody(responseBody) 51 | if (clusterCodename != null) { 52 | return clusterCodename 53 | } 54 | } 55 | } catch (e: Exception) { 56 | Log.e("ClusterCodename", "Error fetching cluster codename from $url", e) 57 | } 58 | } 59 | return null 60 | } 61 | 62 | private fun extractClusterCodenameFromBody(body: String): String? { 63 | val regex = Regex("""=>\s*(\S+)\s*(?:\(|:)""") 64 | val matchResult = regex.find(body) 65 | val codename = matchResult?.groupValues?.get(1) 66 | if (codename != null) { 67 | return codename.trimEnd(':', ' ') 68 | } 69 | return null 70 | } 71 | 72 | private fun convertClusterCodename(clusterCodename: String): String { 73 | val clusterNameBuilder = StringBuilder() 74 | for (char in clusterCodename) { 75 | val mappedChar = lettersMap[char] 76 | if (mappedChar != null) { 77 | clusterNameBuilder.append(mappedChar) 78 | } else { 79 | Log.w("ClusterCodename", "Character '$char' not found in mapping") 80 | } 81 | } 82 | return clusterNameBuilder.toString() 83 | } 84 | 85 | private fun buildAutoGCS(clusterName: String): String { 86 | return "rr1---sn-$clusterName.googlevideo.com" 87 | } 88 | 89 | private fun httpGet(urlStr: String): String? { 90 | var connection: HttpURLConnection? = null 91 | return try { 92 | val url = URL(urlStr) 93 | connection = url.openConnection() as HttpURLConnection 94 | connection.requestMethod = "GET" 95 | connection.connectTimeout = 2000 96 | connection.readTimeout = 2000 97 | 98 | val responseCode = connection.responseCode 99 | if (responseCode == HttpURLConnection.HTTP_OK) { 100 | val inputStream = connection.inputStream 101 | val reader = BufferedReader(InputStreamReader(inputStream)) 102 | val response = reader.readText() 103 | reader.close() 104 | response 105 | } else { 106 | Log.e("HTTPGet", "Non-OK response code: $responseCode") 107 | null 108 | } 109 | } catch (e: Exception) { 110 | Log.e("HTTPGet", "Exception during HTTP GET", e) 111 | null 112 | } finally { 113 | connection?.disconnect() 114 | } 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /app/src/main/java/io/github/dovecoteescapee/byedpi/utility/HistoryUtils.kt: -------------------------------------------------------------------------------- 1 | package io.github.dovecoteescapee.byedpi.utility 2 | 3 | import android.content.Context 4 | import android.content.SharedPreferences 5 | import androidx.preference.PreferenceManager 6 | import io.github.dovecoteescapee.byedpi.data.Command 7 | import com.google.gson.Gson 8 | 9 | class HistoryUtils(context: Context) { 10 | 11 | private val sharedPreferences: SharedPreferences = PreferenceManager.getDefaultSharedPreferences(context) 12 | private val historyKey = "byedpi_command_history" 13 | private val maxHistorySize = 40 14 | 15 | fun addCommand(command: String) { 16 | if (command.isBlank()) return 17 | 18 | val history = getHistory().toMutableList() 19 | val search = history.find { it.text == command } 20 | 21 | if (search == null) { 22 | history.add(0, Command(command)) 23 | if (history.size > maxHistorySize) { 24 | history.removeAt(maxHistorySize) 25 | } 26 | } 27 | 28 | saveHistory(history) 29 | } 30 | 31 | fun pinCommand(command: String) { 32 | val history = getHistory().toMutableList() 33 | history.find { it.text == command }?.pinned = true 34 | saveHistory(history) 35 | } 36 | 37 | fun unpinCommand(command: String) { 38 | val history = getHistory().toMutableList() 39 | history.find { it.text == command }?.pinned = false 40 | saveHistory(history) 41 | } 42 | 43 | fun deleteCommand(command: String) { 44 | val history = getHistory().toMutableList() 45 | history.removeAll { it.text == command } 46 | saveHistory(history) 47 | } 48 | 49 | fun renameCommand(command: String, newName: String) { 50 | val history = getHistory().toMutableList() 51 | history.find { it.text == command }?.name = newName 52 | saveHistory(history) 53 | } 54 | 55 | fun getHistory(): List { 56 | val historyJson = sharedPreferences.getString(historyKey, null) 57 | return if (historyJson != null) { 58 | Gson().fromJson(historyJson, Array::class.java).toList() 59 | } else { 60 | emptyList() 61 | } 62 | } 63 | 64 | fun saveHistory(history: List) { 65 | val historyJson = Gson().toJson(history) 66 | sharedPreferences.edit().putString(historyKey, historyJson).apply() 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /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 | 25 | fun SharedPreferences.getSelectedApps(): List { 26 | return getStringSet("selected_apps", emptySet())?.toList() ?: emptyList() 27 | } 28 | 29 | fun SharedPreferences.checkIpAndPortInCmd(): Pair { 30 | val cmdEnable = getBoolean("byedpi_enable_cmd_settings", false) 31 | if (!cmdEnable) return Pair(null, null) 32 | 33 | val cmdArgs = getString("byedpi_cmd_args", "")?.let { shellSplit(it) } ?: emptyList() 34 | 35 | fun getArgValue(argsList: List, keys: List): String? { 36 | for (i in argsList.indices) { 37 | val arg = argsList[i] 38 | for (key in keys) { 39 | if (key.startsWith("--")) { 40 | if (arg == key && i + 1 < argsList.size) { 41 | return argsList[i + 1] 42 | } else if (arg.startsWith("$key=")) { 43 | return arg.substringAfter('=') 44 | } 45 | } else if (key.startsWith("-")) { 46 | if (arg.startsWith(key) && arg.length > key.length) { 47 | return arg.substring(key.length) 48 | } else if (arg == key && i + 1 < argsList.size) { 49 | return argsList[i + 1] 50 | } 51 | } 52 | } 53 | } 54 | return null 55 | } 56 | 57 | val cmdIp = getArgValue(cmdArgs, listOf("--ip", "-i")) 58 | val cmdPort = getArgValue(cmdArgs, listOf("--port", "-p")) 59 | 60 | return Pair(cmdIp, cmdPort) 61 | } 62 | 63 | 64 | fun SharedPreferences.getProxyIpAndPort(): Pair { 65 | val (cmdIp, cmdPort) = checkIpAndPortInCmd() 66 | 67 | val ip = cmdIp ?: getStringNotNull("byedpi_proxy_ip", "127.0.0.1") 68 | val port = cmdPort ?: getStringNotNull("byedpi_proxy_port", "1080") 69 | 70 | return Pair(ip, port) 71 | } 72 | -------------------------------------------------------------------------------- /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_telegram.xml: -------------------------------------------------------------------------------- 1 | 7 | 9 | 10 | 16 | 17 | 18 | 19 | 20 | 21 | 24 | 25 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_toggle.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_main.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 23 | 24 | 32 | 33 | 40 | 41 | 52 | 53 | 54 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_proxy_test.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 |