mirror of
https://github.com/dovecoteescapee/ByeDPIAndroid.git
synced 2024-12-21 22:06:22 +00:00
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:
parent
54cc788ccd
commit
4669bd9c65
6
.idea/render.experimental.xml
Normal file
6
.idea/render.experimental.xml
Normal 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>
|
@ -11,19 +11,28 @@ android {
|
||||
|
||||
defaultConfig {
|
||||
applicationId = "io.github.dovecoteescapee.byedpi"
|
||||
minSdk = 24
|
||||
minSdk = 21
|
||||
targetSdk = 34
|
||||
versionCode = 2
|
||||
versionName = "0.1.1-alpha"
|
||||
versionCode = 3
|
||||
versionName = "1.0.0-rc1"
|
||||
|
||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||
}
|
||||
|
||||
buildFeatures {
|
||||
buildConfig = true
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
release {
|
||||
buildConfigField("String", "VERSION_NAME", "\"${defaultConfig.versionName}\"")
|
||||
|
||||
isMinifyEnabled = false
|
||||
proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro")
|
||||
}
|
||||
debug {
|
||||
buildConfigField("String", "VERSION_NAME", "\"${defaultConfig.versionName}-debug\"")
|
||||
}
|
||||
}
|
||||
compileOptions {
|
||||
sourceCompatibility = JavaVersion.VERSION_1_8
|
||||
@ -72,7 +81,7 @@ abstract class BuildTun2Socks : DefaultTask() {
|
||||
}
|
||||
project.exec {
|
||||
workingDir = tun2socksDir
|
||||
commandLine("gomobile", "bind", "-o", tun2socksOutput, "./engine")
|
||||
commandLine("gomobile", "bind", "-o", tun2socksOutput, "-trimpath", "./engine")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -3,9 +3,9 @@
|
||||
xmlns:tools="http://schemas.android.com/tools">
|
||||
|
||||
<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_SPECIAL_USE" />
|
||||
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
||||
|
||||
<application
|
||||
android:allowBackup="true"
|
||||
@ -16,10 +16,11 @@
|
||||
android:roundIcon="@mipmap/ic_launcher_round"
|
||||
android:supportsRtl="true"
|
||||
android:theme="@style/Theme.ByeDPI"
|
||||
tools:targetApi="31">
|
||||
tools:targetApi="34">
|
||||
<activity
|
||||
android:name=".MainActivity"
|
||||
android:exported="true">
|
||||
android:name=".activities.MainActivity"
|
||||
android:exported="true"
|
||||
android:launchMode="singleInstance">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
|
||||
@ -28,16 +29,31 @@
|
||||
</activity>
|
||||
|
||||
<activity
|
||||
android:name=".SettingsActivity"
|
||||
android:name=".activities.SettingsActivity"
|
||||
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:exported="true">
|
||||
android:foregroundServiceType="specialUse"
|
||||
android:exported="false">
|
||||
<intent-filter>
|
||||
<action android:name="android.net.VpnService"/>
|
||||
</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>
|
||||
</application>
|
||||
|
||||
|
@ -30,6 +30,7 @@ add_library(${CMAKE_PROJECT_NAME} SHARED
|
||||
byedpi/desync.c
|
||||
byedpi/packets.c
|
||||
byedpi/proxy.c
|
||||
byedpi/main.c
|
||||
native-lib.c
|
||||
)
|
||||
|
||||
@ -37,6 +38,8 @@ include_directories("byedpi")
|
||||
|
||||
set(CMAKE_C_FLAGS "-std=c99 -O2 -D_XOPEN_SOURCE=500")
|
||||
|
||||
add_compile_definitions(ANDROID_APP)
|
||||
|
||||
# Specifies libraries CMake should link to your target library. You
|
||||
# can link libraries from various origins, such as libraries defined in this
|
||||
# build script, prebuilt third-party libraries, or Android system libraries.
|
||||
|
@ -1 +1 @@
|
||||
Subproject commit d880b0441f8c31e0609fb98be90d722190cd737b
|
||||
Subproject commit 12adfe285f603bfa38c19e9057d897b2b4e40941
|
@ -2,74 +2,40 @@
|
||||
#include <proxy.h>
|
||||
#include <params.h>
|
||||
#include <packets.h>
|
||||
#include <unistd.h>
|
||||
#include <string.h>
|
||||
#include <netdb.h>
|
||||
#include <sys/eventfd.h>
|
||||
|
||||
#include <jni.h>
|
||||
#include <android/log.h>
|
||||
#include <unistd.h>
|
||||
#include <pthread.h>
|
||||
#include <string.h>
|
||||
|
||||
extern int NOT_EXIT;
|
||||
|
||||
struct packet fake_tls = {
|
||||
sizeof(tls_data), tls_data
|
||||
},
|
||||
fake_http = {
|
||||
sizeof(http_data), http_data
|
||||
const enum demode DESYNC_METHODS[] = {
|
||||
DESYNC_NONE,
|
||||
DESYNC_SPLIT,
|
||||
DESYNC_DISORDER,
|
||||
DESYNC_FAKE
|
||||
};
|
||||
|
||||
struct params params = {
|
||||
.ttl = 8,
|
||||
.split = 3,
|
||||
.sfdelay = 3000,
|
||||
.attack = DESYNC_NONE,
|
||||
.split_host = 0,
|
||||
.def_ttl = 0,
|
||||
.custom_ttl = 0,
|
||||
.mod_http = 0,
|
||||
.tlsrec = 0,
|
||||
.tlsrec_pos = 0,
|
||||
.tlsrec_sni = 0,
|
||||
.de_known = 0,
|
||||
extern struct packet fake_tls, fake_http;
|
||||
extern int get_default_ttl();
|
||||
extern int get_addr(const char *str, struct sockaddr_ina *addr);
|
||||
|
||||
.ipv6 = 1,
|
||||
.resolve = 1,
|
||||
.max_open = 512,
|
||||
.bfsize = 16384,
|
||||
.baddr = {
|
||||
.sin6_family = AF_INET
|
||||
},
|
||||
.debug = 2
|
||||
};
|
||||
|
||||
int get_default_ttl()
|
||||
{
|
||||
int orig_ttl = -1, fd;
|
||||
socklen_t tsize = sizeof(orig_ttl);
|
||||
|
||||
if ((fd = socket(AF_INET, SOCK_STREAM, 0)) < 0) {
|
||||
uniperror("socket");
|
||||
JNIEXPORT jint JNICALL
|
||||
Java_io_github_dovecoteescapee_byedpi_core_ByeDpiProxy_jniEventFd(JNIEnv *env, jobject thiz) {
|
||||
int fd = eventfd(0, EFD_NONBLOCK);
|
||||
if (fd < 0) {
|
||||
return -1;
|
||||
}
|
||||
if (getsockopt(fd, IPPROTO_IP, IP_TTL,
|
||||
(char *)&orig_ttl, &tsize) < 0) {
|
||||
uniperror("getsockopt IP_TTL");
|
||||
}
|
||||
close(fd);
|
||||
return orig_ttl;
|
||||
return fd;
|
||||
}
|
||||
|
||||
void *run(void *srv) {
|
||||
LOG(LOG_S, "Start proxy thread");
|
||||
listener(*((struct sockaddr_ina *) srv));
|
||||
free(srv);
|
||||
LOG(LOG_S, "Stop proxy thread");
|
||||
return NULL;
|
||||
}
|
||||
|
||||
JNIEXPORT jlong JNICALL
|
||||
Java_io_github_dovecoteescapee_byedpi_ByeDpiVpnService_jniStartProxy(
|
||||
JNIEXPORT jint JNICALL
|
||||
Java_io_github_dovecoteescapee_byedpi_core_ByeDpiProxy_jniStartProxy(
|
||||
JNIEnv *env,
|
||||
jobject thiz,
|
||||
jint event_fd,
|
||||
jstring ip,
|
||||
jint port,
|
||||
jint max_connections,
|
||||
jint buffer_size,
|
||||
@ -82,24 +48,35 @@ Java_io_github_dovecoteescapee_byedpi_ByeDpiVpnService_jniStartProxy(
|
||||
jint fake_ttl,
|
||||
jboolean host_mixed_case,
|
||||
jboolean domain_mixed_case,
|
||||
jboolean host_remove_space,
|
||||
jboolean host_remove_spaces,
|
||||
jboolean tls_record_split,
|
||||
jint tls_record_split_position,
|
||||
jboolean tls_record_split_at_sni) {
|
||||
enum demode desync_methods[] = {DESYNC_NONE, DESYNC_SPLIT, DESYNC_DISORDER, DESYNC_FAKE};
|
||||
|
||||
struct sockaddr_ina s = {
|
||||
.in.sin_family = AF_INET,
|
||||
.in.sin_addr.s_addr = inet_addr("0.0.0.0"),
|
||||
};
|
||||
|
||||
const char *address = (*env)->GetStringUTFChars(env, ip, 0);
|
||||
if (get_addr(address, &s) < 0) {
|
||||
return -1;
|
||||
}
|
||||
s.in.sin_port = htons(port);
|
||||
|
||||
params.max_open = max_connections;
|
||||
params.bfsize = buffer_size;
|
||||
params.def_ttl = default_ttl;
|
||||
params.resolve = !no_domain;
|
||||
params.de_known = desync_known;
|
||||
params.attack = desync_methods[desync_method];
|
||||
params.attack = DESYNC_METHODS[desync_method];
|
||||
params.split = split_position;
|
||||
params.split_host = split_at_host;
|
||||
params.ttl = fake_ttl;
|
||||
params.mod_http |= host_mixed_case ? MH_HMIX : 0;
|
||||
params.mod_http |= domain_mixed_case ? MH_DMIX : 0;
|
||||
params.mod_http |= host_remove_space ? MH_SPACE : 0;
|
||||
params.mod_http =
|
||||
MH_HMIX * host_mixed_case |
|
||||
MH_DMIX * domain_mixed_case |
|
||||
MH_SPACE * host_remove_spaces;
|
||||
params.tlsrec = tls_record_split;
|
||||
params.tlsrec_pos = tls_record_split_position;
|
||||
params.tlsrec_sni = tls_record_split_at_sni;
|
||||
@ -110,23 +87,22 @@ Java_io_github_dovecoteescapee_byedpi_ByeDpiVpnService_jniStartProxy(
|
||||
}
|
||||
}
|
||||
|
||||
struct sockaddr_ina *srv = malloc(sizeof(struct sockaddr_ina));
|
||||
srv->in.sin_family = AF_INET;
|
||||
srv->in.sin_addr.s_addr = inet_addr("0.0.0.0");
|
||||
srv->in.sin_port = htons(port);
|
||||
int res = listener(event_fd, s);
|
||||
|
||||
NOT_EXIT = 1;
|
||||
|
||||
pthread_t proxy_thread;
|
||||
if (pthread_create(&proxy_thread, NULL, run, srv) != 0) {
|
||||
LOG(LOG_S, "Failed to start proxy thread");
|
||||
return -1;
|
||||
if (close(event_fd) < 0) {
|
||||
uniperror("close");
|
||||
}
|
||||
|
||||
return proxy_thread;
|
||||
return res < 0 ? get_e() : 0;
|
||||
}
|
||||
|
||||
JNIEXPORT void JNICALL
|
||||
Java_io_github_dovecoteescapee_byedpi_ByeDpiVpnService_jniStopProxy(JNIEnv *env, jobject thiz, jlong proxy_thread) {
|
||||
NOT_EXIT = 0;
|
||||
}
|
||||
JNIEXPORT jint JNICALL
|
||||
Java_io_github_dovecoteescapee_byedpi_core_ByeDpiProxy_jniStopProxy(JNIEnv *env, jobject thiz,
|
||||
jint event_fd) {
|
||||
if (eventfd_write(event_fd, 1) < 0) {
|
||||
uniperror("eventfd_write");
|
||||
LOG(LOG_S, "event_fd: %d", event_fd);
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
@ -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")
|
||||
}
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
@ -1,6 +0,0 @@
|
||||
package io.github.dovecoteescapee.byedpi
|
||||
|
||||
enum class Status {
|
||||
RUNNING,
|
||||
STOPPED
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
@ -1,6 +1,9 @@
|
||||
package io.github.dovecoteescapee.byedpi
|
||||
package io.github.dovecoteescapee.byedpi.core
|
||||
|
||||
import android.content.SharedPreferences
|
||||
|
||||
class ByeDpiProxyPreferences(
|
||||
ip: String? = null,
|
||||
port: Int? = null,
|
||||
maxConnections: Int? = null,
|
||||
bufferSize: Int? = null,
|
||||
@ -18,6 +21,7 @@ class ByeDpiProxyPreferences(
|
||||
tlsRecordSplitPosition: Int? = null,
|
||||
tlsRecordSplitAtSni: Boolean? = null,
|
||||
) {
|
||||
val ip: String = ip ?: "127.0.0.1"
|
||||
val port: Int = port ?: 1080
|
||||
val maxConnections: Int = maxConnections ?: 512
|
||||
val bufferSize: Int = bufferSize ?: 16384
|
||||
@ -35,6 +39,29 @@ class ByeDpiProxyPreferences(
|
||||
val tlsRecordSplitPosition: Int = tlsRecordSplitPosition ?: 0
|
||||
val tlsRecordSplitAtSni: Boolean = tlsRecordSplitAtSni ?: false
|
||||
|
||||
constructor(preferences: SharedPreferences) : this(
|
||||
ip = preferences.getString("byedpi_proxy_ip", null),
|
||||
port = preferences.getString("byedpi_proxy_port", null)?.toInt(),
|
||||
maxConnections = preferences.getString("byedpi_max_connections", null)?.toInt(),
|
||||
bufferSize = preferences.getString("byedpi_buffer_size", null)?.toInt(),
|
||||
defaultTtl = preferences.getString("byedpi_default_ttl", null)?.toInt(),
|
||||
noDomain = preferences.getBoolean("byedpi_no_domain", false),
|
||||
desyncKnown = preferences.getBoolean("byedpi_desync_known", false),
|
||||
desyncMethod = preferences.getString("byedpi_desync_method", null)
|
||||
?.let { DesyncMethod.fromName(it) },
|
||||
splitPosition = preferences.getString("byedpi_split_position", null)?.toInt(),
|
||||
splitAtHost = preferences.getBoolean("byedpi_split_at_host", false),
|
||||
fakeTtl = preferences.getString("byedpi_fake_ttl", null)?.toInt(),
|
||||
hostMixedCase = preferences.getBoolean("byedpi_host_mixed_case", false),
|
||||
domainMixedCase = preferences.getBoolean("byedpi_domain_mixed_case", false),
|
||||
hostRemoveSpaces = preferences.getBoolean("byedpi_host_remove_spaces", false),
|
||||
tlsRecordSplit = preferences.getBoolean("byedpi_tlsrec_enabled", false),
|
||||
tlsRecordSplitPosition = preferences.getString("byedpi_tlsrec_position", null)?.toInt(),
|
||||
tlsRecordSplitAtSni = preferences.getBoolean("byedpi_tlsrec_at_sni", false),
|
||||
|
||||
|
||||
)
|
||||
|
||||
enum class DesyncMethod {
|
||||
None,
|
||||
Split,
|
@ -0,0 +1,4 @@
|
||||
package io.github.dovecoteescapee.byedpi.data
|
||||
|
||||
const val START_ACTION = "start"
|
||||
const val STOP_ACTION = "stop"
|
@ -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")
|
||||
}
|
||||
}
|
||||
}
|
@ -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")
|
||||
}
|
@ -0,0 +1,7 @@
|
||||
package io.github.dovecoteescapee.byedpi.data
|
||||
|
||||
enum class ServiceStatus {
|
||||
DISCONNECTED,
|
||||
CONNECTED,
|
||||
FAILED,
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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,
|
||||
)
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
@ -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()
|
@ -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"))
|
8
app/src/main/res/drawable/ic_github_36.xml
Normal file
8
app/src/main/res/drawable/ic_github_36.xml
Normal 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>
|
@ -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>
|
@ -4,23 +4,49 @@
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
tools:context=".MainActivity">
|
||||
tools:context=".activities.MainActivity">
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/status_button"
|
||||
style="@style/Widget.MaterialComponents.Button.OutlinedButton"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/start"
|
||||
android:textSize="24sp"
|
||||
android:textAllCaps="false"
|
||||
android:paddingLeft="10pt"
|
||||
android:paddingRight="10pt"
|
||||
android:paddingTop="5pt"
|
||||
android:paddingRight="10pt"
|
||||
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_constraintHorizontal_bias="0.5"
|
||||
app:layout_constraintStart_toStartOf="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>
|
||||
|
@ -2,7 +2,7 @@
|
||||
<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">
|
||||
tools:context=".activities.MainActivity">
|
||||
|
||||
<item
|
||||
android:id="@+id/action_settings"
|
||||
@ -11,4 +11,10 @@
|
||||
android:icon="@drawable/baseline_settings_24"
|
||||
app:showAsAction="ifRoom" />
|
||||
|
||||
<item
|
||||
android:id="@+id/action_save_logs"
|
||||
android:orderInCategory="2"
|
||||
android:title="@string/save_logs"
|
||||
app:showAsAction="never" />
|
||||
|
||||
</menu>
|
||||
|
@ -2,7 +2,7 @@
|
||||
<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">
|
||||
tools:context=".activities.MainActivity">
|
||||
|
||||
<item
|
||||
android:id="@+id/action_reset_settings"
|
||||
@ -10,10 +10,4 @@
|
||||
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>
|
||||
|
@ -4,13 +4,10 @@
|
||||
<!-- Primary brand color. -->
|
||||
<item name="colorPrimary">@color/white</item>
|
||||
<item name="colorPrimaryVariant">#E5E5E5</item>
|
||||
<item name="colorOnPrimary">@color/white</item>
|
||||
<item name="colorOnPrimary">@color/black</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. -->
|
||||
<item name="colorSecondary">#5976DF</item>
|
||||
<item name="colorSecondaryVariant">#3C52A3</item>
|
||||
<item name="colorOnSecondary">@color/white</item>
|
||||
</style>
|
||||
</resources>
|
||||
|
@ -1,5 +1,25 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<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">
|
||||
<item name="none">None</item>
|
||||
<item name="split">Split</item>
|
||||
|
@ -1,7 +1,17 @@
|
||||
<resources>
|
||||
<string name="app_name">ByeDPI</string>
|
||||
<string name="start">Start</string>
|
||||
<string name="stop">Stop</string>
|
||||
<string name="vpn_connect">Connect</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="vpn_permission_denied">VPN permission denied</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="reset_settings">Reset</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>
|
@ -6,9 +6,9 @@
|
||||
<item name="colorPrimaryVariant">#3C52A3</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>
|
||||
<item name="colorSecondary">#5976DF</item>
|
||||
<item name="colorSecondaryVariant">#3C52A3</item>
|
||||
<item name="colorOnSecondary">@color/white</item>
|
||||
<!-- Status bar color. -->
|
||||
<item name="android:statusBarColor">?attr/colorPrimaryVariant</item>
|
||||
<!-- Customize your theme here. -->
|
||||
|
@ -1,109 +1,172 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<PreferenceScreen 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"
|
||||
android:tag="settings_screen">
|
||||
|
||||
<com.takisoft.preferencex.EditTextPreference
|
||||
android:key="dns_ip"
|
||||
android:title="DNS"
|
||||
android:defaultValue="9.9.9.9"
|
||||
app:useSimpleSummaryProvider="true"/>
|
||||
<PreferenceCategory
|
||||
android:title="General">
|
||||
|
||||
<com.takisoft.preferencex.EditTextPreference
|
||||
android:key="byedpi_port"
|
||||
android:title="Port"
|
||||
android:inputType="number"
|
||||
android:defaultValue="1080"
|
||||
app:useSimpleSummaryProvider="true"/>
|
||||
<DropDownPreference
|
||||
android:key="app_theme"
|
||||
android:title="@string/theme_settings"
|
||||
android:entries="@array/themes"
|
||||
android:entryValues="@array/themes_entries"
|
||||
android:defaultValue="system"
|
||||
app:useSimpleSummaryProvider="true" />
|
||||
|
||||
<com.takisoft.preferencex.EditTextPreference
|
||||
android:key="byedpi_max_connections"
|
||||
android:title="Maximum number of connections"
|
||||
android:inputType="number"
|
||||
android:defaultValue="512"
|
||||
app:useSimpleSummaryProvider="true"/>
|
||||
<DropDownPreference
|
||||
android:key="byedpi_mode"
|
||||
android:title="@string/mode_setting"
|
||||
android:entries="@array/byedpi_modes"
|
||||
android:entryValues="@array/byedpi_modes_entries"
|
||||
android:defaultValue="vpn"
|
||||
app:useSimpleSummaryProvider="true" />
|
||||
|
||||
<com.takisoft.preferencex.EditTextPreference
|
||||
android:key="byedpi_buffer_size"
|
||||
android:title="Buffer size"
|
||||
android:inputType="number"
|
||||
android:defaultValue="16384"
|
||||
app:useSimpleSummaryProvider="true" />
|
||||
</PreferenceCategory>
|
||||
|
||||
<com.takisoft.preferencex.EditTextPreference
|
||||
android:key="byedpi_default_ttl"
|
||||
android:title="Default TTL"
|
||||
android:inputType="number"
|
||||
android:defaultValue="0"
|
||||
app:useSimpleSummaryProvider="true" />
|
||||
<PreferenceCategory
|
||||
android:title="ByeDPI">
|
||||
|
||||
<CheckBoxPreference
|
||||
android:key="byedpi_no_domain"
|
||||
android:title="No domain"
|
||||
android:defaultValue="false"/>
|
||||
<Preference
|
||||
android:key="byedpi_readme"
|
||||
android:title="@string/byedpi_readme_link"
|
||||
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
|
||||
android:key="byedpi_desync_knows"
|
||||
android:title="Desync only HTTPS and TLS"
|
||||
android:defaultValue="false"/>
|
||||
<com.takisoft.preferencex.EditTextPreference
|
||||
android:key="dns_ip"
|
||||
android:title="@string/dbs_ip_setting"
|
||||
android:defaultValue="9.9.9.9"
|
||||
app:useSimpleSummaryProvider="true"/>
|
||||
|
||||
<DropDownPreference
|
||||
android:key="byedpi_desync_method"
|
||||
android:title="Desync method"
|
||||
android:entries="@array/byedpi_desync_methods"
|
||||
android:entryValues="@array/byedpi_desync_methods_entries"
|
||||
android:defaultValue="disorder"
|
||||
app:useSimpleSummaryProvider="true" />
|
||||
<com.takisoft.preferencex.EditTextPreference
|
||||
android:key="byedpi_proxy_ip"
|
||||
android:title="@string/bye_dpi_proxy_ip_setting"
|
||||
android:defaultValue="127.0.0.1"
|
||||
app:useSimpleSummaryProvider="true" />
|
||||
|
||||
<com.takisoft.preferencex.EditTextPreference
|
||||
android:key="byedpi_split_position"
|
||||
android:title="Split position"
|
||||
android:inputType="numberSigned"
|
||||
android:defaultValue="3"
|
||||
app:useSimpleSummaryProvider="true" />
|
||||
<com.takisoft.preferencex.EditTextPreference
|
||||
android:key="byedpi_proxy_port"
|
||||
android:title="@string/byedpi_proxy_port_setting"
|
||||
android:inputType="number"
|
||||
android:defaultValue="1080"
|
||||
app:useSimpleSummaryProvider="true"/>
|
||||
|
||||
<CheckBoxPreference
|
||||
android:key="byedpi_split_at_host"
|
||||
android:title="Split at host"
|
||||
android:defaultValue="0"/>
|
||||
<com.takisoft.preferencex.EditTextPreference
|
||||
android:key="byedpi_max_connections"
|
||||
android:title="@string/byedpi_max_connections_setting"
|
||||
android:inputType="number"
|
||||
android:defaultValue="512"
|
||||
app:useSimpleSummaryProvider="true"/>
|
||||
|
||||
<com.takisoft.preferencex.EditTextPreference
|
||||
android:key="byedpi_fake_ttl"
|
||||
android:title="TTL of fake packets"
|
||||
android:inputType="number"
|
||||
android:defaultValue="8"
|
||||
app:useSimpleSummaryProvider="true" />
|
||||
<com.takisoft.preferencex.EditTextPreference
|
||||
android:key="byedpi_buffer_size"
|
||||
android:title="@string/byedpi_buffer_size_setting"
|
||||
android:inputType="number"
|
||||
android:defaultValue="16384"
|
||||
app:useSimpleSummaryProvider="true" />
|
||||
|
||||
<CheckBoxPreference
|
||||
android:key="byedpi_host_mixed_case"
|
||||
android:title="Host mixed case"
|
||||
android:defaultValue="false"/>
|
||||
<com.takisoft.preferencex.EditTextPreference
|
||||
android:key="byedpi_default_ttl"
|
||||
android:title="@string/byedpi_default_ttl_setting"
|
||||
android:inputType="number"
|
||||
android:defaultValue="0"
|
||||
app:useSimpleSummaryProvider="true" />
|
||||
|
||||
<CheckBoxPreference
|
||||
android:key="byedpi_domain_mixed_case"
|
||||
android:title="Domain mixed case"
|
||||
android:defaultValue="false"/>
|
||||
<CheckBoxPreference
|
||||
android:key="byedpi_no_domain"
|
||||
android:title="@string/byedpi_no_domain_setting"
|
||||
android:defaultValue="false"/>
|
||||
|
||||
<CheckBoxPreference
|
||||
android:key="byedpi_host_remove_spaces"
|
||||
android:title="Host remove spaces"
|
||||
android:defaultValue="false"/>
|
||||
<CheckBoxPreference
|
||||
android:key="byedpi_desync_known"
|
||||
android:title="@string/byedpi_desync_known_setting"
|
||||
android:defaultValue="false"/>
|
||||
|
||||
<CheckBoxPreference
|
||||
android:key="byedpi_tlsrec_enabled"
|
||||
android:title="Split TLS record"
|
||||
android:defaultValue="false"/>
|
||||
<DropDownPreference
|
||||
android:key="byedpi_desync_method"
|
||||
android:title="@string/byedpi_desync_method_setting"
|
||||
android:entries="@array/byedpi_desync_methods"
|
||||
android:entryValues="@array/byedpi_desync_methods_entries"
|
||||
android:defaultValue="disorder"
|
||||
app:useSimpleSummaryProvider="true" />
|
||||
|
||||
<com.takisoft.preferencex.EditTextPreference
|
||||
android:key="byedpi_tlsrec_position"
|
||||
android:title="TLS record split position"
|
||||
android:inputType="numberSigned"
|
||||
android:defaultValue="0"
|
||||
app:useSimpleSummaryProvider="true" />
|
||||
<com.takisoft.preferencex.EditTextPreference
|
||||
android:key="byedpi_split_position"
|
||||
android:title="@string/byedpi_split_position_setting"
|
||||
android:inputType="numberSigned"
|
||||
android:defaultValue="3"
|
||||
app:useSimpleSummaryProvider="true" />
|
||||
|
||||
<CheckBoxPreference
|
||||
android:key="byedpi_tlsrec_at_sni"
|
||||
android:title="Split TLS record at SNI"
|
||||
android:defaultValue="false"/>
|
||||
<CheckBoxPreference
|
||||
android:key="byedpi_split_at_host"
|
||||
android:title="@string/byedpi_split_at_host_setting"
|
||||
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>
|
||||
|
Loading…
Reference in New Issue
Block a user