mirror of
https://github.com/dovecoteescapee/ByeDPIAndroid.git
synced 2024-12-22 06:15:44 +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 {
|
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")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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>
|
||||||
|
|
||||||
|
@ -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
|
@ -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;
|
||||||
}
|
}
|
@ -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(
|
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,
|
@ -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"
|
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>
|
||||||
|
@ -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>
|
||||||
|
@ -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>
|
||||||
|
@ -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>
|
||||||
|
@ -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>
|
||||||
|
@ -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>
|
@ -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. -->
|
||||||
|
@ -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>
|
||||||
|
Loading…
Reference in New Issue
Block a user