v1.0.0-rc1:

- Change min SDK to 21
- Refactor code
- Fix crushes
- Add proxy mode
- Add listening ip setting
- Update main screen
- Update settings screen
- Move logs saving to main screen
This commit is contained in:
dovecoteescapee 2024-02-27 00:00:53 +03:00
parent 54cc788ccd
commit 4669bd9c65
35 changed files with 1645 additions and 750 deletions

View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="RenderSettings">
<option name="showDecorations" value="true" />
</component>
</project>

View File

@ -11,19 +11,28 @@ android {
defaultConfig { defaultConfig {
applicationId = "io.github.dovecoteescapee.byedpi" applicationId = "io.github.dovecoteescapee.byedpi"
minSdk = 24 minSdk = 21
targetSdk = 34 targetSdk = 34
versionCode = 2 versionCode = 3
versionName = "0.1.1-alpha" versionName = "1.0.0-rc1"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
} }
buildFeatures {
buildConfig = true
}
buildTypes { buildTypes {
release { release {
buildConfigField("String", "VERSION_NAME", "\"${defaultConfig.versionName}\"")
isMinifyEnabled = false isMinifyEnabled = false
proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro") proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro")
} }
debug {
buildConfigField("String", "VERSION_NAME", "\"${defaultConfig.versionName}-debug\"")
}
} }
compileOptions { compileOptions {
sourceCompatibility = JavaVersion.VERSION_1_8 sourceCompatibility = JavaVersion.VERSION_1_8
@ -72,7 +81,7 @@ abstract class BuildTun2Socks : DefaultTask() {
} }
project.exec { project.exec {
workingDir = tun2socksDir workingDir = tun2socksDir
commandLine("gomobile", "bind", "-o", tun2socksOutput, "./engine") commandLine("gomobile", "bind", "-o", tun2socksOutput, "-trimpath", "./engine")
} }
} }
} }

View File

@ -3,9 +3,9 @@
xmlns:tools="http://schemas.android.com/tools"> xmlns:tools="http://schemas.android.com/tools">
<uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" /> <uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_SPECIAL_USE" /> <uses-permission android:name="android.permission.FOREGROUND_SERVICE_SPECIAL_USE" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<application <application
android:allowBackup="true" android:allowBackup="true"
@ -16,10 +16,11 @@
android:roundIcon="@mipmap/ic_launcher_round" android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true" android:supportsRtl="true"
android:theme="@style/Theme.ByeDPI" android:theme="@style/Theme.ByeDPI"
tools:targetApi="31"> tools:targetApi="34">
<activity <activity
android:name=".MainActivity" android:name=".activities.MainActivity"
android:exported="true"> android:exported="true"
android:launchMode="singleInstance">
<intent-filter> <intent-filter>
<action android:name="android.intent.action.MAIN" /> <action android:name="android.intent.action.MAIN" />
@ -28,16 +29,31 @@
</activity> </activity>
<activity <activity
android:name=".SettingsActivity" android:name=".activities.SettingsActivity"
android:label="@string/title_settings" android:label="@string/title_settings"
android:exported="false"/> android:exported="true"/>
<service android:name=".ByeDpiVpnService" <service android:name=".services.ByeDpiVpnService"
android:permission="android.permission.BIND_VPN_SERVICE" android:permission="android.permission.BIND_VPN_SERVICE"
android:exported="true"> android:foregroundServiceType="specialUse"
android:exported="false">
<intent-filter> <intent-filter>
<action android:name="android.net.VpnService"/> <action android:name="android.net.VpnService"/>
</intent-filter> </intent-filter>
<meta-data
android:name="android.net.VpnService.SUPPORTS_ALWAYS_ON"
android:value="false" />
<property
android:name="android.app.PROPERTY_SPECIAL_USE_FGS_SUBTYPE"
android:value="vpn" />
</service>
<service android:name=".services.ByeDpiProxyService"
android:foregroundServiceType="specialUse"
android:exported="false">
<property
android:name="android.app.PROPERTY_SPECIAL_USE_FGS_SUBTYPE"
android:value="proxy" />
</service> </service>
</application> </application>

View File

