Release 0.1.1-alpha:

- Fix bug with UDP
- Fix some crashes
- Add DNS setting
- Add button reset settings
- Add log saving
- Update UI
This commit is contained in:
dovecoteescapee 2024-02-23 18:49:36 +03:00
parent cf8031115e
commit 54cc788ccd
23 changed files with 365 additions and 133 deletions

1
.gitignore vendored
View File

@ -2,6 +2,7 @@
.gradle .gradle
/local.properties /local.properties
/.idea/caches /.idea/caches
/.idea/deploymentTargetDropDown.xml
/.idea/libraries /.idea/libraries
/.idea/modules.xml /.idea/modules.xml
/.idea/workspace.xml /.idea/workspace.xml

View File

@ -1,10 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="deploymentTargetDropDown">
<value>
<entry key="app">
<State />
</entry>
</value>
</component>
</project>

View File

@ -1,6 +1,6 @@
# ByeDPI for Android # 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 ## Features

2
app/.gitignore vendored
View File

@ -1,3 +1,5 @@
/build /build
/debug
/release
*.aar *.aar
*.jar *.jar

View File

@ -13,8 +13,8 @@ android {
applicationId = "io.github.dovecoteescapee.byedpi" applicationId = "io.github.dovecoteescapee.byedpi"
minSdk = 24 minSdk = 24
targetSdk = 34 targetSdk = 34
versionCode = 1 versionCode = 2
versionName = "0.1.0-alpha" versionName = "0.1.1-alpha"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
} }

@ -1 +1 @@
Subproject commit 792ee44a8efa7ec4daadd2cbfda762329d1cbff2 Subproject commit da7fa48a3784a5dca6714d4734a3eb3b181a96aa

Binary file not shown.

View File

@ -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"
}

View File

@ -29,7 +29,8 @@
<activity <activity
android:name=".SettingsActivity" android:name=".SettingsActivity"
android:exported="true" /> android:label="@string/title_settings"
android:exported="false"/>
<service android:name=".ByeDpiVpnService" <service android:name=".ByeDpiVpnService"
android:permission="android.permission.BIND_VPN_SERVICE" android:permission="android.permission.BIND_VPN_SERVICE"

View File

@ -3,9 +3,12 @@
#include <params.h> #include <params.h>
#include <packets.h> #include <packets.h>
#include <jni.h> #include <jni.h>
#include <android/log.h>
#include <unistd.h> #include <unistd.h>
#include <pthread.h>
#include <string.h>
extern int big_loop(int fd); extern int NOT_EXIT;
struct packet fake_tls = { struct packet fake_tls = {
sizeof(tls_data), tls_data sizeof(tls_data), tls_data
@ -33,7 +36,7 @@ struct params params = {
.max_open = 512, .max_open = 512,
.bfsize = 16384, .bfsize = 16384,
.baddr = { .baddr = {
.sin6_family = AF_INET6 .sin6_family = AF_INET
}, },
.debug = 2 .debug = 2
}; };
@ -55,8 +58,16 @@ int get_default_ttl()
return orig_ttl; return orig_ttl;
} }
JNIEXPORT jint JNICALL void *run(void *srv) {
Java_io_github_dovecoteescapee_byedpi_ByeDpiVpnService_startProxy__IIIIZZIIZIZZZIZ( 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, JNIEnv *env,
jobject thiz, jobject thiz,
jint port, jint port,
@ -72,7 +83,8 @@ Java_io_github_dovecoteescapee_byedpi_ByeDpiVpnService_startProxy__IIIIZZIIZIZZZ
jboolean host_mixed_case, jboolean host_mixed_case,
jboolean domain_mixed_case, jboolean domain_mixed_case,
jboolean host_remove_space, jboolean host_remove_space,
jint tls_record_split, jboolean tls_record_split,
jint tls_record_split_position,
jboolean tls_record_split_at_sni) { jboolean tls_record_split_at_sni) {
enum demode desync_methods[] = {DESYNC_NONE, DESYNC_SPLIT, DESYNC_DISORDER, DESYNC_FAKE}; 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 |= domain_mixed_case ? MH_DMIX : 0;
params.mod_http |= host_remove_space ? MH_SPACE : 0; params.mod_http |= host_remove_space ? MH_SPACE : 0;
params.tlsrec = tls_record_split; params.tlsrec = tls_record_split;
params.tlsrec_pos = tls_record_split_position;
params.tlsrec_sni = tls_record_split_at_sni; params.tlsrec_sni = tls_record_split_at_sni;
if (!params.def_ttl && params.attack != DESYNC_NONE) { if (!params.def_ttl && params.attack != DESYNC_NONE) {
@ -97,12 +110,23 @@ Java_io_github_dovecoteescapee_byedpi_ByeDpiVpnService_startProxy__IIIIZZIIZIZZZ
} }
} }
struct sockaddr_ina srv = { struct sockaddr_ina *srv = malloc(sizeof(struct sockaddr_ina));
.in = { srv->in.sin_family = AF_INET;
.sin_family = AF_INET, srv->in.sin_addr.s_addr = inet_addr("0.0.0.0");
.sin_port = htons(port), srv->in.sin_port = htons(port);
}
};
return listener(srv); 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;
}

