diff --git a/.idea/render.experimental.xml b/.idea/render.experimental.xml
new file mode 100644
index 0000000..8ec256a
--- /dev/null
+++ b/.idea/render.experimental.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/build.gradle.kts b/app/build.gradle.kts
index db05b74..aba15c1 100644
--- a/app/build.gradle.kts
+++ b/app/build.gradle.kts
@@ -11,19 +11,28 @@ android {
defaultConfig {
applicationId = "io.github.dovecoteescapee.byedpi"
- minSdk = 24
+ minSdk = 21
targetSdk = 34
- versionCode = 2
- versionName = "0.1.1-alpha"
+ versionCode = 3
+ versionName = "1.0.0-rc1"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
}
+ buildFeatures {
+ buildConfig = true
+ }
+
buildTypes {
release {
+ buildConfigField("String", "VERSION_NAME", "\"${defaultConfig.versionName}\"")
+
isMinifyEnabled = false
proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro")
}
+ debug {
+ buildConfigField("String", "VERSION_NAME", "\"${defaultConfig.versionName}-debug\"")
+ }
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_1_8
@@ -72,7 +81,7 @@ abstract class BuildTun2Socks : DefaultTask() {
}
project.exec {
workingDir = tun2socksDir
- commandLine("gomobile", "bind", "-o", tun2socksOutput, "./engine")
+ commandLine("gomobile", "bind", "-o", tun2socksOutput, "-trimpath", "./engine")
}
}
}
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index fc8effe..95bf82d 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -3,9 +3,9 @@
xmlns:tools="http://schemas.android.com/tools">
+
-
+ tools:targetApi="34">
+ android:name=".activities.MainActivity"
+ android:exported="true"
+ android:launchMode="singleInstance">
@@ -28,16 +29,31 @@
+ android:exported="true"/>
-
+ android:foregroundServiceType="specialUse"
+ android:exported="false">
+
+
+
+
+
+
diff --git a/app/src/main/cpp/CMakeLists.txt b/app/src/main/cpp/CMakeLists.txt
index 351b18e..d034952 100644
--- a/app/src/main/cpp/CMakeLists.txt
+++ b/app/src/main/cpp/CMakeLists.txt
@@ -30,6 +30,7 @@ add_library(${CMAKE_PROJECT_NAME} SHARED
byedpi/desync.c
byedpi/packets.c
byedpi/proxy.c
+ byedpi/main.c
native-lib.c
)
@@ -37,6 +38,8 @@ include_directories("byedpi")
set(CMAKE_C_FLAGS "-std=c99 -O2 -D_XOPEN_SOURCE=500")
+add_compile_definitions(ANDROID_APP)
+
# Specifies libraries CMake should link to your target library. You
# can link libraries from various origins, such as libraries defined in this
# build script, prebuilt third-party libraries, or Android system libraries.
diff --git a/app/src/main/cpp/byedpi b/app/src/main/cpp/byedpi
index d880b04..12adfe2 160000
--- a/app/src/main/cpp/byedpi
+++ b/app/src/main/cpp/byedpi
@@ -1 +1 @@
-Subproject commit d880b0441f8c31e0609fb98be90d722190cd737b
+Subproject commit 12adfe285f603bfa38c19e9057d897b2b4e40941
diff --git a/app/src/main/cpp/native-lib.c b/app/src/main/cpp/native-lib.c
index 1d538e4..9371211 100644
--- a/app/src/main/cpp/native-lib.c
+++ b/app/src/main/cpp/native-lib.c
@@ -2,74 +2,40 @@
#include
#include
#include
+#include
+#include
+#include
+#include
+
#include
#include
-#include
-#include
-#include
-extern int NOT_EXIT;
-
-struct packet fake_tls = {
- sizeof(tls_data), tls_data
-},
- fake_http = {
- sizeof(http_data), http_data
+const enum demode DESYNC_METHODS[] = {
+ DESYNC_NONE,
+ DESYNC_SPLIT,
+ DESYNC_DISORDER,
+ DESYNC_FAKE
};
-struct params params = {
- .ttl = 8,
- .split = 3,
- .sfdelay = 3000,
- .attack = DESYNC_NONE,
- .split_host = 0,
- .def_ttl = 0,
- .custom_ttl = 0,
- .mod_http = 0,
- .tlsrec = 0,
- .tlsrec_pos = 0,
- .tlsrec_sni = 0,
- .de_known = 0,
+extern struct packet fake_tls, fake_http;
+extern int get_default_ttl();
+extern int get_addr(const char *str, struct sockaddr_ina *addr);
- .ipv6 = 1,
- .resolve = 1,
- .max_open = 512,
- .bfsize = 16384,
- .baddr = {
- .sin6_family = AF_INET
- },
- .debug = 2
-};
-
-int get_default_ttl()
-{
- int orig_ttl = -1, fd;
- socklen_t tsize = sizeof(orig_ttl);
-
- if ((fd = socket(AF_INET, SOCK_STREAM, 0)) < 0) {
- uniperror("socket");
+JNIEXPORT jint JNICALL
+Java_io_github_dovecoteescapee_byedpi_core_ByeDpiProxy_jniEventFd(JNIEnv *env, jobject thiz) {
+ int fd = eventfd(0, EFD_NONBLOCK);
+ if (fd < 0) {
return -1;
}
- if (getsockopt(fd, IPPROTO_IP, IP_TTL,
- (char *)&orig_ttl, &tsize) < 0) {
- uniperror("getsockopt IP_TTL");
- }
- close(fd);
- return orig_ttl;
+ return fd;
}
-void *run(void *srv) {
- LOG(LOG_S, "Start proxy thread");
- listener(*((struct sockaddr_ina *) srv));
- free(srv);
- LOG(LOG_S, "Stop proxy thread");
- return NULL;
-}
-
-JNIEXPORT jlong JNICALL
-Java_io_github_dovecoteescapee_byedpi_ByeDpiVpnService_jniStartProxy(
+JNIEXPORT jint JNICALL
+Java_io_github_dovecoteescapee_byedpi_core_ByeDpiProxy_jniStartProxy(
JNIEnv *env,
jobject thiz,
+ jint event_fd,
+ jstring ip,
jint port,
jint max_connections,
jint buffer_size,
@@ -82,24 +48,35 @@ Java_io_github_dovecoteescapee_byedpi_ByeDpiVpnService_jniStartProxy(
jint fake_ttl,
jboolean host_mixed_case,
jboolean domain_mixed_case,
- jboolean host_remove_space,
+ jboolean host_remove_spaces,
jboolean tls_record_split,
jint tls_record_split_position,
jboolean tls_record_split_at_sni) {
- enum demode desync_methods[] = {DESYNC_NONE, DESYNC_SPLIT, DESYNC_DISORDER, DESYNC_FAKE};
+
+ struct sockaddr_ina s = {
+ .in.sin_family = AF_INET,
+ .in.sin_addr.s_addr = inet_addr("0.0.0.0"),
+ };
+
+ const char *address = (*env)->GetStringUTFChars(env, ip, 0);
+ if (get_addr(address, &s) < 0) {
+ return -1;
+ }
+ s.in.sin_port = htons(port);
params.max_open = max_connections;
params.bfsize = buffer_size;
params.def_ttl = default_ttl;
params.resolve = !no_domain;
params.de_known = desync_known;
- params.attack = desync_methods[desync_method];
+ params.attack = DESYNC_METHODS[desync_method];
params.split = split_position;
params.split_host = split_at_host;
params.ttl = fake_ttl;
- params.mod_http |= host_mixed_case ? MH_HMIX : 0;
- params.mod_http |= domain_mixed_case ? MH_DMIX : 0;
- params.mod_http |= host_remove_space ? MH_SPACE : 0;
+ params.mod_http =
+ MH_HMIX * host_mixed_case |
+ MH_DMIX * domain_mixed_case |
+ MH_SPACE * host_remove_spaces;
params.tlsrec = tls_record_split;
params.tlsrec_pos = tls_record_split_position;
params.tlsrec_sni = tls_record_split_at_sni;
@@ -110,23 +87,22 @@ Java_io_github_dovecoteescapee_byedpi_ByeDpiVpnService_jniStartProxy(
}
}
- struct sockaddr_ina *srv = malloc(sizeof(struct sockaddr_ina));
- srv->in.sin_family = AF_INET;
- srv->in.sin_addr.s_addr = inet_addr("0.0.0.0");
- srv->in.sin_port = htons(port);
+ int res = listener(event_fd, s);
- NOT_EXIT = 1;
-
- pthread_t proxy_thread;
- if (pthread_create(&proxy_thread, NULL, run, srv) != 0) {
- LOG(LOG_S, "Failed to start proxy thread");
- return -1;
+ if (close(event_fd) < 0) {
+ uniperror("close");
}
- return proxy_thread;
+ return res < 0 ? get_e() : 0;
}
-JNIEXPORT void JNICALL
-Java_io_github_dovecoteescapee_byedpi_ByeDpiVpnService_jniStopProxy(JNIEnv *env, jobject thiz, jlong proxy_thread) {
- NOT_EXIT = 0;
-}
+JNIEXPORT jint JNICALL
+Java_io_github_dovecoteescapee_byedpi_core_ByeDpiProxy_jniStopProxy(JNIEnv *env, jobject thiz,
+ jint event_fd) {
+ if (eventfd_write(event_fd, 1) < 0) {
+ uniperror("eventfd_write");
+ LOG(LOG_S, "event_fd: %d", event_fd);
+ }
+
+ return 0;
+}
\ No newline at end of file
diff --git a/app/src/main/java/io/github/dovecoteescapee/byedpi/ByeDpiVpnService.kt b/app/src/main/java/io/github/dovecoteescapee/byedpi/ByeDpiVpnService.kt
deleted file mode 100644
index 56b56b9..0000000
--- a/app/src/main/java/io/github/dovecoteescapee/byedpi/ByeDpiVpnService.kt
+++ /dev/null
@@ -1,255 +0,0 @@
-package io.github.dovecoteescapee.byedpi
-
-import android.app.PendingIntent
-import android.content.Intent
-import android.net.VpnService
-import android.os.Build
-import android.os.IBinder
-import android.os.ParcelFileDescriptor
-import android.util.Log
-import androidx.lifecycle.Lifecycle
-import androidx.lifecycle.LifecycleOwner
-import androidx.lifecycle.ServiceLifecycleDispatcher
-import androidx.preference.PreferenceManager
-import engine.Engine
-import engine.Key
-
-class ByeDpiVpnService : VpnService(), LifecycleOwner {
- private var proxyThread: Long = -1
- private var vpn: ParcelFileDescriptor? = null
-
- private val dispatcher = ServiceLifecycleDispatcher(this)
- override val lifecycle: Lifecycle
- get() = dispatcher.lifecycle
-
- companion object {
- private val TAG: String = ByeDpiVpnService::class.java.simpleName
-
- var status: Status = Status.STOPPED
- private set
- }
-
- override fun onCreate() {
- dispatcher.onServicePreSuperOnCreate()
- super.onCreate()
- }
-
- override fun onBind(intent: Intent?): IBinder? {
- dispatcher.onServicePreSuperOnBind()
- return super.onBind(intent)
- }
-
- override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
- if (intent != null) {
- return when (intent.action) {
- "start" -> {
- run()
- START_STICKY
- }
-
- "stop" -> {
- stop()
- START_NOT_STICKY
- }
-
- else -> {
- throw IllegalArgumentException("Unknown action ${intent.action}")
- }
- }
- }
- return super.onStartCommand(intent, flags, startId)
- }
-
- override fun onRevoke() {
- super.onRevoke()
- Log.i(TAG, "VPN revoked")
- stop()
- }
-
- override fun onDestroy() {
- dispatcher.onServicePreSuperOnDestroy()
- super.onDestroy()
- Log.i(TAG, "Service destroyed")
- stop()
- }
-
- private fun run() {
- val preferences = getPreferences()
- status = Status.RUNNING
-
- if (proxyThread >= 0) {
- Log.w(TAG, "Proxy already running")
- return
- }
-
- proxyThread = startProxy(preferences)
- if (proxyThread < 0) {
- status = Status.STOPPED
- Log.e(TAG, "Proxy failed to start")
- return
- }
-
- val vpn = getBuilder().establish()
- this.vpn = vpn
- if (vpn == null) {
- Log.e(TAG, "VPN connection failed")
- return
- }
-
- Log.d(TAG, "fd: ${vpn.fd}")
- startTun2Socks(vpn.fd, preferences.port)
- }
-
- private fun getPreferences(): ByeDpiProxyPreferences {
- val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this)
-
- return ByeDpiProxyPreferences(
- port =
- sharedPreferences.getString("byedpi_proxy_port", null)?.toInt(),
- maxConnections =
- sharedPreferences.getString("byedpi_max_connections", null)?.toInt(),
- bufferSize =
- sharedPreferences.getString("byedpi_buffer_size", null)?.toInt(),
- defaultTtl =
- sharedPreferences.getString("byedpi_default_ttl", null)?.toInt(),
- noDomain =
- sharedPreferences.getBoolean("byedpi_no_domain", false),
- desyncKnown =
- sharedPreferences.getBoolean("byedpi_desync_known", false),
- desyncMethod =
- sharedPreferences.getString("byedpi_desync_method", null)
- ?.let { ByeDpiProxyPreferences.DesyncMethod.fromName(it) },
- splitPosition =
- sharedPreferences.getString("byedpi_split_position", null)?.toInt(),
- splitAtHost =
- sharedPreferences.getBoolean("byedpi_split_at_host", false),
- fakeTtl =
- sharedPreferences.getString("byedpi_fake_ttl", null)?.toInt(),
- hostMixedCase =
- sharedPreferences.getBoolean("byedpi_host_mixed_case", false),
- domainMixedCase =
- sharedPreferences.getBoolean("byedpi_domain_mixed_case", false),
- hostRemoveSpaces =
- sharedPreferences.getBoolean("byedpi_host_remove_spaces", false),
- tlsRecordSplit =
- sharedPreferences.getBoolean("byedpi_tlsrec", false),
- tlsRecordSplitPosition =
- sharedPreferences.getString("byedpi_tlsrec_position", null)?.toInt(),
- tlsRecordSplitAtSni =
- sharedPreferences.getBoolean("byedpi_tlsrec_at_sni", false),
- )
- }
-
- private fun stop() {
- status = Status.STOPPED
- stopTun2Socks()
- stopProxy()
- }
-
- private fun startProxy(preferences: ByeDpiProxyPreferences): Long {
- Log.i(TAG, "Proxy started")
- return jniStartProxy(
- port = preferences.port,
- maxConnections = preferences.maxConnections,
- bufferSize = preferences.bufferSize,
- defaultTtl = preferences.defaultTtl,
- noDomain = preferences.noDomain,
- desyncKnown = preferences.desyncKnown,
- desyncMethod = preferences.desyncMethod.ordinal,
- splitPosition = preferences.splitPosition,
- splitAtHost = preferences.splitAtHost,
- fakeTtl = preferences.fakeTtl,
- hostMixedCase = preferences.hostMixedCase,
- domainMixedCase = preferences.domainMixedCase,
- hostRemoveSpace = preferences.hostRemoveSpaces,
- tlsRecordSplit = preferences.tlsRecordSplit,
- tlsRecordSplitPosition = preferences.tlsRecordSplitPosition,
- tlsRecordSplitAtSni = preferences.tlsRecordSplitAtSni,
- )
- }
-
- private fun stopProxy() {
- if (proxyThread < 0) {
- Log.w(TAG, "Proxy not running")
- }
- jniStopProxy(proxyThread)
- proxyThread = -1
- }
-
- private fun startTun2Socks(fd: Int, port: Int) {
- val key = Key().apply {
- mark = 0
- mtu = 0
- device = "fd://$fd"
-
- setInterface("")
- logLevel = "debug"
- udpProxy = "direct://"
- tcpProxy = "socks5://127.0.0.1:$port"
-
- restAPI = ""
- tcpSendBufferSize = ""
- tcpReceiveBufferSize = ""
- tcpModerateReceiveBuffer = false
- }
-
- Engine.insert(key)
-
- Log.i(TAG, "Tun2Socks started")
- Engine.start()
- }
-
- private fun stopTun2Socks() {
- Log.i(TAG, "Tun2socks stopped")
- vpn?.close() ?: Log.w(TAG, "VPN not running")
- }
-
- private external fun jniStartProxy(
- port: Int,
- maxConnections: Int,
- bufferSize: Int,
- defaultTtl: Int,
- noDomain: Boolean,
- desyncKnown: Boolean,
- desyncMethod: Int,
- splitPosition: Int,
- splitAtHost: Boolean,
- fakeTtl: Int,
- hostMixedCase: Boolean,
- domainMixedCase: Boolean,
- hostRemoveSpace: Boolean,
- tlsRecordSplit: Boolean,
- tlsRecordSplitPosition: Int,
- tlsRecordSplitAtSni: Boolean,
- ): Long
-
- private external fun jniStopProxy(proxyThread: Long)
-
- private fun getBuilder(): Builder {
- val builder = Builder()
- builder.setSession("ByeDPI")
- builder.setConfigureIntent(
- PendingIntent.getActivity(
- this,
- 0,
- Intent(this, MainActivity::class.java),
- PendingIntent.FLAG_IMMUTABLE,
- )
- )
-
- val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this)
- val dns = sharedPreferences.getString("dns_ip", "9.9.9.9")!!
-
- builder.addAddress("10.10.10.10", 32)
- builder.addRoute("0.0.0.0", 0)
- builder.addRoute("0:0:0:0:0:0:0:0", 0)
- builder.addDnsServer(dns)
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
- builder.setMetered(false)
- }
-
- builder.addDisallowedApplication("io.github.dovecoteescapee.byedpi")
-
- return builder
- }
-}
diff --git a/app/src/main/java/io/github/dovecoteescapee/byedpi/MainActivity.kt b/app/src/main/java/io/github/dovecoteescapee/byedpi/MainActivity.kt
deleted file mode 100644
index 29e96cf..0000000
--- a/app/src/main/java/io/github/dovecoteescapee/byedpi/MainActivity.kt
+++ /dev/null
@@ -1,104 +0,0 @@
-package io.github.dovecoteescapee.byedpi
-
-import android.content.Intent
-import android.net.VpnService
-import androidx.appcompat.app.AppCompatActivity
-import android.os.Bundle
-import android.view.Menu
-import android.view.MenuItem
-import android.widget.Toast
-import androidx.activity.result.contract.ActivityResultContracts
-import io.github.dovecoteescapee.byedpi.databinding.ActivityMainBinding
-
-class MainActivity : AppCompatActivity() {
- private lateinit var binding: ActivityMainBinding
-
- private val register =
- registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
- if (it.resultCode == RESULT_OK) {
- startVpnService()
- } else {
- Toast.makeText(this, R.string.vpn_permission_denied, Toast.LENGTH_SHORT).show()
- }
- }
-
- override fun onCreate(savedInstanceState: Bundle?) {
- super.onCreate(savedInstanceState)
-
- binding = ActivityMainBinding.inflate(layoutInflater)
- setContentView(binding.root)
-
- binding.statusButton.setOnClickListener {
- when (ByeDpiVpnService.status) {
- Status.STOPPED -> {
- val intentPrepare = VpnService.prepare(this)
- if (intentPrepare != null) {
- register.launch(intentPrepare)
- } else {
- startVpnService()
- }
- updateStatus(Status.RUNNING)
- }
- Status.RUNNING -> {
- stopVpnService()
- updateStatus(Status.STOPPED)
- }
- }
- }
-
- updateStatus(ByeDpiVpnService.status)
- }
-
- override fun onCreateOptionsMenu(menu: Menu?): Boolean {
- menuInflater.inflate(R.menu.menu_main, menu)
- return true
- }
-
- override fun onOptionsItemSelected(item: MenuItem): Boolean =
- when (item.itemId) {
- R.id.action_settings -> {
- val intent = Intent(this, SettingsActivity::class.java)
- if (ByeDpiVpnService.status == Status.RUNNING) {
- Toast.makeText(this, R.string.settings_unavailable, Toast.LENGTH_SHORT)
- .show()
- } else {
- startActivity(intent)
- }
-
- true
- }
-
- else -> super.onOptionsItemSelected(item)
- }
-
- private fun startVpnService() {
- val intent = Intent(this, ByeDpiVpnService::class.java)
- intent.action = "start"
- startService(intent)
- }
-
- private fun stopVpnService() {
- val intent = Intent(this, ByeDpiVpnService::class.java)
- intent.action = "stop"
- startService(intent)
- stopService(intent)
- }
-
- private fun updateStatus(status : Status) {
- when (status) {
- Status.STOPPED -> {
- binding.statusButton.setText(R.string.start)
- }
- Status.RUNNING -> {
- binding.statusButton.setText(R.string.stop)
- }
- }
- }
-
- companion object {
- // Used to load the 'byedpi' library on application startup.
- init {
- System.loadLibrary("byedpi")
- }
- }
-}
\ No newline at end of file
diff --git a/app/src/main/java/io/github/dovecoteescapee/byedpi/SettingsActivity.kt b/app/src/main/java/io/github/dovecoteescapee/byedpi/SettingsActivity.kt
deleted file mode 100644
index 9bdf42e..0000000
--- a/app/src/main/java/io/github/dovecoteescapee/byedpi/SettingsActivity.kt
+++ /dev/null
@@ -1,115 +0,0 @@
-package io.github.dovecoteescapee.byedpi
-
-import android.content.Intent
-import android.os.Bundle
-import android.util.Log
-import android.view.Menu
-import android.view.MenuItem
-import android.widget.Toast
-import androidx.activity.result.contract.ActivityResultContracts
-import androidx.appcompat.app.AppCompatActivity
-import androidx.lifecycle.lifecycleScope
-import androidx.preference.PreferenceManager
-import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.launch
-import java.io.IOException
-
-class SettingsActivity : AppCompatActivity() {
- private val register =
- registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
- lifecycleScope.launch(Dispatchers.IO) {
- val logs = collectLogs()
-
- if (logs == null) {
- Toast.makeText(
- this@SettingsActivity,
- R.string.logs_failed,
- Toast.LENGTH_SHORT
- ).show()
- } else {
- val uri = it.data?.data ?: run {
- Log.e(TAG, "No data in result")
- return@launch
- }
- contentResolver.openOutputStream(uri)?.use {
- try {
- it.write(logs.toByteArray())
- } catch (e: IOException) {
- Log.e(TAG, "Failed to save logs", e)
- }
- } ?: run {
- Log.e(TAG, "Failed to open output stream")
- }
- }
- }
- }
-
-
- companion object {
- private val TAG: String = SettingsActivity::class.java.simpleName
-
- private fun collectLogs(): String? =
- try {
- Runtime.getRuntime()
- .exec("logcat *:I -d")
- .inputStream.bufferedReader()
- .use { it.readText() }
- } catch (e: Exception) {
- Log.e(TAG, "Failed to collect logs", e)
- null
- }
- }
-
- override fun onCreate(savedInstanceState: Bundle?) {
- super.onCreate(savedInstanceState)
- setContentView(R.layout.activity_settings)
-
- supportFragmentManager
- .beginTransaction()
- .replace(R.id.settings, SettingsFragment())
- .commit()
-
- supportActionBar?.setDisplayHomeAsUpEnabled(true)
- }
-
- override fun onCreateOptionsMenu(menu: Menu?): Boolean {
- menuInflater.inflate(R.menu.menu_settings, menu)
- return true
- }
-
- override fun onOptionsItemSelected(item: MenuItem): Boolean = when (item.itemId) {
- android.R.id.home -> {
- onBackPressedDispatcher.onBackPressed()
- true
- }
-
- R.id.action_save_logs -> {
- collectLogs()
- val intent =
- Intent(Intent.ACTION_CREATE_DOCUMENT).apply {
- addCategory(Intent.CATEGORY_OPENABLE)
- setType("text/plain")
- putExtra(Intent.EXTRA_TITLE, "byedpi.log")
- }
-
- register.launch(intent)
- true
- }
-
- R.id.action_reset_settings -> {
- PreferenceManager
- .getDefaultSharedPreferences(this)
- .edit()
- .clear()
- .apply()
-
- supportFragmentManager
- .beginTransaction()
- .replace(R.id.settings, SettingsFragment())
- .commit()
- true
- }
-
- else -> super.onOptionsItemSelected(item)
- }
-}
\ No newline at end of file
diff --git a/app/src/main/java/io/github/dovecoteescapee/byedpi/SettingsFragment.kt b/app/src/main/java/io/github/dovecoteescapee/byedpi/SettingsFragment.kt
deleted file mode 100644
index 02f9d72..0000000
--- a/app/src/main/java/io/github/dovecoteescapee/byedpi/SettingsFragment.kt
+++ /dev/null
@@ -1,61 +0,0 @@
-package io.github.dovecoteescapee.byedpi
-
-import android.net.InetAddresses
-import android.os.Build
-import android.os.Bundle
-import android.util.Log
-import android.util.Patterns
-import androidx.preference.EditTextPreference
-import androidx.preference.PreferenceFragmentCompat
-
-class SettingsFragment : PreferenceFragmentCompat() {
- companion object {
- private val TAG: String = SettingsFragment::class.java.simpleName
-
- private fun checkIp(ip: String): Boolean =
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
- InetAddresses.isNumericAddress(ip)
- } else {
- Patterns.IP_ADDRESS.matcher(ip).matches()
- }
-
- private fun checkPort(port: String): Boolean =
- port.toIntOrNull()?.let { it in 1..65535 } ?: false
- }
- override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
- setPreferencesFromResource(R.xml.settings, rootKey)
-
- setEditTestPreferenceListener("dns_ip") { checkIp(it) }
- setEditTestPreferenceListener("byedpi_port") { checkPort(it) }
- setEditTestPreferenceListener("byedpi_max_connections") { value ->
- value.toIntOrNull()?.let { it > 0 } ?: false
- }
- setEditTestPreferenceListener("byedpi_buffer_size") { value ->
- value.toIntOrNull()?.let { it > 0 } ?: false
- }
- setEditTestPreferenceListener("byedpi_default_ttl") { value ->
- value.toIntOrNull()?.let { it >= 0 } ?: false
- }
- setEditTestPreferenceListener("byedpi_split_position") { value ->
- value.toIntOrNull() != null
- }
- setEditTestPreferenceListener("byedpi_fake_ttl") { value ->
- value.toIntOrNull()?.let { it >= 0 } ?: false
- }
- setEditTestPreferenceListener("byedpi_tlsrec_position") {
- it.toIntOrNull()?.let { it >= 0 } ?: false
- }
- }
-
- private fun setEditTestPreferenceListener(key: String, check: (String) -> Boolean) {
- findPreference(key)
- ?.setOnPreferenceChangeListener { preference, newValue ->
- newValue as String
- val valid = check(newValue)
- if (!valid) {
- Log.e(TAG, "Invalid ${preference.title}: $newValue")
- }
- valid
- }
- }
-}
\ No newline at end of file
diff --git a/app/src/main/java/io/github/dovecoteescapee/byedpi/Status.kt b/app/src/main/java/io/github/dovecoteescapee/byedpi/Status.kt
deleted file mode 100644
index 9c9cbf4..0000000
--- a/app/src/main/java/io/github/dovecoteescapee/byedpi/Status.kt
+++ /dev/null
@@ -1,6 +0,0 @@
-package io.github.dovecoteescapee.byedpi
-
-enum class Status {
- RUNNING,
- STOPPED
-}
\ No newline at end of file
diff --git a/app/src/main/java/io/github/dovecoteescapee/byedpi/activities/MainActivity.kt b/app/src/main/java/io/github/dovecoteescapee/byedpi/activities/MainActivity.kt
new file mode 100644
index 0000000..56aa3c8
--- /dev/null
+++ b/app/src/main/java/io/github/dovecoteescapee/byedpi/activities/MainActivity.kt
@@ -0,0 +1,359 @@
+package io.github.dovecoteescapee.byedpi.activities
+
+import android.content.BroadcastReceiver
+import android.content.Context
+import android.content.Intent
+import android.content.IntentFilter
+import android.net.VpnService
+import android.os.Build
+import android.os.Bundle
+import android.util.Log
+import android.view.Menu
+import android.view.MenuItem
+import android.widget.Toast
+import androidx.activity.result.contract.ActivityResultContracts
+import androidx.appcompat.app.AppCompatActivity
+import androidx.lifecycle.lifecycleScope
+import io.github.dovecoteescapee.byedpi.data.START_ACTION
+import io.github.dovecoteescapee.byedpi.data.STOP_ACTION
+import io.github.dovecoteescapee.byedpi.R
+import io.github.dovecoteescapee.byedpi.data.AppStatus
+import io.github.dovecoteescapee.byedpi.data.FAILED_BROADCAST
+import io.github.dovecoteescapee.byedpi.data.Mode
+import io.github.dovecoteescapee.byedpi.data.SENDER
+import io.github.dovecoteescapee.byedpi.data.STARTED_BROADCAST
+import io.github.dovecoteescapee.byedpi.data.STOPPED_BROADCAST
+import io.github.dovecoteescapee.byedpi.data.Sender
+import io.github.dovecoteescapee.byedpi.fragments.SettingsFragment
+import io.github.dovecoteescapee.byedpi.databinding.ActivityMainBinding
+import io.github.dovecoteescapee.byedpi.services.ByeDpiProxyService
+import io.github.dovecoteescapee.byedpi.services.ByeDpiVpnService
+import io.github.dovecoteescapee.byedpi.utility.getPreferences
+import io.github.dovecoteescapee.byedpi.utility.mode
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.launch
+import java.io.IOException
+
+class MainActivity : AppCompatActivity() {
+ private lateinit var binding: ActivityMainBinding
+
+ companion object {
+ private val TAG: String = MainActivity::class.java.simpleName
+
+ private var status: AppStatus = AppStatus.Halted
+ private var mode: Mode = Mode.VPN
+
+ private fun collectLogs(): String? =
+ try {
+ Runtime.getRuntime()
+ .exec("logcat *:I -d")
+ .inputStream.bufferedReader()
+ .use { it.readText() }
+ } catch (e: Exception) {
+ Log.e(TAG, "Failed to collect logs", e)
+ null
+ }
+ }
+
+ private val vpnRegister =
+ registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
+ if (it.resultCode == RESULT_OK) {
+ startVpn()
+ } else {
+ Toast.makeText(this, R.string.vpn_permission_denied, Toast.LENGTH_SHORT).show()
+ updateStatus(AppStatus.Halted)
+ }
+ }
+
+ private val logsRegister =
+ registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
+ lifecycleScope.launch(Dispatchers.IO) {
+ val logs = collectLogs()
+
+ if (logs == null) {
+ Toast.makeText(
+ this@MainActivity,
+ R.string.logs_failed,
+ Toast.LENGTH_SHORT
+ ).show()
+ } else {
+ val uri = it.data?.data ?: run {
+ Log.e(TAG, "No data in result")
+ return@launch
+ }
+ contentResolver.openOutputStream(uri)?.use {
+ try {
+ it.write(logs.toByteArray())
+ } catch (e: IOException) {
+ Log.e(TAG, "Failed to save logs", e)
+ }
+ } ?: run {
+ Log.e(TAG, "Failed to open output stream")
+ }
+ }
+ }
+ }
+
+ private val receiver = object : BroadcastReceiver() {
+ override fun onReceive(context: Context?, intent: Intent?) {
+ Log.d(TAG, "Received intent: ${intent?.action}")
+
+ if (intent == null) {
+ Log.w(TAG, "Received null intent")
+ return
+ }
+
+ val senderOrd = intent.getIntExtra(SENDER, -1)
+ val sender = Sender.entries.getOrNull(senderOrd)
+ if (sender == null) {
+ Log.w(TAG, "Received intent with unknown sender: $senderOrd")
+ return
+ }
+
+ when (val action = intent.action) {
+ STARTED_BROADCAST -> if (
+ status == AppStatus.Halted || status == AppStatus.Starting
+ ) {
+ updateStatus(AppStatus.Running, Mode.fromSender(sender))
+ } else {
+ Log.w(TAG, "Received STARTED while status is $status")
+ }
+
+ STOPPED_BROADCAST -> if (
+ mode == Mode.fromSender(sender) &&
+ (status == AppStatus.Running || status == AppStatus.Stopping)
+ ) {
+ updateStatus(AppStatus.Halted)
+ } else {
+ Log.w(TAG, "Received STOPPED $sender while status is $status")
+ }
+
+ FAILED_BROADCAST -> {
+ Toast.makeText(
+ context,
+ getString(R.string.failed_to_start, sender.name),
+ Toast.LENGTH_SHORT,
+ ).show()
+ updateStatus(AppStatus.Halted)
+ }
+
+ else -> Log.w(TAG, "Unknown action: $action")
+ }
+ }
+ }
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+
+ binding = ActivityMainBinding.inflate(layoutInflater)
+ setContentView(binding.root)
+
+ val intentFilter = IntentFilter().apply {
+ addAction(STARTED_BROADCAST)
+ addAction(STOPPED_BROADCAST)
+ addAction(FAILED_BROADCAST)
+ }
+
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
+ registerReceiver(receiver, intentFilter, RECEIVER_EXPORTED)
+ } else {
+ registerReceiver(receiver, intentFilter)
+ }
+
+ binding.statusButton.setOnClickListener {
+ when (status) {
+ AppStatus.Halted -> start()
+ AppStatus.Running -> stop()
+ else -> {
+ // ignore
+ }
+ }
+ }
+
+ val theme = getPreferences(this)
+ .getString("app_theme", null)
+ SettingsFragment.setTheme(theme ?: "system")
+ }
+
+ override fun onResume() {
+ super.onResume()
+ updateStatus()
+ }
+
+ override fun onDestroy() {
+ super.onDestroy()
+ unregisterReceiver(receiver)
+ }
+
+ override fun onCreateOptionsMenu(menu: Menu?): Boolean {
+ menuInflater.inflate(R.menu.menu_main, menu)
+ return true
+ }
+
+ override fun onOptionsItemSelected(item: MenuItem): Boolean =
+ when (item.itemId) {
+ R.id.action_settings -> {
+ if (status == AppStatus.Halted) {
+ val intent = Intent(this, SettingsActivity::class.java)
+ startActivity(intent)
+ } else {
+ Toast.makeText(this, R.string.settings_unavailable, Toast.LENGTH_SHORT)
+ .show()
+ }
+ true
+ }
+
+ R.id.action_save_logs -> {
+ val intent =
+ Intent(Intent.ACTION_CREATE_DOCUMENT).apply {
+ addCategory(Intent.CATEGORY_OPENABLE)
+ type = "text/plain"
+ putExtra(Intent.EXTRA_TITLE, "byedpi.log")
+ }
+
+ logsRegister.launch(intent)
+ true
+ }
+
+ else -> super.onOptionsItemSelected(item)
+ }
+
+ private fun start() {
+// Starting and stopping is too fast
+// updateStatus(AppStatus.Starting)
+
+ val preferences = getPreferences(this)
+ when (val mode = preferences.getString("byedpi_mode", null) ?: "vpn") {
+ "vpn" -> {
+ val intentPrepare = VpnService.prepare(this)
+ if (intentPrepare != null) {
+ vpnRegister.launch(intentPrepare)
+ } else {
+ startVpn()
+ }
+ }
+
+ "proxy" -> startProxy()
+ else -> Log.e(TAG, "Unknown mode: $mode")
+ }
+ }
+
+ private fun startVpn() {
+ Log.i(TAG, "Starting VPN")
+ val intent = Intent(this, ByeDpiVpnService::class.java)
+ intent.action = START_ACTION
+ startService(intent)
+ }
+
+ private fun startProxy() {
+ Log.i(TAG, "Starting proxy")
+ val intent = Intent(this, ByeDpiProxyService::class.java)
+ intent.action = START_ACTION
+ startService(intent)
+ }
+
+ private fun stop() {
+// Starting and stopping is too fast
+// updateStatus(AppStatus.Stopping)
+ when (mode) {
+ Mode.VPN -> stopVpn()
+ Mode.Proxy -> stopProxy()
+ }
+ }
+
+ private fun stopVpn() {
+ Log.i(TAG, "Stopping VPN")
+ val intent = Intent(this, ByeDpiVpnService::class.java)
+ intent.action = STOP_ACTION
+ startService(intent)
+ }
+
+ private fun stopProxy() {
+ Log.i(TAG, "Stopping proxy")
+ val intent = Intent(this, ByeDpiProxyService::class.java)
+ intent.action = STOP_ACTION
+ startService(intent)
+ }
+
+ private fun updateStatus(
+ status: AppStatus = MainActivity.status,
+ mode: Mode = MainActivity.mode,
+ ) {
+ Log.i(TAG, "Updating from ${MainActivity.status} to $status")
+
+ MainActivity.mode = mode
+
+ val preferences = getPreferences(this)
+ val proxyIp = preferences.getString("byedpi_proxy_ip", null) ?: "127.0.0.1"
+ val proxyPort = preferences.getString("byedpi_proxy_port", null) ?: "1080"
+ binding.proxyAddress.text = getString(R.string.proxy_address, proxyIp, proxyPort)
+
+ when (status) {
+ AppStatus.Halted -> {
+ val newMode = preferences.mode()
+ MainActivity.mode = newMode
+ when (newMode) {
+ Mode.VPN -> {
+ binding.statusText.setText(R.string.vpn_disconnected)
+ binding.statusButton.setText(R.string.vpn_connect)
+ }
+
+ Mode.Proxy -> {
+ binding.statusText.setText(R.string.proxy_down)
+ binding.statusButton.setText(R.string.proxy_start)
+ }
+ }
+ MainActivity.status = AppStatus.Halted
+ binding.statusButton.isEnabled = true
+ }
+
+ AppStatus.Running -> {
+ when (mode) {
+ Mode.VPN -> {
+ binding.statusText.setText(R.string.vpn_connected)
+ binding.statusButton.setText(R.string.vpn_disconnect)
+ }
+
+ Mode.Proxy -> {
+ binding.statusText.setText(R.string.proxy_up)
+ binding.statusButton.setText(R.string.proxy_stop)
+ }
+ }
+ MainActivity.status = AppStatus.Running
+ binding.statusButton.isEnabled = true
+ }
+
+ AppStatus.Starting -> {
+ if (MainActivity.status == AppStatus.Halted) {
+ when (mode) {
+ Mode.VPN -> {
+ binding.statusText.setText(R.string.vpn_connecting)
+ }
+
+ Mode.Proxy -> {
+ binding.statusText.setText(R.string.proxy_starting)
+ }
+ }
+ MainActivity.status = AppStatus.Starting
+ binding.statusButton.isEnabled = false
+ }
+ }
+
+ AppStatus.Stopping -> {
+ if (MainActivity.status == AppStatus.Running) {
+ when (mode) {
+ Mode.VPN -> {
+ binding.statusText.setText(R.string.vpn_disconnecting)
+ }
+
+ Mode.Proxy -> {
+ binding.statusText.setText(R.string.proxy_stopping)
+ }
+ }
+ MainActivity.status = AppStatus.Stopping
+ binding.statusButton.isEnabled = false
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/io/github/dovecoteescapee/byedpi/activities/SettingsActivity.kt b/app/src/main/java/io/github/dovecoteescapee/byedpi/activities/SettingsActivity.kt
new file mode 100644
index 0000000..5b95624
--- /dev/null
+++ b/app/src/main/java/io/github/dovecoteescapee/byedpi/activities/SettingsActivity.kt
@@ -0,0 +1,50 @@
+package io.github.dovecoteescapee.byedpi.activities
+
+import android.os.Bundle
+import android.view.Menu
+import android.view.MenuItem
+import androidx.appcompat.app.AppCompatActivity
+import io.github.dovecoteescapee.byedpi.R
+import io.github.dovecoteescapee.byedpi.fragments.SettingsFragment
+import io.github.dovecoteescapee.byedpi.utility.getPreferences
+
+class SettingsActivity : AppCompatActivity() {
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ setContentView(R.layout.activity_settings)
+
+ supportFragmentManager
+ .beginTransaction()
+ .replace(R.id.settings, SettingsFragment())
+ .commit()
+
+ supportActionBar?.setDisplayHomeAsUpEnabled(true)
+ }
+
+ override fun onCreateOptionsMenu(menu: Menu?): Boolean {
+ menuInflater.inflate(R.menu.menu_settings, menu)
+ return true
+ }
+
+ override fun onOptionsItemSelected(item: MenuItem): Boolean = when (item.itemId) {
+ android.R.id.home -> {
+ onBackPressedDispatcher.onBackPressed()
+ true
+ }
+
+ R.id.action_reset_settings -> {
+ getPreferences(this)
+ .edit()
+ .clear()
+ .apply()
+
+ supportFragmentManager
+ .beginTransaction()
+ .replace(R.id.settings, SettingsFragment())
+ .commit()
+ true
+ }
+
+ else -> super.onOptionsItemSelected(item)
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/io/github/dovecoteescapee/byedpi/core/ByeDpiProxy.kt b/app/src/main/java/io/github/dovecoteescapee/byedpi/core/ByeDpiProxy.kt
new file mode 100644
index 0000000..8ee663f
--- /dev/null
+++ b/app/src/main/java/io/github/dovecoteescapee/byedpi/core/ByeDpiProxy.kt
@@ -0,0 +1,68 @@
+package io.github.dovecoteescapee.byedpi.core
+
+import java.io.IOException
+
+class ByeDpiProxy {
+ companion object {
+ init {
+ System.loadLibrary("byedpi")
+ }
+ }
+
+ private val fd = jniEventFd()
+
+ init {
+ if (fd < 0) {
+ throw IOException("Failed to create eventfd")
+ }
+ }
+
+ fun startProxy(preferences: ByeDpiProxyPreferences): Int =
+ jniStartProxy(
+ eventFd = fd,
+ ip = preferences.ip,
+ port = preferences.port,
+ maxConnections = preferences.maxConnections,
+ bufferSize = preferences.bufferSize,
+ defaultTtl = preferences.defaultTtl,
+ noDomain = preferences.noDomain,
+ desyncKnown = preferences.desyncKnown,
+ desyncMethod = preferences.desyncMethod.ordinal,
+ splitPosition = preferences.splitPosition,
+ splitAtHost = preferences.splitAtHost,
+ fakeTtl = preferences.fakeTtl,
+ hostMixedCase = preferences.hostMixedCase,
+ domainMixedCase = preferences.domainMixedCase,
+ hostRemoveSpaces = preferences.hostRemoveSpaces,
+ tlsRecordSplit = preferences.tlsRecordSplit,
+ tlsRecordSplitPosition = preferences.tlsRecordSplitPosition,
+ tlsRecordSplitAtSni = preferences.tlsRecordSplitAtSni,
+ )
+
+ fun stopProxy(): Int = jniStopProxy(fd)
+
+ private external fun jniEventFd(): Int
+
+ private external fun jniStartProxy(
+ eventFd: Int,
+ ip: String,
+ port: Int,
+ maxConnections: Int,
+ bufferSize: Int,
+ defaultTtl: Int,
+ noDomain: Boolean,
+ desyncKnown: Boolean,
+ desyncMethod: Int,
+ splitPosition: Int,
+ splitAtHost: Boolean,
+ fakeTtl: Int,
+ hostMixedCase: Boolean,
+ domainMixedCase: Boolean,
+ hostRemoveSpaces: Boolean,
+ tlsRecordSplit: Boolean,
+ tlsRecordSplitPosition: Int,
+ tlsRecordSplitAtSni: Boolean,
+ ): Int
+
+ private external fun jniStopProxy(eventFd: Int): Int
+}
\ No newline at end of file
diff --git a/app/src/main/java/io/github/dovecoteescapee/byedpi/ByeDpiProxyPreferences.kt b/app/src/main/java/io/github/dovecoteescapee/byedpi/core/ByeDpiProxyPreferences.kt
similarity index 54%
rename from app/src/main/java/io/github/dovecoteescapee/byedpi/ByeDpiProxyPreferences.kt
rename to app/src/main/java/io/github/dovecoteescapee/byedpi/core/ByeDpiProxyPreferences.kt
index f3583f3..b92391d 100644
--- a/app/src/main/java/io/github/dovecoteescapee/byedpi/ByeDpiProxyPreferences.kt
+++ b/app/src/main/java/io/github/dovecoteescapee/byedpi/core/ByeDpiProxyPreferences.kt
@@ -1,6 +1,9 @@
-package io.github.dovecoteescapee.byedpi
+package io.github.dovecoteescapee.byedpi.core
+
+import android.content.SharedPreferences
class ByeDpiProxyPreferences(
+ ip: String? = null,
port: Int? = null,
maxConnections: Int? = null,
bufferSize: Int? = null,
@@ -18,6 +21,7 @@ class ByeDpiProxyPreferences(
tlsRecordSplitPosition: Int? = null,
tlsRecordSplitAtSni: Boolean? = null,
) {
+ val ip: String = ip ?: "127.0.0.1"
val port: Int = port ?: 1080
val maxConnections: Int = maxConnections ?: 512
val bufferSize: Int = bufferSize ?: 16384
@@ -35,6 +39,29 @@ class ByeDpiProxyPreferences(
val tlsRecordSplitPosition: Int = tlsRecordSplitPosition ?: 0
val tlsRecordSplitAtSni: Boolean = tlsRecordSplitAtSni ?: false
+ constructor(preferences: SharedPreferences) : this(
+ ip = preferences.getString("byedpi_proxy_ip", null),
+ port = preferences.getString("byedpi_proxy_port", null)?.toInt(),
+ maxConnections = preferences.getString("byedpi_max_connections", null)?.toInt(),
+ bufferSize = preferences.getString("byedpi_buffer_size", null)?.toInt(),
+ defaultTtl = preferences.getString("byedpi_default_ttl", null)?.toInt(),
+ noDomain = preferences.getBoolean("byedpi_no_domain", false),
+ desyncKnown = preferences.getBoolean("byedpi_desync_known", false),
+ desyncMethod = preferences.getString("byedpi_desync_method", null)
+ ?.let { DesyncMethod.fromName(it) },
+ splitPosition = preferences.getString("byedpi_split_position", null)?.toInt(),
+ splitAtHost = preferences.getBoolean("byedpi_split_at_host", false),
+ fakeTtl = preferences.getString("byedpi_fake_ttl", null)?.toInt(),
+ hostMixedCase = preferences.getBoolean("byedpi_host_mixed_case", false),
+ domainMixedCase = preferences.getBoolean("byedpi_domain_mixed_case", false),
+ hostRemoveSpaces = preferences.getBoolean("byedpi_host_remove_spaces", false),
+ tlsRecordSplit = preferences.getBoolean("byedpi_tlsrec_enabled", false),
+ tlsRecordSplitPosition = preferences.getString("byedpi_tlsrec_position", null)?.toInt(),
+ tlsRecordSplitAtSni = preferences.getBoolean("byedpi_tlsrec_at_sni", false),
+
+
+ )
+
enum class DesyncMethod {
None,
Split,
diff --git a/app/src/main/java/io/github/dovecoteescapee/byedpi/data/Actions.kt b/app/src/main/java/io/github/dovecoteescapee/byedpi/data/Actions.kt
new file mode 100644
index 0000000..c8c9331
--- /dev/null
+++ b/app/src/main/java/io/github/dovecoteescapee/byedpi/data/Actions.kt
@@ -0,0 +1,4 @@
+package io.github.dovecoteescapee.byedpi.data
+
+const val START_ACTION = "start"
+const val STOP_ACTION = "stop"
diff --git a/app/src/main/java/io/github/dovecoteescapee/byedpi/data/AppStatus.kt b/app/src/main/java/io/github/dovecoteescapee/byedpi/data/AppStatus.kt
new file mode 100644
index 0000000..9bade08
--- /dev/null
+++ b/app/src/main/java/io/github/dovecoteescapee/byedpi/data/AppStatus.kt
@@ -0,0 +1,26 @@
+package io.github.dovecoteescapee.byedpi.data
+
+enum class AppStatus {
+ Halted,
+ Running,
+ Starting,
+ Stopping,
+}
+
+enum class Mode {
+ Proxy,
+ VPN;
+
+ companion object {
+ fun fromSender(sender: Sender): Mode = when (sender) {
+ Sender.Proxy -> Proxy
+ Sender.VPN -> VPN
+ }
+
+ fun fromString(name: String): Mode = when (name) {
+ "proxy" -> Proxy
+ "vpn" -> VPN
+ else -> throw IllegalArgumentException("Invalid mode: $name")
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/io/github/dovecoteescapee/byedpi/data/Broadcasts.kt b/app/src/main/java/io/github/dovecoteescapee/byedpi/data/Broadcasts.kt
new file mode 100644
index 0000000..54b0f4e
--- /dev/null
+++ b/app/src/main/java/io/github/dovecoteescapee/byedpi/data/Broadcasts.kt
@@ -0,0 +1,12 @@
+package io.github.dovecoteescapee.byedpi.data
+
+const val STARTED_BROADCAST = "io.github.dovecoteescapee.byedpi.STARTED"
+const val STOPPED_BROADCAST = "io.github.dovecoteescapee.byedpi.STOPPED"
+const val FAILED_BROADCAST = "io.github.dovecoteescapee.byedpi.FAILED"
+
+const val SENDER = "sender"
+
+enum class Sender(val senderName: String) {
+ Proxy("Proxy"),
+ VPN("VPN")
+}
diff --git a/app/src/main/java/io/github/dovecoteescapee/byedpi/data/ServiceStatus.kt b/app/src/main/java/io/github/dovecoteescapee/byedpi/data/ServiceStatus.kt
new file mode 100644
index 0000000..213959f
--- /dev/null
+++ b/app/src/main/java/io/github/dovecoteescapee/byedpi/data/ServiceStatus.kt
@@ -0,0 +1,7 @@
+package io.github.dovecoteescapee.byedpi.data
+
+enum class ServiceStatus {
+ DISCONNECTED,
+ CONNECTED,
+ FAILED,
+}
diff --git a/app/src/main/java/io/github/dovecoteescapee/byedpi/fragments/SettingsFragment.kt b/app/src/main/java/io/github/dovecoteescapee/byedpi/fragments/SettingsFragment.kt
new file mode 100644
index 0000000..dc919a5
--- /dev/null
+++ b/app/src/main/java/io/github/dovecoteescapee/byedpi/fragments/SettingsFragment.kt
@@ -0,0 +1,131 @@
+package io.github.dovecoteescapee.byedpi.fragments
+
+import android.net.InetAddresses
+import android.os.Build
+import android.os.Bundle
+import android.util.Log
+import android.util.Patterns
+import android.widget.Toast
+import androidx.appcompat.app.AppCompatDelegate
+import androidx.preference.DropDownPreference
+import androidx.preference.EditTextPreference
+import androidx.preference.Preference
+import androidx.preference.PreferenceFragmentCompat
+import io.github.dovecoteescapee.byedpi.BuildConfig
+import io.github.dovecoteescapee.byedpi.R
+
+class SettingsFragment : PreferenceFragmentCompat() {
+ companion object {
+ private val TAG: String = SettingsFragment::class.java.simpleName
+
+ fun setTheme(name: String): Boolean = when (val theme = themeByName(name)) {
+ null -> {
+ Log.w(TAG, "Invalid value for app_theme: $name")
+ false
+ }
+
+ else -> {
+ AppCompatDelegate.setDefaultNightMode(theme)
+ true
+ }
+ }
+
+ private fun themeByName(name: String): Int? = when (name) {
+ "system" -> AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM
+ "light" -> AppCompatDelegate.MODE_NIGHT_NO
+ "dark" -> AppCompatDelegate.MODE_NIGHT_YES
+ else -> {
+ Log.w(TAG, "Invalid value for app_theme: $name")
+ null
+ }
+ }
+
+ private fun checkIp(ip: String): Boolean =
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
+ InetAddresses.isNumericAddress(ip)
+ } else {
+ // This pattern doesn't not support IPv6
+ Patterns.IP_ADDRESS.matcher(ip).matches()
+ }
+ }
+
+ override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
+ setPreferencesFromResource(R.xml.settings, rootKey)
+
+ findPreference("app_theme")
+ ?.setOnPreferenceChangeListener { preference, newValue ->
+ setTheme(newValue as String)
+ }
+
+ setEditTextPreferenceListener("dns_ip") { checkIp(it) }
+ setEditTextPreferenceListener("byedpi_proxy_ip") { checkIp(it) }
+ setEditTestPreferenceListenerPort("byedpi_proxy_port")
+ setEditTestPreferenceListenerInt(
+ "byedpi_max_connections",
+ 1,
+ Short.MAX_VALUE.toInt()
+ )
+ setEditTestPreferenceListenerInt(
+ "byedpi_buffer_size",
+ 1,
+ Int.MAX_VALUE / 4
+ )
+ setEditTestPreferenceListenerInt("byedpi_default_ttl", 0, 255)
+ setEditTestPreferenceListenerInt(
+ "byedpi_split_position",
+ Int.MIN_VALUE,
+ Int.MAX_VALUE
+ )
+ setEditTestPreferenceListenerInt("byedpi_fake_ttl", 1, 255)
+ setEditTestPreferenceListenerInt(
+ "byedpi_tlsrec_position",
+ 2 * Short.MIN_VALUE,
+ 2 * Short.MAX_VALUE,
+ )
+
+ findPreference("version")?.summary =
+ BuildConfig.VERSION_NAME
+ }
+
+ private fun setEditTestPreferenceListenerPort(key: String) {
+ setEditTestPreferenceListenerInt(key, 1, 65535)
+ }
+
+ private fun setEditTestPreferenceListenerInt(
+ key: String,
+ min: Int = Int.MIN_VALUE,
+ max: Int = Int.MAX_VALUE
+ ) {
+ setEditTextPreferenceListener(key) { value ->
+ value.toIntOrNull()?.let { it in min..max } ?: false
+ }
+ }
+
+ private fun setEditTextPreferenceListener(key: String, check: (String) -> Boolean) {
+ findPreference(key)
+ ?.setOnPreferenceChangeListener { preference, newValue ->
+ when (newValue) {
+ is String -> {
+ val valid = check(newValue)
+ if (!valid) {
+ Toast.makeText(
+ requireContext(),
+ "Invalid value for ${preference.title}: $newValue",
+ Toast.LENGTH_SHORT
+ ).show()
+ }
+ valid
+ }
+
+ else -> {
+ Log.w(
+ TAG,
+ "Invalid type for ${preference.key}: " +
+ "$newValue has type ${newValue::class.java}"
+ )
+ false
+ }
+ }
+ }
+ }
+}
diff --git a/app/src/main/java/io/github/dovecoteescapee/byedpi/services/ByeDpiProxyService.kt b/app/src/main/java/io/github/dovecoteescapee/byedpi/services/ByeDpiProxyService.kt
new file mode 100644
index 0000000..da9942d
--- /dev/null
+++ b/app/src/main/java/io/github/dovecoteescapee/byedpi/services/ByeDpiProxyService.kt
@@ -0,0 +1,180 @@
+package io.github.dovecoteescapee.byedpi.services
+
+import android.app.Notification
+import android.content.Intent
+import android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_SPECIAL_USE
+import android.os.Build
+import android.util.Log
+import androidx.lifecycle.LifecycleService
+import androidx.lifecycle.lifecycleScope
+import io.github.dovecoteescapee.byedpi.R
+import io.github.dovecoteescapee.byedpi.core.ByeDpiProxy
+import io.github.dovecoteescapee.byedpi.core.ByeDpiProxyPreferences
+import io.github.dovecoteescapee.byedpi.data.START_ACTION
+import io.github.dovecoteescapee.byedpi.data.STOP_ACTION
+import io.github.dovecoteescapee.byedpi.data.FAILED_BROADCAST
+import io.github.dovecoteescapee.byedpi.data.SENDER
+import io.github.dovecoteescapee.byedpi.data.STARTED_BROADCAST
+import io.github.dovecoteescapee.byedpi.data.STOPPED_BROADCAST
+import io.github.dovecoteescapee.byedpi.data.Sender
+import io.github.dovecoteescapee.byedpi.data.ServiceStatus
+import io.github.dovecoteescapee.byedpi.utility.createConnectionNotification
+import io.github.dovecoteescapee.byedpi.utility.getPreferences
+import io.github.dovecoteescapee.byedpi.utility.registerNotificationChannel
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.withContext
+
+class ByeDpiProxyService : LifecycleService() {
+ private var proxy: ByeDpiProxy? = null
+ private var proxyJob: Job? = null
+
+ companion object {
+ private val TAG: String = ByeDpiProxyService::class.java.simpleName
+ private const val FOREGROUND_SERVICE_ID: Int = 2
+ private const val NOTIFICATION_CHANNEL_ID: String = "ByeDPI Proxy"
+
+ @Volatile
+ private var status: ServiceStatus = ServiceStatus.DISCONNECTED
+ }
+
+ override fun onCreate() {
+ super.onCreate()
+ registerNotificationChannel(
+ this,
+ NOTIFICATION_CHANNEL_ID,
+ R.string.proxy_channel_name,
+ )
+ }
+
+ override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
+ super.onStartCommand(intent, flags, startId)
+ return when (val action = intent?.action) {
+ START_ACTION -> {
+ lifecycleScope.launch { start() }
+ START_STICKY
+ }
+
+ STOP_ACTION -> {
+ lifecycleScope.launch { stop() }
+ START_NOT_STICKY
+ }
+
+ else -> {
+ Log.w(TAG, "Unknown action: $action")
+ START_NOT_STICKY
+ }
+ }
+ }
+
+ private suspend fun start() {
+ Log.i(TAG, "Starting")
+
+ if (status == ServiceStatus.CONNECTED) {
+ Log.w(TAG, "Proxy already connected")
+ return
+ }
+
+ try {
+ startProxy()
+ updateStatus(ServiceStatus.CONNECTED)
+ startForeground()
+ } catch (e: Exception) {
+ Log.e(TAG, "Failed to start proxy", e)
+ updateStatus(ServiceStatus.FAILED)
+ stop()
+ }
+ }
+
+ private fun startForeground() {
+ val notification: Notification = createNotification()
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
+ startForeground(
+ FOREGROUND_SERVICE_ID,
+ notification,
+ FOREGROUND_SERVICE_TYPE_SPECIAL_USE,
+ )
+ } else {
+ startForeground(FOREGROUND_SERVICE_ID, notification)
+ }
+ }
+
+ private suspend fun stop() {
+ Log.i(TAG, "Stopping VPN")
+
+ stopProxy()
+ updateStatus(ServiceStatus.DISCONNECTED)
+ stopSelf()
+ }
+
+ private suspend fun startProxy() {
+ Log.i(TAG, "Starting proxy")
+
+ if (proxy != null || proxyJob != null) {
+ Log.w(TAG, "Proxy fields not null")
+ throw IllegalStateException("Proxy fields not null")
+ }
+
+ proxy = ByeDpiProxy()
+ val preferences = getByeDpiPreferences()
+
+ proxyJob = lifecycleScope.launch(Dispatchers.IO) {
+ val code = proxy?.startProxy(preferences)
+
+ withContext(Dispatchers.Main) {
+ if (code != 0) {
+ Log.e(TAG, "Proxy stopped with code $code")
+ updateStatus(ServiceStatus.FAILED)
+ } else {
+ updateStatus(ServiceStatus.DISCONNECTED)
+ }
+ }
+ }
+
+ Log.i(TAG, "Proxy started")
+ }
+
+ private suspend fun stopProxy() {
+ Log.i(TAG, "Stopping proxy")
+
+ if (status == ServiceStatus.DISCONNECTED) {
+ Log.w(TAG, "Proxy already disconnected")
+ return
+ } else {
+ proxy?.stopProxy()
+ proxyJob?.join()
+ proxy = null
+ proxyJob = null
+ }
+
+ Log.i(TAG, "Proxy stopped")
+ }
+
+ private fun getByeDpiPreferences(): ByeDpiProxyPreferences =
+ ByeDpiProxyPreferences(getPreferences(this))
+
+ private fun updateStatus(newStatus: ServiceStatus) {
+ Log.d(TAG, "Proxy status changed from $status to $newStatus")
+
+ status = newStatus
+ val intent = Intent(
+ when (newStatus) {
+ ServiceStatus.CONNECTED -> STARTED_BROADCAST
+ ServiceStatus.DISCONNECTED -> STOPPED_BROADCAST
+ ServiceStatus.FAILED -> FAILED_BROADCAST
+ }
+ )
+ intent.putExtra(SENDER, Sender.Proxy.ordinal)
+ sendBroadcast(intent)
+ }
+
+ private fun createNotification(): Notification =
+ createConnectionNotification(
+ this,
+ NOTIFICATION_CHANNEL_ID,
+ R.string.notification_title,
+ R.string.proxy_notification_content,
+ ByeDpiProxyService::class.java,
+ )
+}
diff --git a/app/src/main/java/io/github/dovecoteescapee/byedpi/services/ByeDpiVpnService.kt b/app/src/main/java/io/github/dovecoteescapee/byedpi/services/ByeDpiVpnService.kt
new file mode 100644
index 0000000..87e06f1
--- /dev/null
+++ b/app/src/main/java/io/github/dovecoteescapee/byedpi/services/ByeDpiVpnService.kt
@@ -0,0 +1,286 @@
+package io.github.dovecoteescapee.byedpi.services
+
+import android.app.Notification
+import android.app.PendingIntent
+import android.content.Intent
+import android.content.pm.ServiceInfo
+import android.os.Build
+import android.os.ParcelFileDescriptor
+import android.util.Log
+import androidx.lifecycle.lifecycleScope
+import engine.Engine
+import engine.Key
+import io.github.dovecoteescapee.byedpi.BuildConfig
+import io.github.dovecoteescapee.byedpi.R
+import io.github.dovecoteescapee.byedpi.activities.MainActivity
+import io.github.dovecoteescapee.byedpi.core.ByeDpiProxy
+import io.github.dovecoteescapee.byedpi.core.ByeDpiProxyPreferences
+import io.github.dovecoteescapee.byedpi.data.START_ACTION
+import io.github.dovecoteescapee.byedpi.data.STOP_ACTION
+import io.github.dovecoteescapee.byedpi.data.FAILED_BROADCAST
+import io.github.dovecoteescapee.byedpi.data.SENDER
+import io.github.dovecoteescapee.byedpi.data.STARTED_BROADCAST
+import io.github.dovecoteescapee.byedpi.data.STOPPED_BROADCAST
+import io.github.dovecoteescapee.byedpi.data.Sender
+import io.github.dovecoteescapee.byedpi.data.ServiceStatus
+import io.github.dovecoteescapee.byedpi.utility.createConnectionNotification
+import io.github.dovecoteescapee.byedpi.utility.getPreferences
+import io.github.dovecoteescapee.byedpi.utility.registerNotificationChannel
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.sync.Semaphore
+import kotlinx.coroutines.sync.withPermit
+import kotlinx.coroutines.withContext
+
+class ByeDpiVpnService : LifecycleVpnService() {
+ private var proxy: ByeDpiProxy? = null
+ private var proxyJob: Job? = null
+ private var vpn: ParcelFileDescriptor? = null
+ private val semaphore = Semaphore(1)
+
+ @Volatile
+ private var stopping: Boolean = false
+
+ companion object {
+ private val TAG: String = ByeDpiVpnService::class.java.simpleName
+ private const val FOREGROUND_SERVICE_ID: Int = 1
+ private const val NOTIFICATION_CHANNEL_ID: String = "ByeDPIVpn"
+
+ @Volatile
+ private var status: ServiceStatus = ServiceStatus.DISCONNECTED
+ }
+
+ override fun onCreate() {
+ super.onCreate()
+ registerNotificationChannel(
+ this,
+ NOTIFICATION_CHANNEL_ID,
+ R.string.vpn_channel_name,
+ )
+ }
+
+ override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
+ super.onStartCommand(intent, flags, startId)
+ return when (val action = intent?.action) {
+ START_ACTION -> {
+ lifecycleScope.launch { start() }
+ START_STICKY
+ }
+
+ STOP_ACTION -> {
+ lifecycleScope.launch { stop() }
+ START_NOT_STICKY
+ }
+
+ else -> {
+ Log.w(TAG, "Unknown action: $action")
+ START_NOT_STICKY
+ }
+ }
+ }
+
+ override fun onRevoke() {
+ Log.i(TAG, "VPN revoked")
+ lifecycleScope.launch { stop() }
+ }
+
+ private suspend fun start() {
+ Log.i(TAG, "Starting")
+
+ if (status == ServiceStatus.CONNECTED) {
+ Log.w(TAG, "VPN already connected")
+ return
+ }
+
+ try {
+ semaphore.withPermit {
+ startProxy()
+ startTun2Socks()
+ }
+ updateStatus(ServiceStatus.CONNECTED)
+ startForeground()
+ } catch (e: Exception) {
+ Log.e(TAG, "Failed to start VPN", e)
+ updateStatus(ServiceStatus.FAILED)
+ stop()
+ }
+ }
+
+ private fun startForeground() {
+ val notification: Notification = createNotification()
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
+ startForeground(
+ FOREGROUND_SERVICE_ID,
+ notification,
+ ServiceInfo.FOREGROUND_SERVICE_TYPE_SPECIAL_USE,
+ )
+ } else {
+ startForeground(FOREGROUND_SERVICE_ID, notification)
+ }
+ }
+
+ private suspend fun stop() {
+ Log.i(TAG, "Stopping")
+
+ // Wait end of starting
+ semaphore.withPermit {
+ stopping = true
+ try {
+ stopTun2Socks()
+ stopProxy()
+ } catch (e: Exception) {
+ Log.e(TAG, "Failed to stop VPN", e)
+ } finally {
+ stopping = false
+ }
+ }
+
+ updateStatus(ServiceStatus.DISCONNECTED)
+ stopSelf()
+ }
+
+ private suspend fun startProxy() {
+ Log.i(TAG, "Starting proxy")
+
+ if (proxy != null || proxyJob != null) {
+ Log.w(TAG, "Proxy fields not null")
+ throw IllegalStateException("Proxy fields not null")
+ }
+
+ proxy = ByeDpiProxy()
+ val preferences = getByeDpiPreferences()
+
+ proxyJob = lifecycleScope.launch(Dispatchers.IO) {
+ val code = proxy?.startProxy(preferences)
+
+ withContext(Dispatchers.Main) {
+ if (code != 0) {
+ Log.e(TAG, "Proxy stopped with code $code")
+ updateStatus(ServiceStatus.FAILED)
+ } else {
+ if (!stopping) {
+ stop()
+ updateStatus(ServiceStatus.DISCONNECTED)
+ }
+ }
+ }
+ }
+
+ Log.i(TAG, "Proxy started")
+ }
+
+ private suspend fun stopProxy() {
+ Log.i(TAG, "Stopping proxy")
+
+ if (status == ServiceStatus.DISCONNECTED) {
+ Log.w(TAG, "Proxy already disconnected")
+ return
+ } else {
+ proxy?.stopProxy() ?: throw IllegalStateException("Proxy field null")
+ proxyJob?.join() ?: throw IllegalStateException("ProxyJob field null")
+ proxy = null
+ proxyJob = null
+ }
+
+ Log.i(TAG, "Proxy stopped")
+ }
+
+ private fun startTun2Socks() {
+ Log.i(TAG, "Starting tun2socks")
+
+ if (vpn != null) {
+ throw IllegalStateException("VPN field not null")
+ }
+
+ val sharedPreferences = getPreferences(this)
+ val port = sharedPreferences.getString("byedpi_proxy_port", null)?.toInt() ?: 1080
+ val dns = sharedPreferences.getString("dns_ip", null) ?: "9.9.9.9"
+
+ val vpn = createBuilder(dns).establish()
+ ?: throw IllegalStateException("VPN connection failed")
+
+ this.vpn = vpn
+// val fd = vpn.detachFd()
+ Engine.insert(createKey(vpn.fd, port))
+ Engine.start()
+
+ Log.i(TAG, "Tun2Socks started")
+ }
+
+ private fun stopTun2Socks() {
+ Log.i(TAG, "Stopping tun2socks")
+// Engine.stop() // sometimes crashes with fdsan
+ vpn?.close() ?: Log.w(TAG, "VPN not running") // Is engine close sockets?
+ vpn = null
+ Log.i(TAG, "Tun2socks stopped")
+ }
+
+ private fun getByeDpiPreferences(): ByeDpiProxyPreferences =
+ ByeDpiProxyPreferences(getPreferences(this))
+
+ private fun updateStatus(newStatus: ServiceStatus) {
+ Log.d(TAG, "VPN status changed from $status to $newStatus")
+
+ status = newStatus
+ val intent = Intent(
+ when (newStatus) {
+ ServiceStatus.CONNECTED -> STARTED_BROADCAST
+ ServiceStatus.DISCONNECTED -> STOPPED_BROADCAST
+ ServiceStatus.FAILED -> FAILED_BROADCAST
+ }
+ )
+ intent.putExtra(SENDER, Sender.VPN.ordinal)
+ sendBroadcast(intent)
+ }
+
+ private fun createNotification(): Notification =
+ createConnectionNotification(
+ this,
+ NOTIFICATION_CHANNEL_ID,
+ R.string.notification_title,
+ R.string.vpn_notification_content,
+ ByeDpiVpnService::class.java,
+ )
+
+ private fun createBuilder(dns: String): Builder {
+ val builder = Builder()
+ builder.setSession("ByeDPI")
+ builder.setConfigureIntent(
+ PendingIntent.getActivity(
+ this,
+ 0,
+ Intent(this, MainActivity::class.java),
+ PendingIntent.FLAG_IMMUTABLE,
+ )
+ )
+
+ builder.addAddress("10.10.10.10", 32)
+ builder.addRoute("0.0.0.0", 0)
+ builder.addRoute("0:0:0:0:0:0:0:0", 0)
+ builder.addDnsServer(dns)
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
+ builder.setMetered(false)
+ }
+
+ builder.addDisallowedApplication("io.github.dovecoteescapee.byedpi")
+
+ return builder
+ }
+
+ private fun createKey(fd: Int, port: Int): Key = Key().apply {
+ mark = 0
+ mtu = 0
+ device = "fd://${fd}"
+
+ setInterface("")
+ logLevel = if (BuildConfig.DEBUG) "debug" else "info"
+ udpProxy = "direct://"
+ tcpProxy = "socks5://127.0.0.1:$port"
+
+ restAPI = ""
+ tcpSendBufferSize = ""
+ tcpReceiveBufferSize = ""
+ tcpModerateReceiveBuffer = false
+ }
+}
diff --git a/app/src/main/java/io/github/dovecoteescapee/byedpi/services/LifecycleVpnService.kt b/app/src/main/java/io/github/dovecoteescapee/byedpi/services/LifecycleVpnService.kt
new file mode 100644
index 0000000..d4bd7e4
--- /dev/null
+++ b/app/src/main/java/io/github/dovecoteescapee/byedpi/services/LifecycleVpnService.kt
@@ -0,0 +1,51 @@
+package io.github.dovecoteescapee.byedpi.services
+
+import android.content.Intent
+import android.net.VpnService
+import android.os.IBinder
+import androidx.annotation.CallSuper
+import androidx.lifecycle.Lifecycle
+import androidx.lifecycle.LifecycleOwner
+import androidx.lifecycle.ServiceLifecycleDispatcher
+
+open class LifecycleVpnService : VpnService(), LifecycleOwner {
+ private val dispatcher = ServiceLifecycleDispatcher(this)
+
+ @CallSuper
+ override fun onCreate() {
+ dispatcher.onServicePreSuperOnCreate()
+ super.onCreate()
+ }
+
+ @CallSuper
+ override fun onBind(intent: Intent): IBinder? {
+ dispatcher.onServicePreSuperOnBind()
+ return super.onBind(intent)
+ }
+
+ @Deprecated("Deprecated in Java")
+ @Suppress("DEPRECATION")
+ @CallSuper
+ override fun onStart(intent: Intent?, startId: Int) {
+ dispatcher.onServicePreSuperOnStart()
+ super.onStart(intent, startId)
+ }
+
+ // this method is added only to annotate it with @CallSuper.
+ // In usual Service, super.onStartCommand is no-op, but in LifecycleService
+ // it results in dispatcher.onServicePreSuperOnStart() call, because
+ // super.onStartCommand calls onStart().
+ @CallSuper
+ override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
+ return super.onStartCommand(intent, flags, startId)
+ }
+
+ @CallSuper
+ override fun onDestroy() {
+ dispatcher.onServicePreSuperOnDestroy()
+ super.onDestroy()
+ }
+
+ override val lifecycle: Lifecycle
+ get() = dispatcher.lifecycle
+}
\ No newline at end of file
diff --git a/app/src/main/java/io/github/dovecoteescapee/byedpi/utility/NotificationUtils.kt b/app/src/main/java/io/github/dovecoteescapee/byedpi/utility/NotificationUtils.kt
new file mode 100644
index 0000000..e7850a7
--- /dev/null
+++ b/app/src/main/java/io/github/dovecoteescapee/byedpi/utility/NotificationUtils.kt
@@ -0,0 +1,61 @@
+package io.github.dovecoteescapee.byedpi.utility
+
+import android.app.Notification
+import android.app.NotificationChannel
+import android.app.NotificationManager
+import android.app.PendingIntent
+import android.content.Context
+import android.content.Intent
+import android.os.Build
+import androidx.annotation.StringRes
+import androidx.core.app.NotificationCompat
+import io.github.dovecoteescapee.byedpi.R
+import io.github.dovecoteescapee.byedpi.activities.MainActivity
+import io.github.dovecoteescapee.byedpi.data.STOP_ACTION
+
+fun registerNotificationChannel(context: Context, id: String, @StringRes name: Int) {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+ val manager = context.getSystemService(NotificationManager::class.java) ?: return
+
+ val channel = NotificationChannel(
+ id,
+ context.getString(name),
+ NotificationManager.IMPORTANCE_DEFAULT
+ )
+ channel.enableLights(false)
+ channel.enableVibration(false)
+ channel.setShowBadge(false)
+
+ manager.createNotificationChannel(channel)
+ }
+}
+
+fun createConnectionNotification(
+ context: Context,
+ channelId: String,
+ @StringRes title: Int,
+ @StringRes content: Int,
+ service: Class<*>,
+): Notification =
+ NotificationCompat.Builder(context, channelId)
+ .setSmallIcon(R.mipmap.ic_launcher)
+ .setSilent(true)
+ .setContentTitle(context.getString(title))
+ .setContentText(context.getString(content))
+ .addAction(0, "Stop",
+ PendingIntent.getService(
+ context,
+ 0,
+ Intent(context, service).setAction(STOP_ACTION),
+ PendingIntent.FLAG_IMMUTABLE,
+ )
+ )
+ .setContentIntent(
+ PendingIntent.getActivity(
+ context,
+ 0,
+ Intent(context, MainActivity::class.java),
+ PendingIntent.FLAG_IMMUTABLE,
+ )
+ )
+ .build()
\ No newline at end of file
diff --git a/app/src/main/java/io/github/dovecoteescapee/byedpi/utility/PreferencesUtils.kt b/app/src/main/java/io/github/dovecoteescapee/byedpi/utility/PreferencesUtils.kt
new file mode 100644
index 0000000..92ef3a5
--- /dev/null
+++ b/app/src/main/java/io/github/dovecoteescapee/byedpi/utility/PreferencesUtils.kt
@@ -0,0 +1,15 @@
+package io.github.dovecoteescapee.byedpi.utility
+
+import android.content.Context
+import android.content.SharedPreferences
+import androidx.preference.PreferenceManager
+import io.github.dovecoteescapee.byedpi.data.Mode
+
+fun getPreferences(context: Context): SharedPreferences =
+ PreferenceManager.getDefaultSharedPreferences(context)
+
+fun SharedPreferences.getStringNotNull(key: String, defValue: String): String =
+ getString(key, defValue) ?: defValue
+
+fun SharedPreferences.mode(): Mode =
+ Mode.fromString(getStringNotNull("byedpi_mode", "vpn"))
diff --git a/app/src/main/res/drawable/ic_github_36.xml b/app/src/main/res/drawable/ic_github_36.xml
new file mode 100644
index 0000000..1a054d2
--- /dev/null
+++ b/app/src/main/res/drawable/ic_github_36.xml
@@ -0,0 +1,8 @@
+
+
+
diff --git a/app/src/main/res/drawable/outline_settings_24.xml b/app/src/main/res/drawable/outline_settings_24.xml
deleted file mode 100644
index 549f086..0000000
--- a/app/src/main/res/drawable/outline_settings_24.xml
+++ /dev/null
@@ -1,5 +0,0 @@
-
-
-
diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml
index 5d31311..2681fbb 100644
--- a/app/src/main/res/layout/activity_main.xml
+++ b/app/src/main/res/layout/activity_main.xml
@@ -4,23 +4,49 @@
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
- tools:context=".MainActivity">
+ tools:context=".activities.MainActivity">
+ app:layout_constraintVertical_chainStyle="packed"
+ tools:text="@string/vpn_connect" />
+
+
+
+
diff --git a/app/src/main/res/menu/menu_main.xml b/app/src/main/res/menu/menu_main.xml
index cf98ff6..6361c00 100644
--- a/app/src/main/res/menu/menu_main.xml
+++ b/app/src/main/res/menu/menu_main.xml
@@ -2,7 +2,7 @@
diff --git a/app/src/main/res/menu/menu_settings.xml b/app/src/main/res/menu/menu_settings.xml
index 7b83655..84c8730 100644
--- a/app/src/main/res/menu/menu_settings.xml
+++ b/app/src/main/res/menu/menu_settings.xml
@@ -2,7 +2,7 @@
diff --git a/app/src/main/res/values-night/themes.xml b/app/src/main/res/values-night/themes.xml
index 0e99879..bfbf657 100644
--- a/app/src/main/res/values-night/themes.xml
+++ b/app/src/main/res/values-night/themes.xml
@@ -4,13 +4,10 @@
- @color/white
- #E5E5E5
- - @color/white
+ - @color/black
- - #38AF42
- - #1F8C28
- - @color/black
-
- - ?attr/colorPrimaryVariant
-
+ - #5976DF
+ - #3C52A3
+ - @color/white
diff --git a/app/src/main/res/values/arrays.xml b/app/src/main/res/values/arrays.xml
index 31ce441..6b27fa8 100644
--- a/app/src/main/res/values/arrays.xml
+++ b/app/src/main/res/values/arrays.xml
@@ -1,5 +1,25 @@
+
+ - System
+ - Light
+ - Dark
+
+
+ - system
+ - light
+ - dark
+
+
+
+ - VPN
+ - Proxy
+
+
+ - vpn
+ - proxy
+
+
- None
- Split
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index a4a522a..cd6688a 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -1,7 +1,17 @@
ByeDPI
- Start
- Stop
+ Connect
+ Disconnect
+ Connected
+ Disconnected
+ Proxy is up
+ Proxy is down
+ Start
+ Stop
+ Connecting…
+ Disconnecting…
+ Starting
+ Stopping
Settings
VPN permission denied
Please stop the VPN service before changing settings
@@ -9,4 +19,34 @@
Save logs
Reset
Failed to collect logs
+ %1$s:%2$s
+ Theme
+ Mode
+ DNS
+ Listen address
+ Port
+ Maximum number of connections
+ Buffer size
+ Default TTL
+ No domain
+ Desync only HTTPS and TLS
+ Desync method
+ Split position
+ Split at host
+ TTL of fake packets
+ Host mixed case
+ Domain mixed case
+ Host remove spaces
+ Split TLS record
+ TLS record split position
+ Split TLS record at SNI
+ Version
+ Source code
+ What does all of this mean?
+ Failed to start %1$s
+ VPN
+ Proxy
+ ByeDPI
+ VPN is running
+ Proxy is running
\ No newline at end of file
diff --git a/app/src/main/res/values/themes.xml b/app/src/main/res/values/themes.xml
index bf9d93b..5640a33 100644
--- a/app/src/main/res/values/themes.xml
+++ b/app/src/main/res/values/themes.xml
@@ -6,9 +6,9 @@
- #3C52A3
- @color/white
- - #38AF42
- - #1F8C28
- - @color/black
+ - #5976DF
+ - #3C52A3
+ - @color/white
- ?attr/colorPrimaryVariant
diff --git a/app/src/main/res/xml/settings.xml b/app/src/main/res/xml/settings.xml
index c318e6b..78d957a 100644
--- a/app/src/main/res/xml/settings.xml
+++ b/app/src/main/res/xml/settings.xml
@@ -1,109 +1,172 @@
-
+
-
+
-
+
-
+
-
+
-
+
+
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+