@ -30,6 +30,7 @@ add_library(${CMAKE_PROJECT_NAME} SHARED
byedpi/desync.c byedpi/desync.c
byedpi/packets.c byedpi/packets.c
byedpi/proxy.c byedpi/proxy.c
byedpi/main.c
native-lib.c native-lib.c
) )
@ -37,6 +38,8 @@ include_directories("byedpi")
set(CMAKE_C_FLAGS "-std=c99 -O2 -D_XOPEN_SOURCE=500") 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 # Specifies libraries CMake should link to your target library. You
# can link libraries from various origins, such as libraries defined in this # can link libraries from various origins, such as libraries defined in this
# build script, prebuilt third-party libraries, or Android system libraries. # build script, prebuilt third-party libraries, or Android system libraries.

@ -1 +1 @@
Subproject commit d880b0441f8c31e0609fb98be90d722190cd737b Subproject commit 12adfe285f603bfa38c19e9057d897b2b4e40941

View File

@ -2,74 +2,40 @@
#include <proxy.h> #include <proxy.h>
#include <params.h> #include <params.h>
#include <packets.h> #include <packets.h>
#include <unistd.h>
#include <string.h>
#include <netdb.h>
#include <sys/eventfd.h>
#include <jni.h> #include <jni.h>
#include <android/log.h> #include <android/log.h>
#include <unistd.h>
#include <pthread.h>
#include <string.h>
extern int NOT_EXIT; const enum demode DESYNC_METHODS[] = {
DESYNC_NONE,
struct packet fake_tls = { DESYNC_SPLIT,
sizeof(tls_data), tls_data DESYNC_DISORDER,
}, DESYNC_FAKE
fake_http = {
sizeof(http_data), http_data
}; };
struct params params = { extern struct packet fake_tls, fake_http;
.ttl = 8, extern int get_default_ttl();
.split = 3, extern int get_addr(const char *str, struct sockaddr_ina *addr);
.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,
.ipv6 = 1, JNIEXPORT jint JNICALL
.resolve = 1, Java_io_github_dovecoteescapee_byedpi_core_ByeDpiProxy_jniEventFd(JNIEnv *env, jobject thiz) {
.max_open = 512, int fd = eventfd(0, EFD_NONBLOCK);
.bfsize = 16384, if (fd < 0) {
.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");
return -1; return -1;
} }
if (getsockopt(fd, IPPROTO_IP, IP_TTL, return fd;
(char *)&orig_ttl, &tsize) < 0) {
uniperror("getsockopt IP_TTL");
}
close(fd);
return orig_ttl;
} }
void *run(void *srv) { JNIEXPORT jint JNICALL
LOG(LOG_S, "Start proxy thread"); Java_io_github_dovecoteescapee_byedpi_core_ByeDpiProxy_jniStartProxy(
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 event_fd,
jstring ip,
jint port, jint port,
jint max_connections, jint max_connections,
jint buffer_size, jint buffer_size,
@ -82,24 +48,35 @@ Java_io_github_dovecoteescapee_byedpi_ByeDpiVpnService_jniStartProxy(
jint fake_ttl, jint fake_ttl,
jboolean host_mixed_case, jboolean host_mixed_case,
jboolean domain_mixed_case, jboolean domain_mixed_case,
jboolean host_remove_space, jboolean host_remove_spaces,
jboolean tls_record_split, jboolean tls_record_split,
jint tls_record_split_position, 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};
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.max_open = max_connections;
params.bfsize = buffer_size; params.bfsize = buffer_size;
params.def_ttl = default_ttl; params.def_ttl = default_ttl;
params.resolve = !no_domain; params.resolve = !no_domain;
params.de_known = desync_known; params.de_known = desync_known;
params.attack = desync_methods[desync_method]; params.attack = DESYNC_METHODS[desync_method];
params.split = split_position; params.split = split_position;
params.split_host = split_at_host; params.split_host = split_at_host;
params.ttl = fake_ttl; params.ttl = fake_ttl;
params.mod_http |= host_mixed_case ? MH_HMIX : 0; params.mod_http =
params.mod_http |= domain_mixed_case ? MH_DMIX : 0; MH_HMIX * host_mixed_case |
params.mod_http |= host_remove_space ? MH_SPACE : 0; MH_DMIX * domain_mixed_case |
MH_SPACE * host_remove_spaces;
params.tlsrec = tls_record_split; params.tlsrec = tls_record_split;
params.tlsrec_pos = tls_record_split_position; params.tlsrec_pos = tls_record_split_position;
params.tlsrec_sni = tls_record_split_at_sni; 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)); int res = listener(event_fd, s);
srv->in.sin_family = AF_INET;
srv->in.sin_addr.s_addr = inet_addr("0.0.0.0");
srv->in.sin_port = htons(port);
NOT_EXIT = 1; if (close(event_fd) < 0) {
uniperror("close");
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; return res < 0 ? get_e() : 0;
} }
JNIEXPORT void JNICALL JNIEXPORT jint JNICALL
Java_io_github_dovecoteescapee_byedpi_ByeDpiVpnService_jniStopProxy(JNIEnv *env, jobject thiz, jlong proxy_thread) { Java_io_github_dovecoteescapee_byedpi_core_ByeDpiProxy_jniStopProxy(JNIEnv *env, jobject thiz,
NOT_EXIT = 0; jint event_fd) {
if (eventfd_write(event_fd, 1) < 0) {
uniperror("eventfd_write");
LOG(LOG_S, "event_fd: %d", event_fd);
}
return 0;
} }