View File

@ -14,7 +14,8 @@ class ByeDpiProxyPreferences(
hostMixedCase: Boolean? = null, hostMixedCase: Boolean? = null,
domainMixedCase: Boolean? = null, domainMixedCase: Boolean? = null,
hostRemoveSpaces: Boolean? = null, hostRemoveSpaces: Boolean? = null,
tlsRecordSplit: Int? = null, tlsRecordSplit: Boolean? = null,
tlsRecordSplitPosition: Int? = null,
tlsRecordSplitAtSni: Boolean? = null, tlsRecordSplitAtSni: Boolean? = null,
) { ) {
val port: Int = port ?: 1080 val port: Int = port ?: 1080
@ -23,14 +24,15 @@ class ByeDpiProxyPreferences(
val defaultTtl: Int = defaultTtl ?: 0 val defaultTtl: Int = defaultTtl ?: 0
val noDomain: Boolean = noDomain ?: false val noDomain: Boolean = noDomain ?: false
val desyncKnown: Boolean = desyncKnown ?: false val desyncKnown: Boolean = desyncKnown ?: false
val desyncMethod: DesyncMethod = desyncMethod ?: DesyncMethod.None val desyncMethod: DesyncMethod = desyncMethod ?: DesyncMethod.Disorder
val splitPosition: Int = splitPosition ?: 3 val splitPosition: Int = splitPosition ?: 3
val splitAtHost: Boolean = splitAtHost ?: false val splitAtHost: Boolean = splitAtHost ?: false
val fakeTtl: Int = fakeTtl ?: 8 val fakeTtl: Int = fakeTtl ?: 8
val hostMixedCase: Boolean = hostMixedCase ?: false val hostMixedCase: Boolean = hostMixedCase ?: false
val domainMixedCase: Boolean = domainMixedCase ?: false val domainMixedCase: Boolean = domainMixedCase ?: false
val hostRemoveSpaces: Boolean = hostRemoveSpaces ?: 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 val tlsRecordSplitAtSni: Boolean = tlsRecordSplitAtSni ?: false
enum class DesyncMethod { enum class DesyncMethod {

View File

@ -5,28 +5,26 @@ import android.content.Intent
import android.net.VpnService import android.net.VpnService
import android.os.Build import android.os.Build
import android.os.IBinder import android.os.IBinder
import android.os.ParcelFileDescriptor
import android.util.Log import android.util.Log
import androidx.lifecycle.Lifecycle import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.ServiceLifecycleDispatcher import androidx.lifecycle.ServiceLifecycleDispatcher
import androidx.lifecycle.lifecycleScope
import androidx.preference.PreferenceManager import androidx.preference.PreferenceManager
import engine.Engine import engine.Engine
import engine.Key import engine.Key
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
class ByeDpiVpnService : VpnService(), LifecycleOwner { class ByeDpiVpnService : VpnService(), LifecycleOwner {
private val TAG: String = this::class.java.simpleName private var proxyThread: Long = -1
private var proxyJob: Job? = null private var vpn: ParcelFileDescriptor? = null
private val dispatcher = ServiceLifecycleDispatcher(this) private val dispatcher = ServiceLifecycleDispatcher(this)
override val lifecycle: Lifecycle override val lifecycle: Lifecycle
get() = dispatcher.lifecycle get() = dispatcher.lifecycle
companion object { companion object {
private val TAG: String = ByeDpiVpnService::class.java.simpleName
var status: Status = Status.STOPPED var status: Status = Status.STOPPED
private set private set
} }
@ -48,6 +46,7 @@ class ByeDpiVpnService : VpnService(), LifecycleOwner {
run() run()
START_STICKY START_STICKY
} }
"stop" -> { "stop" -> {
stop() stop()
START_NOT_STICKY START_NOT_STICKY
@ -75,26 +74,30 @@ class ByeDpiVpnService : VpnService(), LifecycleOwner {
} }
private fun run() { private fun run() {
val preferences = getPreferences(); val preferences = getPreferences()
status = Status.RUNNING status = Status.RUNNING
if (proxyJob != null) { if (proxyThread >= 0) {
Log.w(TAG, "Proxy already running") Log.w(TAG, "Proxy already running")
return return
} }
proxyJob = lifecycleScope.launch(Dispatchers.IO) { proxyThread = startProxy(preferences)
runProxy(preferences) if (proxyThread < 0) {
status = Status.STOPPED
Log.e(TAG, "Proxy failed to start")
return
} }
val vpn = getBuilder().establish() val vpn = getBuilder().establish()
this.vpn = vpn
if (vpn == null) { if (vpn == null) {
Log.e(TAG, "VPN connection failed") Log.e(TAG, "VPN connection failed")
return return
} }
startTun2Socks(vpn.detachFd(), preferences.port) Log.d(TAG, "fd: ${vpn.fd}")
startTun2Socks(vpn.fd, preferences.port)
} }
private fun getPreferences(): ByeDpiProxyPreferences { private fun getPreferences(): ByeDpiProxyPreferences {
@ -102,36 +105,38 @@ class ByeDpiVpnService : VpnService(), LifecycleOwner {
return ByeDpiProxyPreferences( return ByeDpiProxyPreferences(
port = port =
sharedPreferences.getString("byedpi_proxy_port", null)?.toInt(), sharedPreferences.getString("byedpi_proxy_port", null)?.toInt(),
maxConnections = maxConnections =
sharedPreferences.getString("byedpi_max_connections", null)?.toInt(), sharedPreferences.getString("byedpi_max_connections", null)?.toInt(),
bufferSize = bufferSize =
sharedPreferences.getString("byedpi_buffer_size", null)?.toInt(), sharedPreferences.getString("byedpi_buffer_size", null)?.toInt(),
defaultTtl = defaultTtl =
sharedPreferences.getString("byedpi_default_ttl", null)?.toInt(), sharedPreferences.getString("byedpi_default_ttl", null)?.toInt(),
noDomain = noDomain =
sharedPreferences.getBoolean("byedpi_no_domain", false), sharedPreferences.getBoolean("byedpi_no_domain", false),
desyncKnown = desyncKnown =
sharedPreferences.getBoolean("byedpi_desync_known", false), sharedPreferences.getBoolean("byedpi_desync_known", false),
desyncMethod = desyncMethod =
sharedPreferences.getString("byedpi_desync_method", null) sharedPreferences.getString("byedpi_desync_method", null)
?.let { ByeDpiProxyPreferences.DesyncMethod.fromName(it) }, ?.let { ByeDpiProxyPreferences.DesyncMethod.fromName(it) },
splitPosition = splitPosition =
sharedPreferences.getString("byedpi_split_position", null)?.toInt(), sharedPreferences.getString("byedpi_split_position", null)?.toInt(),
splitAtHost = splitAtHost =
sharedPreferences.getBoolean("byedpi_split_at_host", false), sharedPreferences.getBoolean("byedpi_split_at_host", false),
fakeTtl = fakeTtl =
sharedPreferences.getString("byedpi_fake_ttl", null)?.toInt(), sharedPreferences.getString("byedpi_fake_ttl", null)?.toInt(),
hostMixedCase = hostMixedCase =
sharedPreferences.getBoolean("byedpi_host_mixed_case", false), sharedPreferences.getBoolean("byedpi_host_mixed_case", false),
domainMixedCase = domainMixedCase =
sharedPreferences.getBoolean("byedpi_domain_mixed_case", false), sharedPreferences.getBoolean("byedpi_domain_mixed_case", false),
hostRemoveSpaces = hostRemoveSpaces =
sharedPreferences.getBoolean("byedpi_host_remove_spaces", false), sharedPreferences.getBoolean("byedpi_host_remove_spaces", false),
tlsRecordSplit = tlsRecordSplit =
sharedPreferences.getString("byedpi_tlsrec", null)?.toInt(), sharedPreferences.getBoolean("byedpi_tlsrec", false),
tlsRecordSplitPosition =
sharedPreferences.getString("byedpi_tlsrec_position", null)?.toInt(),
tlsRecordSplitAtSni = tlsRecordSplitAtSni =
sharedPreferences.getBoolean("byedpi_tlsrec_at_sni", false), sharedPreferences.getBoolean("byedpi_tlsrec_at_sni", false),
) )
} }
@ -141,9 +146,9 @@ class ByeDpiVpnService : VpnService(), LifecycleOwner {
stopProxy() stopProxy()
} }
private fun runProxy(preferences: ByeDpiProxyPreferences) : Int { private fun startProxy(preferences: ByeDpiProxyPreferences): Long {
Log.i(TAG, "Proxy started") Log.i(TAG, "Proxy started")
val res = startProxy( return jniStartProxy(
port = preferences.port, port = preferences.port,
maxConnections = preferences.maxConnections, maxConnections = preferences.maxConnections,
bufferSize = preferences.bufferSize, bufferSize = preferences.bufferSize,
@ -158,21 +163,20 @@ class ByeDpiVpnService : VpnService(), LifecycleOwner {
domainMixedCase = preferences.domainMixedCase, domainMixedCase = preferences.domainMixedCase,
hostRemoveSpace = preferences.hostRemoveSpaces, hostRemoveSpace = preferences.hostRemoveSpaces,
tlsRecordSplit = preferences.tlsRecordSplit, tlsRecordSplit = preferences.tlsRecordSplit,
tlsRecordSplitPosition = preferences.tlsRecordSplitPosition,
tlsRecordSplitAtSni = preferences.tlsRecordSplitAtSni, tlsRecordSplitAtSni = preferences.tlsRecordSplitAtSni,
) )
Log.i(TAG, "Proxy stopped")
return res
} }
private fun stopProxy() { private fun stopProxy() {
proxyJob?.let { if (proxyThread < 0) {
Log.i(TAG, "Proxy stopped") Log.w(TAG, "Proxy not running")
it.cancel() }
proxyJob = null jniStopProxy(proxyThread)
} ?: Log.w(TAG, "Proxy not running") proxyThread = -1
} }
private fun startTun2Socks(fd: Int, port:Int) { private fun startTun2Socks(fd: Int, port: Int) {
val key = Key().apply { val key = Key().apply {
mark = 0 mark = 0
mtu = 0 mtu = 0
@ -197,10 +201,10 @@ class ByeDpiVpnService : VpnService(), LifecycleOwner {
private fun stopTun2Socks() { private fun stopTun2Socks() {
Log.i(TAG, "Tun2socks stopped") 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, port: Int,
maxConnections: Int, maxConnections: Int,
bufferSize: Int, bufferSize: Int,
@ -214,9 +218,12 @@ class ByeDpiVpnService : VpnService(), LifecycleOwner {
hostMixedCase: Boolean, hostMixedCase: Boolean,
domainMixedCase: Boolean, domainMixedCase: Boolean,
hostRemoveSpace: Boolean, hostRemoveSpace: Boolean,
tlsRecordSplit: Int, tlsRecordSplit: Boolean,
tlsRecordSplitPosition: Int,
tlsRecordSplitAtSni: Boolean, tlsRecordSplitAtSni: Boolean,
): Int ): Long
private external fun jniStopProxy(proxyThread: Long)
private fun getBuilder(): Builder { private fun getBuilder(): Builder {
val builder = 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.addAddress("10.10.10.10", 32)
builder.addRoute("0.0.0.0", 0) builder.addRoute("0.0.0.0", 0)
builder.addRoute("0:0:0:0:0:0:0:0", 0) builder.addRoute("0:0:0:0:0:0:0:0", 0)
builder.addDnsServer("1.1.1.1") builder.addDnsServer(dns)
builder.addDnsServer("1.0.0.1")
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
builder.setMetered(false) builder.setMetered(false)
} }

View File

@ -4,21 +4,23 @@ import android.content.Intent
import android.net.VpnService import android.net.VpnService
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle import android.os.Bundle
import android.view.Menu
import android.view.MenuItem
import android.widget.Toast import android.widget.Toast
import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.result.contract.ActivityResultContracts
import androidx.preference.PreferenceManager
import io.github.dovecoteescapee.byedpi.databinding.ActivityMainBinding import io.github.dovecoteescapee.byedpi.databinding.ActivityMainBinding
class MainActivity : AppCompatActivity() { class MainActivity : AppCompatActivity() {
private lateinit var binding: ActivityMainBinding private lateinit var binding: ActivityMainBinding
private val register = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { private val register =
if (it.resultCode == RESULT_OK) { registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
startVpnService() if (it.resultCode == RESULT_OK) {
} else { startVpnService()
Toast.makeText(this, R.string.vpn_permission_denied, Toast.LENGTH_SHORT).show() } else {
Toast.makeText(this, R.string.vpn_permission_denied, Toast.LENGTH_SHORT).show()
}
} }
}
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) 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) 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() { private fun startVpnService() {
val intent = Intent(this, ByeDpiVpnService::class.java) val intent = Intent(this, ByeDpiVpnService::class.java)
intent.action = "start" intent.action = "start"
@ -67,6 +81,7 @@ class MainActivity : AppCompatActivity() {
val intent = Intent(this, ByeDpiVpnService::class.java) val intent = Intent(this, ByeDpiVpnService::class.java)
intent.action = "stop" intent.action = "stop"
startService(intent) startService(intent)
stopService(intent)
} }
private fun updateStatus(status : Status) { private fun updateStatus(status : Status) {

View File

@ -1,9 +1,65 @@
package io.github.dovecoteescapee.byedpi package io.github.dovecoteescapee.byedpi
import android.content.Intent
import android.os.Bundle 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.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() { 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?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
setContentView(R.layout.activity_settings) setContentView(R.layout.activity_settings)
@ -12,5 +68,48 @@ class SettingsActivity : AppCompatActivity() {
.beginTransaction() .beginTransaction()
.replace(R.id.settings, SettingsFragment()) .replace(R.id.settings, SettingsFragment())
.commit() .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)
} }
} }

View File

@ -1,10 +1,61 @@
package io.github.dovecoteescapee.byedpi package io.github.dovecoteescapee.byedpi
import android.net.InetAddresses
import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.util.Log
import android.util.Patterns
import androidx.preference.EditTextPreference
import androidx.preference.PreferenceFragmentCompat import androidx.preference.PreferenceFragmentCompat
class SettingsFragment : 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?) { override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
setPreferencesFromResource(R.xml.settings, rootKey) 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<EditTextPreference>(key)
?.setOnPreferenceChangeListener { preference, newValue ->
newValue as String
val valid = check(newValue)
if (!valid) {
Log.e(TAG, "Invalid ${preference.title}: $newValue")
}
valid
}
} }
} }

View File

@ -0,0 +1,5 @@
<vector android:height="24dp" android:tint="#FFFFFF"
android:viewportHeight="24" android:viewportWidth="24"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="@android:color/white" android:pathData="M19.14,12.94c0.04,-0.3 0.06,-0.61 0.06,-0.94c0,-0.32 -0.02,-0.64 -0.07,-0.94l2.03,-1.58c0.18,-0.14 0.23,-0.41 0.12,-0.61l-1.92,-3.32c-0.12,-0.22 -0.37,-0.29 -0.59,-0.22l-2.39,0.96c-0.5,-0.38 -1.03,-0.7 -1.62,-0.94L14.4,2.81c-0.04,-0.24 -0.24,-0.41 -0.48,-0.41h-3.84c-0.24,0 -0.43,0.17 -0.47,0.41L9.25,5.35C8.66,5.59 8.12,5.92 7.63,6.29L5.24,5.33c-0.22,-0.08 -0.47,0 -0.59,0.22L2.74,8.87C2.62,9.08 2.66,9.34 2.86,9.48l2.03,1.58C4.84,11.36 4.8,11.69 4.8,12s0.02,0.64 0.07,0.94l-2.03,1.58c-0.18,0.14 -0.23,0.41 -0.12,0.61l1.92,3.32c0.12,0.22 0.37,0.29 0.59,0.22l2.39,-0.96c0.5,0.38 1.03,0.7 1.62,0.94l0.36,2.54c0.05,0.24 0.24,0.41 0.48,0.41h3.84c0.24,0 0.44,-0.17 0.47,-0.41l0.36,-2.54c0.59,-0.24 1.13,-0.56 1.62,-0.94l2.39,0.96c0.22,0.08 0.47,0 0.59,-0.22l1.92,-3.32c0.12,-0.22 0.07,-0.47 -0.12,-0.61L19.14,12.94zM12,15.6c-1.98,0 -3.6,-1.62 -3.6,-3.6s1.62,-3.6 3.6,-3.6s3.6,1.62 3.6,3.6S13.98,15.6 12,15.6z"/>
</vector>

View File

@ -23,16 +23,4 @@
app:layout_constraintTop_toTopOf="parent" app:layout_constraintTop_toTopOf="parent"
style="@style/Widget.MaterialComponents.Button.OutlinedButton" /> style="@style/Widget.MaterialComponents.Button.OutlinedButton" />
<ImageButton
android:id="@+id/settings_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@drawable/outline_settings_24"
android:layout_marginTop="20dp"
android:layout_marginEnd="20dp"
android:background="@android:color/transparent"
android:contentDescription="@string/settings"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintEnd_toEndOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout> </androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -0,0 +1,14 @@
<?xml version="1.0" encoding="utf-8" ?>
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
tools:context=".MainActivity">
<item
android:id="@+id/action_settings"
android:orderInCategory="1"
android:title="@string/title_settings"
android:icon="@drawable/baseline_settings_24"
app:showAsAction="ifRoom" />
</menu>

View File

@ -0,0 +1,19 @@
<?xml version="1.0" encoding="utf-8" ?>
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
tools:context=".MainActivity">
<item
android:id="@+id/action_reset_settings"
android:orderInCategory="1"
android:title="@string/reset_settings"
app:showAsAction="never" />
<item
android:id="@+id/action_save_logs"
android:orderInCategory="2"
android:title="@string/save_logs"
app:showAsAction="never" />
</menu>

View File

@ -0,0 +1,16 @@
<resources>
<!-- Base application theme. -->
<style name="Theme.ByeDPI" parent="Theme.MaterialComponents.DayNight.DarkActionBar">
<!-- Primary brand color. -->
<item name="colorPrimary">@color/white</item>
<item name="colorPrimaryVariant">#E5E5E5</item>
<item name="colorOnPrimary">@color/white</item>
<!-- Secondary brand color. -->
<item name="colorSecondary">#38AF42</item>
<item name="colorSecondaryVariant">#1F8C28</item>
<item name="colorOnSecondary">@color/black</item>
<!-- Status bar color. -->
<item name="android:statusBarColor">?attr/colorPrimaryVariant</item>
<!-- Customize your theme here. -->
</style>
</resources>

View File

@ -5,4 +5,8 @@
<string name="settings">Settings</string> <string name="settings">Settings</string>
<string name="vpn_permission_denied">VPN permission denied</string> <string name="vpn_permission_denied">VPN permission denied</string>
<string name="settings_unavailable">Please stop the VPN service before changing settings</string> <string name="settings_unavailable">Please stop the VPN service before changing settings</string>
<string name="title_settings">Settings</string>
<string name="save_logs">Save logs</string>
<string name="reset_settings">Reset</string>
<string name="logs_failed">Failed to collect logs</string>
</resources> </resources>

View File

@ -1,6 +1,6 @@
<resources xmlns:tools="http://schemas.android.com/tools"> <resources>
<!-- Base application theme. --> <!-- Base application theme. -->
<style name="Theme.ByeDPI" parent="Theme.MaterialComponents.DayNight.NoActionBar"> <style name="Theme.ByeDPI" parent="Theme.MaterialComponents.DayNight.DarkActionBar">
<!-- Primary brand color. --> <!-- Primary brand color. -->
<item name="colorPrimary">#5976DF</item> <item name="colorPrimary">#5976DF</item>
<item name="colorPrimaryVariant">#3C52A3</item> <item name="colorPrimaryVariant">#3C52A3</item>
@ -13,4 +13,4 @@
<item name="android:statusBarColor">?attr/colorPrimaryVariant</item> <item name="android:statusBarColor">?attr/colorPrimaryVariant</item>
<!-- Customize your theme here. --> <!-- Customize your theme here. -->
</style> </style>
</resources> </resources>

View File

@ -1,6 +1,13 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android" <PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"> xmlns:app="http://schemas.android.com/apk/res-auto"
android:tag="settings_screen">
<com.takisoft.preferencex.EditTextPreference
android:key="dns_ip"
android:title="DNS"
android:defaultValue="9.9.9.9"
app:useSimpleSummaryProvider="true"/>
<com.takisoft.preferencex.EditTextPreference <com.takisoft.preferencex.EditTextPreference
android:key="byedpi_port" android:key="byedpi_port"
@ -45,13 +52,13 @@
android:title="Desync method" android:title="Desync method"
android:entries="@array/byedpi_desync_methods" android:entries="@array/byedpi_desync_methods"
android:entryValues="@array/byedpi_desync_methods_entries" android:entryValues="@array/byedpi_desync_methods_entries"
android:defaultValue="none" android:defaultValue="disorder"
app:useSimpleSummaryProvider="true" /> app:useSimpleSummaryProvider="true" />
<com.takisoft.preferencex.EditTextPreference <com.takisoft.preferencex.EditTextPreference
android:key="byedpi_split_position" android:key="byedpi_split_position"
android:title="Split position" android:title="Split position"
android:inputType="number" android:inputType="numberSigned"
android:defaultValue="3" android:defaultValue="3"
app:useSimpleSummaryProvider="true" /> app:useSimpleSummaryProvider="true" />
@ -82,10 +89,15 @@
android:title="Host remove spaces" android:title="Host remove spaces"
android:defaultValue="false"/> android:defaultValue="false"/>
<CheckBoxPreference
android:key="byedpi_tlsrec_enabled"
android:title="Split TLS record"
android:defaultValue="false"/>
<com.takisoft.preferencex.EditTextPreference <com.takisoft.preferencex.EditTextPreference
android:key="byedpi_tlsrec" android:key="byedpi_tlsrec_position"
android:title="TLS record split position" android:title="TLS record split position"
android:inputType="number" android:inputType="numberSigned"
android:defaultValue="0" android:defaultValue="0"
app:useSimpleSummaryProvider="true" /> app:useSimpleSummaryProvider="true" />