diff --git a/.gitignore b/.gitignore
index aa724b7..aedf400 100644
--- a/.gitignore
+++ b/.gitignore
@@ -2,6 +2,7 @@
.gradle
/local.properties
/.idea/caches
+/.idea/deploymentTargetDropDown.xml
/.idea/libraries
/.idea/modules.xml
/.idea/workspace.xml
diff --git a/.idea/deploymentTargetDropDown.xml b/.idea/deploymentTargetDropDown.xml
deleted file mode 100644
index 0c0c338..0000000
--- a/.idea/deploymentTargetDropDown.xml
+++ /dev/null
@@ -1,10 +0,0 @@
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/README.md b/README.md
index b56bd63..441b1e0 100644
--- a/README.md
+++ b/README.md
@@ -1,6 +1,6 @@
# ByeDPI for Android
-Application for Android that start a local VPN service to bypass DPI (Deep Packet Inspection) and unblock the internet
+Application for Android that start a local VPN service to bypass DPI (Deep Packet Inspection) and censorship.
## Features
diff --git a/app/.gitignore b/app/.gitignore
index 4c49c4e..b7e35bd 100644
--- a/app/.gitignore
+++ b/app/.gitignore
@@ -1,3 +1,5 @@
/build
+/debug
+/release
*.aar
*.jar
\ No newline at end of file
diff --git a/app/build.gradle.kts b/app/build.gradle.kts
index f429cf4..db05b74 100644
--- a/app/build.gradle.kts
+++ b/app/build.gradle.kts
@@ -13,8 +13,8 @@ android {
applicationId = "io.github.dovecoteescapee.byedpi"
minSdk = 24
targetSdk = 34
- versionCode = 1
- versionName = "0.1.0-alpha"
+ versionCode = 2
+ versionName = "0.1.1-alpha"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
}
diff --git a/app/libs/tun2socks b/app/libs/tun2socks
index 792ee44..da7fa48 160000
--- a/app/libs/tun2socks
+++ b/app/libs/tun2socks
@@ -1 +1 @@
-Subproject commit 792ee44a8efa7ec4daadd2cbfda762329d1cbff2
+Subproject commit da7fa48a3784a5dca6714d4734a3eb3b181a96aa
diff --git a/app/release/byedpi.apk b/app/release/byedpi.apk
deleted file mode 100644
index 0b8e873..0000000
Binary files a/app/release/byedpi.apk and /dev/null differ
diff --git a/app/release/output-metadata.json b/app/release/output-metadata.json
deleted file mode 100644
index c402b78..0000000
--- a/app/release/output-metadata.json
+++ /dev/null
@@ -1,20 +0,0 @@
-{
- "version": 3,
- "artifactType": {
- "type": "APK",
- "kind": "Directory"
- },
- "applicationId": "io.github.dovecoteescapee.byedpi",
- "variantName": "release",
- "elements": [
- {
- "type": "SINGLE",
- "filters": [],
- "attributes": [],
- "versionCode": 1,
- "versionName": "0.1.0-alpha",
- "outputFile": "app-release.apk"
- }
- ],
- "elementType": "File"
-}
\ No newline at end of file
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 1719579..fc8effe 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -29,7 +29,8 @@
+ android:label="@string/title_settings"
+ android:exported="false"/>
#include
#include
+#include
#include
+#include
+#include
-extern int big_loop(int fd);
+extern int NOT_EXIT;
struct packet fake_tls = {
sizeof(tls_data), tls_data
@@ -33,7 +36,7 @@ struct params params = {
.max_open = 512,
.bfsize = 16384,
.baddr = {
- .sin6_family = AF_INET6
+ .sin6_family = AF_INET
},
.debug = 2
};
@@ -55,8 +58,16 @@ int get_default_ttl()
return orig_ttl;
}
-JNIEXPORT jint JNICALL
-Java_io_github_dovecoteescapee_byedpi_ByeDpiVpnService_startProxy__IIIIZZIIZIZZZIZ(
+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(
JNIEnv *env,
jobject thiz,
jint port,
@@ -72,7 +83,8 @@ Java_io_github_dovecoteescapee_byedpi_ByeDpiVpnService_startProxy__IIIIZZIIZIZZZ
jboolean host_mixed_case,
jboolean domain_mixed_case,
jboolean host_remove_space,
- jint tls_record_split,
+ 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};
@@ -89,6 +101,7 @@ Java_io_github_dovecoteescapee_byedpi_ByeDpiVpnService_startProxy__IIIIZZIIZIZZZ
params.mod_http |= domain_mixed_case ? MH_DMIX : 0;
params.mod_http |= host_remove_space ? MH_SPACE : 0;
params.tlsrec = tls_record_split;
+ params.tlsrec_pos = tls_record_split_position;
params.tlsrec_sni = tls_record_split_at_sni;
if (!params.def_ttl && params.attack != DESYNC_NONE) {
@@ -97,12 +110,23 @@ Java_io_github_dovecoteescapee_byedpi_ByeDpiVpnService_startProxy__IIIIZZIIZIZZZ
}
}
- struct sockaddr_ina srv = {
- .in = {
- .sin_family = AF_INET,
- .sin_port = htons(port),
- }
- };
+ 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);
- return listener(srv);
-}
\ No newline at end of file
+ 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;
+ }
+
+ return proxy_thread;
+}
+
+JNIEXPORT void JNICALL
+Java_io_github_dovecoteescapee_byedpi_ByeDpiVpnService_jniStopProxy(JNIEnv *env, jobject thiz, jlong proxy_thread) {
+ NOT_EXIT = 0;
+}
diff --git a/app/src/main/java/io/github/dovecoteescapee/byedpi/ByeDpiProxyPreferences.kt b/app/src/main/java/io/github/dovecoteescapee/byedpi/ByeDpiProxyPreferences.kt
index 177d39e..f3583f3 100644
--- a/app/src/main/java/io/github/dovecoteescapee/byedpi/ByeDpiProxyPreferences.kt
+++ b/app/src/main/java/io/github/dovecoteescapee/byedpi/ByeDpiProxyPreferences.kt
@@ -14,7 +14,8 @@ class ByeDpiProxyPreferences(
hostMixedCase: Boolean? = null,
domainMixedCase: Boolean? = null,
hostRemoveSpaces: Boolean? = null,
- tlsRecordSplit: Int? = null,
+ tlsRecordSplit: Boolean? = null,
+ tlsRecordSplitPosition: Int? = null,
tlsRecordSplitAtSni: Boolean? = null,
) {
val port: Int = port ?: 1080
@@ -23,14 +24,15 @@ class ByeDpiProxyPreferences(
val defaultTtl: Int = defaultTtl ?: 0
val noDomain: Boolean = noDomain ?: false
val desyncKnown: Boolean = desyncKnown ?: false
- val desyncMethod: DesyncMethod = desyncMethod ?: DesyncMethod.None
+ val desyncMethod: DesyncMethod = desyncMethod ?: DesyncMethod.Disorder
val splitPosition: Int = splitPosition ?: 3
val splitAtHost: Boolean = splitAtHost ?: false
val fakeTtl: Int = fakeTtl ?: 8
val hostMixedCase: Boolean = hostMixedCase ?: false
val domainMixedCase: Boolean = domainMixedCase ?: false
val hostRemoveSpaces: Boolean = hostRemoveSpaces ?: false
- val tlsRecordSplit: Int = tlsRecordSplit ?: 0
+ val tlsRecordSplit: Boolean = tlsRecordSplit ?: false
+ val tlsRecordSplitPosition: Int = tlsRecordSplitPosition ?: 0
val tlsRecordSplitAtSni: Boolean = tlsRecordSplitAtSni ?: false
enum class DesyncMethod {
diff --git a/app/src/main/java/io/github/dovecoteescapee/byedpi/ByeDpiVpnService.kt b/app/src/main/java/io/github/dovecoteescapee/byedpi/ByeDpiVpnService.kt
index f3f954b..56b56b9 100644
--- a/app/src/main/java/io/github/dovecoteescapee/byedpi/ByeDpiVpnService.kt
+++ b/app/src/main/java/io/github/dovecoteescapee/byedpi/ByeDpiVpnService.kt
@@ -5,28 +5,26 @@ 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.lifecycle.lifecycleScope
import androidx.preference.PreferenceManager
import engine.Engine
import engine.Key
-import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.Job
-import kotlinx.coroutines.launch
-
class ByeDpiVpnService : VpnService(), LifecycleOwner {
- private val TAG: String = this::class.java.simpleName
- private var proxyJob: Job? = null
+ 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
}
@@ -48,6 +46,7 @@ class ByeDpiVpnService : VpnService(), LifecycleOwner {
run()
START_STICKY
}
+
"stop" -> {
stop()
START_NOT_STICKY
@@ -75,26 +74,30 @@ class ByeDpiVpnService : VpnService(), LifecycleOwner {
}
private fun run() {
- val preferences = getPreferences();
-
+ val preferences = getPreferences()
status = Status.RUNNING
- if (proxyJob != null) {
+ if (proxyThread >= 0) {
Log.w(TAG, "Proxy already running")
return
}
- proxyJob = lifecycleScope.launch(Dispatchers.IO) {
- runProxy(preferences)
+ 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
}
- startTun2Socks(vpn.detachFd(), preferences.port)
+ Log.d(TAG, "fd: ${vpn.fd}")
+ startTun2Socks(vpn.fd, preferences.port)
}
private fun getPreferences(): ByeDpiProxyPreferences {
@@ -102,36 +105,38 @@ class ByeDpiVpnService : VpnService(), LifecycleOwner {
return ByeDpiProxyPreferences(
port =
- sharedPreferences.getString("byedpi_proxy_port", null)?.toInt(),
+ sharedPreferences.getString("byedpi_proxy_port", null)?.toInt(),
maxConnections =
- sharedPreferences.getString("byedpi_max_connections", null)?.toInt(),
+ sharedPreferences.getString("byedpi_max_connections", null)?.toInt(),
bufferSize =
- sharedPreferences.getString("byedpi_buffer_size", null)?.toInt(),
+ sharedPreferences.getString("byedpi_buffer_size", null)?.toInt(),
defaultTtl =
- sharedPreferences.getString("byedpi_default_ttl", null)?.toInt(),
+ sharedPreferences.getString("byedpi_default_ttl", null)?.toInt(),
noDomain =
- sharedPreferences.getBoolean("byedpi_no_domain", false),
+ sharedPreferences.getBoolean("byedpi_no_domain", false),
desyncKnown =
- sharedPreferences.getBoolean("byedpi_desync_known", false),
+ sharedPreferences.getBoolean("byedpi_desync_known", false),
desyncMethod =
- sharedPreferences.getString("byedpi_desync_method", null)
- ?.let { ByeDpiProxyPreferences.DesyncMethod.fromName(it) },
+ sharedPreferences.getString("byedpi_desync_method", null)
+ ?.let { ByeDpiProxyPreferences.DesyncMethod.fromName(it) },
splitPosition =
- sharedPreferences.getString("byedpi_split_position", null)?.toInt(),
+ sharedPreferences.getString("byedpi_split_position", null)?.toInt(),
splitAtHost =
- sharedPreferences.getBoolean("byedpi_split_at_host", false),
+ sharedPreferences.getBoolean("byedpi_split_at_host", false),
fakeTtl =
- sharedPreferences.getString("byedpi_fake_ttl", null)?.toInt(),
+ sharedPreferences.getString("byedpi_fake_ttl", null)?.toInt(),
hostMixedCase =
- sharedPreferences.getBoolean("byedpi_host_mixed_case", false),
+ sharedPreferences.getBoolean("byedpi_host_mixed_case", false),
domainMixedCase =
- sharedPreferences.getBoolean("byedpi_domain_mixed_case", false),
+ sharedPreferences.getBoolean("byedpi_domain_mixed_case", false),
hostRemoveSpaces =
- sharedPreferences.getBoolean("byedpi_host_remove_spaces", false),
+ sharedPreferences.getBoolean("byedpi_host_remove_spaces", false),
tlsRecordSplit =
- sharedPreferences.getString("byedpi_tlsrec", null)?.toInt(),
+ sharedPreferences.getBoolean("byedpi_tlsrec", false),
+ tlsRecordSplitPosition =
+ sharedPreferences.getString("byedpi_tlsrec_position", null)?.toInt(),
tlsRecordSplitAtSni =
- sharedPreferences.getBoolean("byedpi_tlsrec_at_sni", false),
+ sharedPreferences.getBoolean("byedpi_tlsrec_at_sni", false),
)
}
@@ -141,9 +146,9 @@ class ByeDpiVpnService : VpnService(), LifecycleOwner {
stopProxy()
}
- private fun runProxy(preferences: ByeDpiProxyPreferences) : Int {
+ private fun startProxy(preferences: ByeDpiProxyPreferences): Long {
Log.i(TAG, "Proxy started")
- val res = startProxy(
+ return jniStartProxy(
port = preferences.port,
maxConnections = preferences.maxConnections,
bufferSize = preferences.bufferSize,
@@ -158,21 +163,20 @@ class ByeDpiVpnService : VpnService(), LifecycleOwner {
domainMixedCase = preferences.domainMixedCase,
hostRemoveSpace = preferences.hostRemoveSpaces,
tlsRecordSplit = preferences.tlsRecordSplit,
+ tlsRecordSplitPosition = preferences.tlsRecordSplitPosition,
tlsRecordSplitAtSni = preferences.tlsRecordSplitAtSni,
)
- Log.i(TAG, "Proxy stopped")
- return res
}
private fun stopProxy() {
- proxyJob?.let {
- Log.i(TAG, "Proxy stopped")
- it.cancel()
- proxyJob = null
- } ?: Log.w(TAG, "Proxy not running")
+ if (proxyThread < 0) {
+ Log.w(TAG, "Proxy not running")
+ }
+ jniStopProxy(proxyThread)
+ proxyThread = -1
}
- private fun startTun2Socks(fd: Int, port:Int) {
+ private fun startTun2Socks(fd: Int, port: Int) {
val key = Key().apply {
mark = 0
mtu = 0
@@ -197,10 +201,10 @@ class ByeDpiVpnService : VpnService(), LifecycleOwner {
private fun stopTun2Socks() {
Log.i(TAG, "Tun2socks stopped")
- Engine.stop()
+ vpn?.close() ?: Log.w(TAG, "VPN not running")
}
- private external fun startProxy(
+ private external fun jniStartProxy(
port: Int,
maxConnections: Int,
bufferSize: Int,
@@ -214,9 +218,12 @@ class ByeDpiVpnService : VpnService(), LifecycleOwner {
hostMixedCase: Boolean,
domainMixedCase: Boolean,
hostRemoveSpace: Boolean,
- tlsRecordSplit: Int,
+ tlsRecordSplit: Boolean,
+ tlsRecordSplitPosition: Int,
tlsRecordSplitAtSni: Boolean,
- ): Int
+ ): Long
+
+ private external fun jniStopProxy(proxyThread: Long)
private fun getBuilder(): Builder {
val builder = Builder()
@@ -230,11 +237,13 @@ class ByeDpiVpnService : VpnService(), LifecycleOwner {
)
)
+ 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("1.1.1.1")
- builder.addDnsServer("1.0.0.1")
+ builder.addDnsServer(dns)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
builder.setMetered(false)
}
diff --git a/app/src/main/java/io/github/dovecoteescapee/byedpi/MainActivity.kt b/app/src/main/java/io/github/dovecoteescapee/byedpi/MainActivity.kt
index 184f67b..29e96cf 100644
--- a/app/src/main/java/io/github/dovecoteescapee/byedpi/MainActivity.kt
+++ b/app/src/main/java/io/github/dovecoteescapee/byedpi/MainActivity.kt
@@ -4,21 +4,23 @@ 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 androidx.preference.PreferenceManager
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()
+ 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)
@@ -44,19 +46,31 @@ class MainActivity : AppCompatActivity() {
}
}
- binding.settingsButton.setOnClickListener {
- 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)
- }
- }
-
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"
@@ -67,6 +81,7 @@ class MainActivity : AppCompatActivity() {
val intent = Intent(this, ByeDpiVpnService::class.java)
intent.action = "stop"
startService(intent)
+ stopService(intent)
}
private fun updateStatus(status : Status) {
diff --git a/app/src/main/java/io/github/dovecoteescapee/byedpi/SettingsActivity.kt b/app/src/main/java/io/github/dovecoteescapee/byedpi/SettingsActivity.kt
index df414ae..9bdf42e 100644
--- a/app/src/main/java/io/github/dovecoteescapee/byedpi/SettingsActivity.kt
+++ b/app/src/main/java/io/github/dovecoteescapee/byedpi/SettingsActivity.kt
@@ -1,9 +1,65 @@
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)
@@ -12,5 +68,48 @@ class SettingsActivity : AppCompatActivity() {
.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
index 2fcc5d8..02f9d72 100644
--- a/app/src/main/java/io/github/dovecoteescapee/byedpi/SettingsFragment.kt
+++ b/app/src/main/java/io/github/dovecoteescapee/byedpi/SettingsFragment.kt
@@ -1,10 +1,61 @@
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/res/drawable/baseline_settings_24.xml b/app/src/main/res/drawable/baseline_settings_24.xml
new file mode 100644
index 0000000..b240b83
--- /dev/null
+++ b/app/src/main/res/drawable/baseline_settings_24.xml
@@ -0,0 +1,5 @@
+
+
+
diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml
index 45544b4..5d31311 100644
--- a/app/src/main/res/layout/activity_main.xml
+++ b/app/src/main/res/layout/activity_main.xml
@@ -23,16 +23,4 @@
app:layout_constraintTop_toTopOf="parent"
style="@style/Widget.MaterialComponents.Button.OutlinedButton" />
-
-
diff --git a/app/src/main/res/menu/menu_main.xml b/app/src/main/res/menu/menu_main.xml
new file mode 100644
index 0000000..cf98ff6
--- /dev/null
+++ b/app/src/main/res/menu/menu_main.xml
@@ -0,0 +1,14 @@
+
+
diff --git a/app/src/main/res/menu/menu_settings.xml b/app/src/main/res/menu/menu_settings.xml
new file mode 100644
index 0000000..7b83655
--- /dev/null
+++ b/app/src/main/res/menu/menu_settings.xml
@@ -0,0 +1,19 @@
+
+
diff --git a/app/src/main/res/values-night/themes.xml b/app/src/main/res/values-night/themes.xml
new file mode 100644
index 0000000..0e99879
--- /dev/null
+++ b/app/src/main/res/values-night/themes.xml
@@ -0,0 +1,16 @@
+
+
+
+
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 397e633..a4a522a 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -5,4 +5,8 @@
Settings
VPN permission denied
Please stop the VPN service before changing settings
+ Settings
+ Save logs
+ Reset
+ Failed to collect logs
\ 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 e3796a2..bf9d93b 100644
--- a/app/src/main/res/values/themes.xml
+++ b/app/src/main/res/values/themes.xml
@@ -1,6 +1,6 @@
-
+
-
-
\ No newline at end of file
+
diff --git a/app/src/main/res/xml/settings.xml b/app/src/main/res/xml/settings.xml
index 3a48600..c318e6b 100644
--- a/app/src/main/res/xml/settings.xml
+++ b/app/src/main/res/xml/settings.xml
@@ -1,6 +1,13 @@
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ android:tag="settings_screen">
+
+
@@ -82,10 +89,15 @@
android:title="Host remove spaces"
android:defaultValue="false"/>
+
+