View File

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

View File

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

View File

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

View File

@ -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<EditTextPreference>(key)
?.setOnPreferenceChangeListener { preference, newValue ->
newValue as String
val valid = check(newValue)
if (!valid) {
Log.e(TAG, "Invalid ${preference.title}: $newValue")
}
valid
}
}
}

View File

@ -1,6 +0,0 @@
package io.github.dovecoteescapee.byedpi
enum class Status {
RUNNING,
STOPPED
}

View File

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

View File

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

View File

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

View File

@ -1,6 +1,9 @@
package io.github.dovecoteescapee.byedpi package io.github.dovecoteescapee.byedpi.core
import android.content.SharedPreferences
class ByeDpiProxyPreferences( class ByeDpiProxyPreferences(
ip: String? = null,
port: Int? = null, port: Int? = null,
maxConnections: Int? = null, maxConnections: Int? = null,
bufferSize: Int? = null, bufferSize: Int? = null,
@ -18,6 +21,7 @@ class ByeDpiProxyPreferences(
tlsRecordSplitPosition: Int? = null, tlsRecordSplitPosition: Int? = null,
tlsRecordSplitAtSni: Boolean? = null, tlsRecordSplitAtSni: Boolean? = null,
) { ) {
val ip: String = ip ?: "127.0.0.1"
val port: Int = port ?: 1080 val port: Int = port ?: 1080
val maxConnections: Int = maxConnections ?: 512 val maxConnections: Int = maxConnections ?: 512
val bufferSize: Int = bufferSize ?: 16384 val bufferSize: Int = bufferSize ?: 16384
@ -35,6 +39,29 @@ class ByeDpiProxyPreferences(
val tlsRecordSplitPosition: Int = tlsRecordSplitPosition ?: 0 val tlsRecordSplitPosition: Int = tlsRecordSplitPosition ?: 0
val tlsRecordSplitAtSni: Boolean = tlsRecordSplitAtSni ?: false 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 { enum class DesyncMethod {
None, None,
Split, Split,

View File

@ -0,0 +1,4 @@
package io.github.dovecoteescapee.byedpi.data
const val START_ACTION = "start"
const val STOP_ACTION = "stop"

View File

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

View File

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

View File

@ -0,0 +1,7 @@
package io.github.dovecoteescapee.byedpi.data
enum class ServiceStatus {
DISCONNECTED,
CONNECTED,
FAILED,
}

View File

@ -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<DropDownPreference>("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<Preference>("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<EditTextPreference>(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
}
}
}
}
}

View File

@ -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,
)
}

View File

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

View File

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

View File

@ -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()

View File

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

View File

@ -0,0 +1,8 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:height="36dp"
android:viewportHeight="24"
android:viewportWidth="24"
android:width="36dp"
android:tint="?attr/colorOnBackground">
<path android:fillColor="#FF000000" android:pathData="M10.9,2.1c-4.6,0.5 -8.3,4.2 -8.8,8.7c-0.5,4.7 2.2,8.9 6.3,10.5C8.7,21.4 9,21.2 9,20.8v-1.6c0,0 -0.4,0.1 -0.9,0.1c-1.4,0 -2,-1.2 -2.1,-1.9c-0.1,-0.4 -0.3,-0.7 -0.6,-1C5.1,16.3 5,16.3 5,16.2C5,16 5.3,16 5.4,16c0.6,0 1.1,0.7 1.3,1c0.5,0.8 1.1,1 1.4,1c0.4,0 0.7,-0.1 0.9,-0.2c0.1,-0.7 0.4,-1.4 1,-1.8c-2.3,-0.5 -4,-1.8 -4,-4c0,-1.1 0.5,-2.2 1.2,-3C7.1,8.8 7,8.3 7,7.6C7,7.2 7,6.6 7.3,6c0,0 1.4,0 2.8,1.3C10.6,7.1 11.3,7 12,7s1.4,0.1 2,0.3C15.3,6 16.8,6 16.8,6C17,6.6 17,7.2 17,7.6c0,0.8 -0.1,1.2 -0.2,1.4c0.7,0.8 1.2,1.8 1.2,3c0,2.2 -1.7,3.5 -4,4c0.6,0.5 1,1.4 1,2.3v2.6c0,0.3 0.3,0.6 0.7,0.5c3.7,-1.5 6.3,-5.1 6.3,-9.3C22,6.1 16.9,1.4 10.9,2.1z"/>
</vector>

View File

@ -1,5 +0,0 @@
<vector android:height="48dp" android:tint="#5976DF"
android:viewportHeight="24" android:viewportWidth="24"
android:width="48dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="@android:color/white" android:pathData="M19.43,12.98c0.04,-0.32 0.07,-0.64 0.07,-0.98 0,-0.34 -0.03,-0.66 -0.07,-0.98l2.11,-1.65c0.19,-0.15 0.24,-0.42 0.12,-0.64l-2,-3.46c-0.09,-0.16 -0.26,-0.25 -0.44,-0.25 -0.06,0 -0.12,0.01 -0.17,0.03l-2.49,1c-0.52,-0.4 -1.08,-0.73 -1.69,-0.98l-0.38,-2.65C14.46,2.18 14.25,2 14,2h-4c-0.25,0 -0.46,0.18 -0.49,0.42l-0.38,2.65c-0.61,0.25 -1.17,0.59 -1.69,0.98l-2.49,-1c-0.06,-0.02 -0.12,-0.03 -0.18,-0.03 -0.17,0 -0.34,0.09 -0.43,0.25l-2,3.46c-0.13,0.22 -0.07,0.49 0.12,0.64l2.11,1.65c-0.04,0.32 -0.07,0.65 -0.07,0.98 0,0.33 0.03,0.66 0.07,0.98l-2.11,1.65c-0.19,0.15 -0.24,0.42 -0.12,0.64l2,3.46c0.09,0.16 0.26,0.25 0.44,0.25 0.06,0 0.12,-0.01 0.17,-0.03l2.49,-1c0.52,0.4 1.08,0.73 1.69,0.98l0.38,2.65c0.03,0.24 0.24,0.42 0.49,0.42h4c0.25,0 0.46,-0.18 0.49,-0.42l0.38,-2.65c0.61,-0.25 1.17,-0.59 1.69,-0.98l2.49,1c0.06,0.02 0.12,0.03 0.18,0.03 0.17,0 0.34,-0.09 0.43,-0.25l2,-3.46c0.12,-0.22 0.07,-0.49 -0.12,-0.64l-2.11,-1.65zM17.45,11.27c0.04,0.31 0.05,0.52 0.05,0.73 0,0.21 -0.02,0.43 -0.05,0.73l-0.14,1.13 0.89,0.7 1.08,0.84 -0.7,1.21 -1.27,-0.51 -1.04,-0.42 -0.9,0.68c-0.43,0.32 -0.84,0.56 -1.25,0.73l-1.06,0.43 -0.16,1.13 -0.2,1.35h-1.4l-0.19,-1.35 -0.16,-1.13 -1.06,-0.43c-0.43,-0.18 -0.83,-0.41 -1.23,-0.71l-0.91,-0.7 -1.06,0.43 -1.27,0.51 -0.7,-1.21 1.08,-0.84 0.89,-0.7 -0.14,-1.13c-0.03,-0.31 -0.05,-0.54 -0.05,-0.74s0.02,-0.43 0.05,-0.73l0.14,-1.13 -0.89,-0.7 -1.08,-0.84 0.7,-1.21 1.27,0.51 1.04,0.42 0.9,-0.68c0.43,-0.32 0.84,-0.56 1.25,-0.73l1.06,-0.43 0.16,-1.13 0.2,-1.35h1.39l0.19,1.35 0.16,1.13 1.06,0.43c0.43,0.18 0.83,0.41 1.23,0.71l0.91,0.7 1.06,-0.43 1.27,-0.51 0.7,1.21 -1.07,0.85 -0.89,0.7 0.14,1.13zM12,8c-2.21,0 -4,1.79 -4,4s1.79,4 4,4 4,-1.79 4,-4 -1.79,-4 -4,-4zM12,14c-1.1,0 -2,-0.9 -2,-2s0.9,-2 2,-2 2,0.9 2,2 -0.9,2 -2,2z"/>
</vector>

View File

@ -4,23 +4,49 @@
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
tools:context=".MainActivity"> tools:context=".activities.MainActivity">
<com.google.android.material.button.MaterialButton <com.google.android.material.button.MaterialButton
android:id="@+id/status_button" android:id="@+id/status_button"
style="@style/Widget.MaterialComponents.Button.OutlinedButton"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:text="@string/start"
android:textSize="24sp"
android:textAllCaps="false"
android:paddingLeft="10pt" android:paddingLeft="10pt"
android:paddingRight="10pt"
android:paddingTop="5pt" android:paddingTop="5pt"
android:paddingRight="10pt"
android:paddingBottom="5pt" android:paddingBottom="5pt"
app:layout_constraintBottom_toBottomOf="parent" android:textAllCaps="false"
android:textSize="24sp"
app:layout_constraintBottom_toTopOf="@+id/status_text"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" app:layout_constraintTop_toTopOf="parent"
style="@style/Widget.MaterialComponents.Button.OutlinedButton" /> app:layout_constraintVertical_chainStyle="packed"
tools:text="@string/vpn_connect" />
<com.google.android.material.textview.MaterialTextView
android:id="@+id/status_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintBottom_toTopOf="@+id/proxy_address"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/status_button"
app:layout_constraintVertical_chainStyle="packed"
tools:text="@string/vpn_disconnected" />
<com.google.android.material.textview.MaterialTextView
android:id="@+id/proxy_address"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/status_text"
app:layout_constraintVertical_chainStyle="packed"
tools:text="127.0.0.1:1080" />
</androidx.constraintlayout.widget.ConstraintLayout> </androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -2,7 +2,7 @@
<menu xmlns:android="http://schemas.android.com/apk/res/android" <menu 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"
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
tools:context=".MainActivity"> tools:context=".activities.MainActivity">
<item <item
android:id="@+id/action_settings" android:id="@+id/action_settings"
@ -11,4 +11,10 @@
android:icon="@drawable/baseline_settings_24" android:icon="@drawable/baseline_settings_24"
app:showAsAction="ifRoom" /> app:showAsAction="ifRoom" />
<item
android:id="@+id/action_save_logs"
android:orderInCategory="2"
android:title="@string/save_logs"
app:showAsAction="never" />
</menu> </menu>

View File

@ -2,7 +2,7 @@
<menu xmlns:android="http://schemas.android.com/apk/res/android" <menu 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"
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
tools:context=".MainActivity"> tools:context=".activities.MainActivity">
<item <item
android:id="@+id/action_reset_settings" android:id="@+id/action_reset_settings"
@ -10,10 +10,4 @@
android:title="@string/reset_settings" android:title="@string/reset_settings"
app:showAsAction="never" /> app:showAsAction="never" />
<item
android:id="@+id/action_save_logs"
android:orderInCategory="2"
android:title="@string/save_logs"
app:showAsAction="never" />
</menu> </menu>

View File

@ -4,13 +4,10 @@
<!-- Primary brand color. --> <!-- Primary brand color. -->
<item name="colorPrimary">@color/white</item> <item name="colorPrimary">@color/white</item>
<item name="colorPrimaryVariant">#E5E5E5</item> <item name="colorPrimaryVariant">#E5E5E5</item>
<item name="colorOnPrimary">@color/white</item> <item name="colorOnPrimary">@color/black</item>
<!-- Secondary brand color. --> <!-- Secondary brand color. -->
<item name="colorSecondary">#38AF42</item> <item name="colorSecondary">#5976DF</item>
<item name="colorSecondaryVariant">#1F8C28</item> <item name="colorSecondaryVariant">#3C52A3</item>
<item name="colorOnSecondary">@color/black</item> <item name="colorOnSecondary">@color/white</item>
<!-- Status bar color. -->
<item name="android:statusBarColor">?attr/colorPrimaryVariant</item>
<!-- Customize your theme here. -->
</style> </style>
</resources> </resources>

View File

@ -1,5 +1,25 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<resources> <resources>
<array name="themes">
<item name="system">System</item>
<item name="light">Light</item>
<item name="dark">Dark</item>
</array>
<array name="themes_entries">
<item name="system">system</item>
<item name="light">light</item>
<item name="dark">dark</item>
</array>
<array name="byedpi_modes">
<item name="vpn">VPN</item>
<item name="proxy">Proxy</item>
</array>
<array name="byedpi_modes_entries">
<item name="vpn">vpn</item>
<item name="proxy">proxy</item>
</array>
<array name="byedpi_desync_methods"> <array name="byedpi_desync_methods">
<item name="none">None</item> <item name="none">None</item>
<item name="split">Split</item> <item name="split">Split</item>

View File

@ -1,7 +1,17 @@
<resources> <resources>
<string name="app_name">ByeDPI</string> <string name="app_name">ByeDPI</string>
<string name="start">Start</string> <string name="vpn_connect">Connect</string>
<string name="stop">Stop</string> <string name="vpn_disconnect">Disconnect</string>
<string name="vpn_connected">Connected</string>
<string name="vpn_disconnected">Disconnected</string>
<string name="proxy_up">Proxy is up</string>
<string name="proxy_down">Proxy is down</string>
<string name="proxy_start">Start</string>
<string name="proxy_stop">Stop</string>
<string name="vpn_connecting">Connecting…</string>
<string name="vpn_disconnecting">Disconnecting…</string>
<string name="proxy_starting">Starting</string>
<string name="proxy_stopping">Stopping</string>
<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>
@ -9,4 +19,34 @@
<string name="save_logs">Save logs</string> <string name="save_logs">Save logs</string>
<string name="reset_settings">Reset</string> <string name="reset_settings">Reset</string>
<string name="logs_failed">Failed to collect logs</string> <string name="logs_failed">Failed to collect logs</string>
<string name="proxy_address">%1$s:%2$s</string>
<string name="theme_settings">Theme</string>
<string name="mode_setting">Mode</string>
<string name="dbs_ip_setting">DNS</string>
<string name="bye_dpi_proxy_ip_setting">Listen address</string>
<string name="byedpi_proxy_port_setting">Port</string>
<string name="byedpi_max_connections_setting">Maximum number of connections</string>
<string name="byedpi_buffer_size_setting">Buffer size</string>
<string name="byedpi_default_ttl_setting">Default TTL</string>
<string name="byedpi_no_domain_setting">No domain</string>
<string name="byedpi_desync_known_setting">Desync only HTTPS and TLS</string>
<string name="byedpi_desync_method_setting">Desync method</string>
<string name="byedpi_split_position_setting">Split position</string>
<string name="byedpi_split_at_host_setting">Split at host</string>
<string name="byedpi_fake_ttl_setting">TTL of fake packets</string>
<string name="byedpi_host_mixed_case_setting">Host mixed case</string>
<string name="byedpi_domain_mixed_case_setting">Domain mixed case</string>
<string name="byedpi_host_remove_spaces_setting">Host remove spaces</string>
<string name="byedpi_tlsrec_enabled_setting">Split TLS record</string>
<string name="byedpi_tlsrec_position_setting">TLS record split position</string>
<string name="byedpi_tlsrec_at_sni_setting">Split TLS record at SNI</string>
<string name="version">Version</string>
<string name="source_code_link">Source code</string>
<string name="byedpi_readme_link">What does all of this mean?</string>
<string name="failed_to_start">Failed to start %1$s</string>
<string name="vpn_channel_name">VPN</string>
<string name="proxy_channel_name">Proxy</string>
<string name="notification_title">ByeDPI</string>
<string name="vpn_notification_content">VPN is running</string>
<string name="proxy_notification_content">Proxy is running</string>
</resources> </resources>

View File

@ -6,9 +6,9 @@
<item name="colorPrimaryVariant">#3C52A3</item> <item name="colorPrimaryVariant">#3C52A3</item>
<item name="colorOnPrimary">@color/white</item> <item name="colorOnPrimary">@color/white</item>
<!-- Secondary brand color. --> <!-- Secondary brand color. -->
<item name="colorSecondary">#38AF42</item> <item name="colorSecondary">#5976DF</item>
<item name="colorSecondaryVariant">#1F8C28</item> <item name="colorSecondaryVariant">#3C52A3</item>
<item name="colorOnSecondary">@color/black</item> <item name="colorOnSecondary">@color/white</item>
<!-- Status bar color. --> <!-- Status bar color. -->
<item name="android:statusBarColor">?attr/colorPrimaryVariant</item> <item name="android:statusBarColor">?attr/colorPrimaryVariant</item>
<!-- Customize your theme here. --> <!-- Customize your theme here. -->

View File

@ -1,109 +1,172 @@
<?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"
xmlns:tools="http://schemas.android.com/tools"
android:tag="settings_screen"> android:tag="settings_screen">
<com.takisoft.preferencex.EditTextPreference <PreferenceCategory
android:key="dns_ip" android:title="General">
android:title="DNS"
android:defaultValue="9.9.9.9"
app:useSimpleSummaryProvider="true"/>
<com.takisoft.preferencex.EditTextPreference <DropDownPreference
android:key="byedpi_port" android:key="app_theme"
android:title="Port" android:title="@string/theme_settings"
android:inputType="number" android:entries="@array/themes"
android:defaultValue="1080" android:entryValues="@array/themes_entries"
app:useSimpleSummaryProvider="true"/> android:defaultValue="system"
app:useSimpleSummaryProvider="true" />
<com.takisoft.preferencex.EditTextPreference <DropDownPreference
android:key="byedpi_max_connections" android:key="byedpi_mode"
android:title="Maximum number of connections" android:title="@string/mode_setting"
android:inputType="number" android:entries="@array/byedpi_modes"
android:defaultValue="512" android:entryValues="@array/byedpi_modes_entries"
app:useSimpleSummaryProvider="true"/> android:defaultValue="vpn"
app:useSimpleSummaryProvider="true" />
<com.takisoft.preferencex.EditTextPreference </PreferenceCategory>
android:key="byedpi_buffer_size"
android:title="Buffer size"
android:inputType="number"
android:defaultValue="16384"
app:useSimpleSummaryProvider="true" />
<com.takisoft.preferencex.EditTextPreference <PreferenceCategory
android:key="byedpi_default_ttl" android:title="ByeDPI">
android:title="Default TTL"
android:inputType="number"
android:defaultValue="0"
app:useSimpleSummaryProvider="true" />
<CheckBoxPreference <Preference
android:key="byedpi_no_domain" android:key="byedpi_readme"
android:title="No domain" android:title="@string/byedpi_readme_link"
android:defaultValue="false"/> android:textColor="?attr/textFillColor"
android:icon="@drawable/ic_github_36">
<intent
android:action="android.intent.action.VIEW"
android:data="https://github.com/hufrea/byedpi/tree/main#readme-ov-file" />
</Preference>
<CheckBoxPreference <com.takisoft.preferencex.EditTextPreference
android:key="byedpi_desync_knows" android:key="dns_ip"
android:title="Desync only HTTPS and TLS" android:title="@string/dbs_ip_setting"
android:defaultValue="false"/> android:defaultValue="9.9.9.9"
app:useSimpleSummaryProvider="true"/>
<DropDownPreference <com.takisoft.preferencex.EditTextPreference
android:key="byedpi_desync_method" android:key="byedpi_proxy_ip"
android:title="Desync method" android:title="@string/bye_dpi_proxy_ip_setting"
android:entries="@array/byedpi_desync_methods" android:defaultValue="127.0.0.1"
android:entryValues="@array/byedpi_desync_methods_entries" app:useSimpleSummaryProvider="true" />
android:defaultValue="disorder"
app:useSimpleSummaryProvider="true" />
<com.takisoft.preferencex.EditTextPreference <com.takisoft.preferencex.EditTextPreference
android:key="byedpi_split_position" android:key="byedpi_proxy_port"
android:title="Split position" android:title="@string/byedpi_proxy_port_setting"
android:inputType="numberSigned" android:inputType="number"
android:defaultValue="3" android:defaultValue="1080"
app:useSimpleSummaryProvider="true" /> app:useSimpleSummaryProvider="true"/>
<CheckBoxPreference <com.takisoft.preferencex.EditTextPreference
android:key="byedpi_split_at_host" android:key="byedpi_max_connections"
android:title="Split at host" android:title="@string/byedpi_max_connections_setting"
android:defaultValue="0"/> android:inputType="number"
android:defaultValue="512"
app:useSimpleSummaryProvider="true"/>
<com.takisoft.preferencex.EditTextPreference <com.takisoft.preferencex.EditTextPreference
android:key="byedpi_fake_ttl" android:key="byedpi_buffer_size"
android:title="TTL of fake packets" android:title="@string/byedpi_buffer_size_setting"
android:inputType="number" android:inputType="number"
android:defaultValue="8" android:defaultValue="16384"
app:useSimpleSummaryProvider="true" /> app:useSimpleSummaryProvider="true" />
<CheckBoxPreference <com.takisoft.preferencex.EditTextPreference
android:key="byedpi_host_mixed_case" android:key="byedpi_default_ttl"
android:title="Host mixed case" android:title="@string/byedpi_default_ttl_setting"
android:defaultValue="false"/> android:inputType="number"
android:defaultValue="0"
app:useSimpleSummaryProvider="true" />
<CheckBoxPreference <CheckBoxPreference
android:key="byedpi_domain_mixed_case" android:key="byedpi_no_domain"
android:title="Domain mixed case" android:title="@string/byedpi_no_domain_setting"
android:defaultValue="false"/> android:defaultValue="false"/>
<CheckBoxPreference <CheckBoxPreference
android:key="byedpi_host_remove_spaces" android:key="byedpi_desync_known"
android:title="Host remove spaces" android:title="@string/byedpi_desync_known_setting"
android:defaultValue="false"/> android:defaultValue="false"/>
<CheckBoxPreference <DropDownPreference
android:key="byedpi_tlsrec_enabled" android:key="byedpi_desync_method"
android:title="Split TLS record" android:title="@string/byedpi_desync_method_setting"
android:defaultValue="false"/> android:entries="@array/byedpi_desync_methods"
android:entryValues="@array/byedpi_desync_methods_entries"
android:defaultValue="disorder"
app:useSimpleSummaryProvider="true" />
<com.takisoft.preferencex.EditTextPreference <com.takisoft.preferencex.EditTextPreference
android:key="byedpi_tlsrec_position" android:key="byedpi_split_position"
android:title="TLS record split position" android:title="@string/byedpi_split_position_setting"
android:inputType="numberSigned" android:inputType="numberSigned"
android:defaultValue="0" android:defaultValue="3"
app:useSimpleSummaryProvider="true" /> app:useSimpleSummaryProvider="true" />
<CheckBoxPreference <CheckBoxPreference
android:key="byedpi_tlsrec_at_sni" android:key="byedpi_split_at_host"
android:title="Split TLS record at SNI" android:title="@string/byedpi_split_at_host_setting"
android:defaultValue="false"/> android:defaultValue="0"/>
<com.takisoft.preferencex.EditTextPreference
android:key="byedpi_fake_ttl"
android:title="@string/byedpi_fake_ttl_setting"
android:inputType="number"
android:defaultValue="8"
app:useSimpleSummaryProvider="true" />
<CheckBoxPreference
android:key="byedpi_host_mixed_case"
android:title="@string/byedpi_host_mixed_case_setting"
android:defaultValue="false"/>
<CheckBoxPreference
android:key="byedpi_domain_mixed_case"
android:title="@string/byedpi_domain_mixed_case_setting"
android:defaultValue="false"/>
<CheckBoxPreference
android:key="byedpi_host_remove_spaces"
android:title="@string/byedpi_host_remove_spaces_setting"
android:defaultValue="false"/>
<CheckBoxPreference
android:key="byedpi_tlsrec_enabled"
android:title="@string/byedpi_tlsrec_enabled_setting"
android:defaultValue="false"/>
<com.takisoft.preferencex.EditTextPreference
android:key="byedpi_tlsrec_position"
android:title="@string/byedpi_tlsrec_position_setting"
android:inputType="numberSigned"
android:defaultValue="0"
app:useSimpleSummaryProvider="true" />
<CheckBoxPreference
android:key="byedpi_tlsrec_at_sni"
android:title="@string/byedpi_tlsrec_at_sni_setting"
android:defaultValue="false" />
</PreferenceCategory>
<PreferenceCategory
android:title="About">
<Preference
android:key="version"
android:title="@string/version"
app:useSimpleSummaryProvider="true"
tools:summary="1.0.0" />
<Preference
android:key="source_code"
android:title="@string/source_code_link"
android:icon="@drawable/ic_github_36">
<intent
android:action="android.intent.action.VIEW"
android:data="https://github.com/dovecoteescapee/ByeDPIAndroid" />
</Preference>
</PreferenceCategory>
</PreferenceScreen> </PreferenceScreen>