diff --git a/CMakeLists.txt b/CMakeLists.txt
index 7f363dcd..28bb2266 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -2,7 +2,7 @@ cmake_minimum_required(VERSION 3.25.0 FATAL_ERROR)
set(PROJECT AmneziaVPN)
-project(${PROJECT} VERSION 4.5.3.0
+project(${PROJECT} VERSION 4.6.0.0
DESCRIPTION "AmneziaVPN"
HOMEPAGE_URL "https://amnezia.org/"
)
@@ -11,7 +11,7 @@ string(TIMESTAMP CURRENT_DATE "%Y-%m-%d")
set(RELEASE_DATE "${CURRENT_DATE}")
set(APP_MAJOR_VERSION ${CMAKE_PROJECT_VERSION_MAJOR}.${CMAKE_PROJECT_VERSION_MINOR}.${CMAKE_PROJECT_VERSION_PATCH})
-set(APP_ANDROID_VERSION_CODE 52)
+set(APP_ANDROID_VERSION_CODE 53)
if(${CMAKE_SYSTEM_NAME} STREQUAL "Linux")
set(MZ_PLATFORM_NAME "linux")
diff --git a/client/3rd-prebuilt b/client/3rd-prebuilt
index eb43e90f..ea49bf87 160000
--- a/client/3rd-prebuilt
+++ b/client/3rd-prebuilt
@@ -1 +1 @@
-Subproject commit eb43e90f389745af6d7ca3be92a96e400ba6dc6c
+Subproject commit ea49bf8796afbc5bd70a0f98f4d99c9ea4792d80
diff --git a/client/android/AndroidManifest.xml b/client/android/AndroidManifest.xml
index 9637b029..f1d2682b 100644
--- a/client/android/AndroidManifest.xml
+++ b/client/android/AndroidManifest.xml
@@ -136,8 +136,34 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+ , onError: (String) -> Unit) {
- super.initialize(context, state, onError)
- loadSharedLibrary(context, "ovpn3")
- this.context = context
+ override fun internalInit() {
+ if (!isInitialized) loadSharedLibrary(context, "ovpn3")
+ if (this::scope.isInitialized) {
+ scope.cancel()
+ }
scope = CoroutineScope(Dispatchers.IO)
}
diff --git a/client/android/protocolApi/src/main/kotlin/Protocol.kt b/client/android/protocolApi/src/main/kotlin/Protocol.kt
index e51d0fc1..a475a2fc 100644
--- a/client/android/protocolApi/src/main/kotlin/Protocol.kt
+++ b/client/android/protocolApi/src/main/kotlin/Protocol.kt
@@ -27,14 +27,21 @@ private const val SPLIT_TUNNEL_EXCLUDE = 2
abstract class Protocol {
abstract val statistics: Statistics
+ protected lateinit var context: Context
protected lateinit var state: MutableStateFlow
protected lateinit var onError: (String) -> Unit
+ protected var isInitialized: Boolean = false
- open fun initialize(context: Context, state: MutableStateFlow, onError: (String) -> Unit) {
+ fun initialize(context: Context, state: MutableStateFlow, onError: (String) -> Unit) {
+ this.context = context
this.state = state
this.onError = onError
+ internalInit()
+ isInitialized = true
}
+ protected abstract fun internalInit()
+
abstract fun startVpn(config: JSONObject, vpnBuilder: Builder, protect: (Int) -> Boolean)
abstract fun stopVpn()
diff --git a/client/android/qt/build.gradle.kts b/client/android/qt/build.gradle.kts
index 2ec2c941..139adf4f 100644
--- a/client/android/qt/build.gradle.kts
+++ b/client/android/qt/build.gradle.kts
@@ -21,5 +21,5 @@ android {
}
dependencies {
- implementation(fileTree(mapOf("dir" to "../libs", "include" to listOf("*.jar", "*.aar"))))
+ implementation(fileTree(mapOf("dir" to "../libs", "include" to listOf("*.jar"))))
}
diff --git a/client/android/settings.gradle.kts b/client/android/settings.gradle.kts
index d270731b..5cfc8314 100644
--- a/client/android/settings.gradle.kts
+++ b/client/android/settings.gradle.kts
@@ -36,6 +36,8 @@ include(":wireguard")
include(":awg")
include(":openvpn")
include(":cloak")
+include(":xray")
+include(":xray:libXray")
// get values from gradle or local properties
val androidBuildToolsVersion: String by gradleProperties
diff --git a/client/android/src/org/amnezia/vpn/AmneziaActivity.kt b/client/android/src/org/amnezia/vpn/AmneziaActivity.kt
index c9063f22..202fe2e6 100644
--- a/client/android/src/org/amnezia/vpn/AmneziaActivity.kt
+++ b/client/android/src/org/amnezia/vpn/AmneziaActivity.kt
@@ -34,6 +34,7 @@ import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
+import kotlinx.coroutines.async
import kotlinx.coroutines.cancel
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
@@ -43,6 +44,8 @@ import org.amnezia.vpn.protocol.getStatus
import org.amnezia.vpn.qt.QtAndroidController
import org.amnezia.vpn.util.Log
import org.amnezia.vpn.util.Prefs
+import org.json.JSONException
+import org.json.JSONObject
import org.qtproject.qt.android.bindings.QtActivity
private const val TAG = "AmneziaActivity"
@@ -59,6 +62,7 @@ class AmneziaActivity : QtActivity() {
private lateinit var mainScope: CoroutineScope
private val qtInitialized = CompletableDeferred()
+ private var vpnProto: VpnProto? = null
private var isWaitingStatus = true
private var isServiceConnected = false
private var isInBoundState = false
@@ -141,6 +145,7 @@ class AmneziaActivity : QtActivity() {
override fun onBindingDied(name: ComponentName?) {
Log.w(TAG, "Binding to the ${name?.flattenToString()} unexpectedly died")
doUnbindService()
+ QtAndroidController.onServiceDisconnected()
doBindService()
}
}
@@ -153,15 +158,20 @@ class AmneziaActivity : QtActivity() {
super.onCreate(savedInstanceState)
Log.d(TAG, "Create Amnezia activity: $intent")
mainScope = CoroutineScope(SupervisorJob() + Dispatchers.Main.immediate)
+ val proto = mainScope.async(Dispatchers.IO) {
+ VpnStateStore.getVpnState().vpnProto
+ }
vpnServiceMessenger = IpcMessenger(
"VpnService",
onDeadObjectException = {
doUnbindService()
+ QtAndroidController.onServiceDisconnected()
doBindService()
}
)
registerBroadcastReceivers()
intent?.let(::processIntent)
+ runBlocking { vpnProto = proto.await() }
}
private fun registerBroadcastReceivers() {
@@ -209,13 +219,18 @@ class AmneziaActivity : QtActivity() {
Log.d(TAG, "Start Amnezia activity")
mainScope.launch {
qtInitialized.await()
- doBindService()
+ vpnProto?.let { proto ->
+ if (AmneziaVpnService.isRunning(applicationContext, proto.processName)) {
+ doBindService()
+ }
+ }
}
}
override fun onStop() {
Log.d(TAG, "Stop Amnezia activity")
doUnbindService()
+ QtAndroidController.onServiceDisconnected()
super.onStop()
}
@@ -269,10 +284,12 @@ class AmneziaActivity : QtActivity() {
@MainThread
private fun doBindService() {
Log.d(TAG, "Bind service")
- Intent(this, AmneziaVpnService::class.java).also {
- bindService(it, serviceConnection, BIND_ABOVE_CLIENT and BIND_AUTO_CREATE)
+ vpnProto?.let { proto ->
+ Intent(this, proto.serviceClass).also {
+ bindService(it, serviceConnection, BIND_ABOVE_CLIENT and BIND_AUTO_CREATE)
+ }
+ isInBoundState = true
}
- isInBoundState = true
}
@MainThread
@@ -280,7 +297,6 @@ class AmneziaActivity : QtActivity() {
if (isInBoundState) {
Log.d(TAG, "Unbind service")
isWaitingStatus = true
- QtAndroidController.onServiceDisconnected()
isServiceConnected = false
vpnServiceMessenger.send(Action.UNREGISTER_CLIENT, activityMessenger)
vpnServiceMessenger.reset()
@@ -365,13 +381,31 @@ class AmneziaActivity : QtActivity() {
@MainThread
private fun startVpn(vpnConfig: String) {
- if (isServiceConnected) {
- connectToVpn(vpnConfig)
- } else {
+ getVpnProto(vpnConfig)?.let { proto ->
+ Log.d(TAG, "Proto from config: $proto, current proto: $vpnProto")
+ if (isServiceConnected) {
+ if (proto == vpnProto) {
+ connectToVpn(vpnConfig)
+ return
+ }
+ doUnbindService()
+ }
+ vpnProto = proto
isWaitingStatus = false
- startVpnService(vpnConfig)
+ startVpnService(vpnConfig, proto)
doBindService()
- }
+ } ?: QtAndroidController.onServiceError()
+ }
+
+ private fun getVpnProto(vpnConfig: String): VpnProto? = try {
+ require(vpnConfig.isNotBlank()) { "Blank VPN config" }
+ VpnProto.get(JSONObject(vpnConfig).getString("protocol"))
+ } catch (e: JSONException) {
+ Log.e(TAG, "Invalid VPN config json format: ${e.message}")
+ null
+ } catch (e: IllegalArgumentException) {
+ Log.e(TAG, "Protocol not found: ${e.message}")
+ null
}
private fun connectToVpn(vpnConfig: String) {
@@ -383,15 +417,15 @@ class AmneziaActivity : QtActivity() {
}
}
- private fun startVpnService(vpnConfig: String) {
- Log.d(TAG, "Start VPN service")
- Intent(this, AmneziaVpnService::class.java).apply {
+ private fun startVpnService(vpnConfig: String, proto: VpnProto) {
+ Log.d(TAG, "Start VPN service: $proto")
+ Intent(this, proto.serviceClass).apply {
putExtra(MSG_VPN_CONFIG, vpnConfig)
}.also {
try {
ContextCompat.startForegroundService(this, it)
} catch (e: SecurityException) {
- Log.e(TAG, "Failed to start AmneziaVpnService: $e")
+ Log.e(TAG, "Failed to start ${proto.serviceClass.simpleName}: $e")
QtAndroidController.onServiceError()
}
}
diff --git a/client/android/src/org/amnezia/vpn/AmneziaTileService.kt b/client/android/src/org/amnezia/vpn/AmneziaTileService.kt
index 1d13feac..32d5710d 100644
--- a/client/android/src/org/amnezia/vpn/AmneziaTileService.kt
+++ b/client/android/src/org/amnezia/vpn/AmneziaTileService.kt
@@ -39,6 +39,9 @@ class AmneziaTileService : TileService() {
@Volatile
private var isServiceConnected = false
+
+ @Volatile
+ private var vpnProto: VpnProto? = null
private var isInBoundState = false
@Volatile
private var isVpnConfigExists = false
@@ -94,16 +97,21 @@ class AmneziaTileService : TileService() {
override fun onStartListening() {
super.onStartListening()
- Log.d(TAG, "Start listening")
- if (AmneziaVpnService.isRunning(applicationContext)) {
- Log.d(TAG, "Vpn service is running")
- doBindService()
- } else {
- Log.d(TAG, "Vpn service is not running")
- isServiceConnected = false
- updateVpnState(DISCONNECTED)
+ scope.launch {
+ Log.d(TAG, "Start listening")
+ vpnProto = VpnStateStore.getVpnState().vpnProto
+ vpnProto.also { proto ->
+ if (proto != null && AmneziaVpnService.isRunning(applicationContext, proto.processName)) {
+ Log.d(TAG, "Vpn service is running")
+ doBindService()
+ } else {
+ Log.d(TAG, "Vpn service is not running")
+ isServiceConnected = false
+ updateVpnState(DISCONNECTED)
+ }
+ }
+ vpnStateListeningJob = launchVpnStateListening()
}
- vpnStateListeningJob = launchVpnStateListening()
}
override fun onStopListening() {
@@ -124,7 +132,7 @@ class AmneziaTileService : TileService() {
}
private fun onClickInternal() {
- if (isVpnConfigExists) {
+ if (isVpnConfigExists && vpnProto != null) {
Log.d(TAG, "Change VPN state")
if (qsTile.state == Tile.STATE_INACTIVE) {
Log.d(TAG, "Start VPN")
@@ -147,10 +155,12 @@ class AmneziaTileService : TileService() {
private fun doBindService() {
Log.d(TAG, "Bind service")
- Intent(this, AmneziaVpnService::class.java).also {
- bindService(it, serviceConnection, BIND_ABOVE_CLIENT)
+ vpnProto?.let { proto ->
+ Intent(this, proto.serviceClass).also {
+ bindService(it, serviceConnection, BIND_ABOVE_CLIENT)
+ }
+ isInBoundState = true
}
- isInBoundState = true
}
private fun doUnbindService() {
@@ -180,6 +190,7 @@ class AmneziaTileService : TileService() {
if (VpnService.prepare(applicationContext) != null) {
Intent(this, VpnRequestActivity::class.java).apply {
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
+ putExtra(EXTRA_PROTOCOL, vpnProto)
}.also {
startActivityAndCollapseCompat(it)
}
@@ -189,14 +200,16 @@ class AmneziaTileService : TileService() {
}
private fun startVpnService() {
- try {
- ContextCompat.startForegroundService(
- applicationContext,
- Intent(this, AmneziaVpnService::class.java)
- )
- } catch (e: SecurityException) {
- Log.e(TAG, "Failed to start AmneziaVpnService: $e")
- }
+ vpnProto?.let { proto ->
+ try {
+ ContextCompat.startForegroundService(
+ applicationContext,
+ Intent(this, proto.serviceClass)
+ )
+ } catch (e: SecurityException) {
+ Log.e(TAG, "Failed to start ${proto.serviceClass.simpleName}: $e")
+ }
+ } ?: Log.e(TAG, "Failed to start vpn service: vpnProto is null")
}
private fun connectToVpn() = vpnServiceMessenger.send(Action.CONNECT)
@@ -220,11 +233,8 @@ class AmneziaTileService : TileService() {
}
}
- private fun updateVpnState(state: ProtocolState) {
- scope.launch {
- VpnStateStore.store { it.copy(protocolState = state) }
- }
- }
+ private fun updateVpnState(state: ProtocolState) =
+ scope.launch { VpnStateStore.store { it.copy(protocolState = state) } }
private fun launchVpnStateListening() =
scope.launch { VpnStateStore.dataFlow().collectLatest(::updateTile) }
@@ -232,9 +242,10 @@ class AmneziaTileService : TileService() {
private fun updateTile(vpnState: VpnState) {
Log.d(TAG, "Update tile: $vpnState")
isVpnConfigExists = vpnState.serverName != null
+ vpnProto = vpnState.vpnProto
val tile = qsTile ?: return
tile.apply {
- label = vpnState.serverName ?: DEFAULT_TILE_LABEL
+ label = (vpnState.serverName ?: DEFAULT_TILE_LABEL) + (vpnProto?.let { " ${it.label}" } ?: "")
when (val protocolState = vpnState.protocolState) {
CONNECTED -> {
state = Tile.STATE_ACTIVE
diff --git a/client/android/src/org/amnezia/vpn/AmneziaVpnService.kt b/client/android/src/org/amnezia/vpn/AmneziaVpnService.kt
index 89c53481..b30f1503 100644
--- a/client/android/src/org/amnezia/vpn/AmneziaVpnService.kt
+++ b/client/android/src/org/amnezia/vpn/AmneziaVpnService.kt
@@ -1,5 +1,6 @@
package org.amnezia.vpn
+import android.annotation.SuppressLint
import android.app.ActivityManager
import android.app.ActivityManager.RunningAppProcessInfo.IMPORTANCE_FOREGROUND_SERVICE
import android.app.NotificationManager
@@ -39,7 +40,6 @@ import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withTimeout
import org.amnezia.vpn.protocol.BadConfigException
import org.amnezia.vpn.protocol.LoadLibraryException
-import org.amnezia.vpn.protocol.Protocol
import org.amnezia.vpn.protocol.ProtocolState.CONNECTED
import org.amnezia.vpn.protocol.ProtocolState.CONNECTING
import org.amnezia.vpn.protocol.ProtocolState.DISCONNECTED
@@ -48,11 +48,7 @@ import org.amnezia.vpn.protocol.ProtocolState.RECONNECTING
import org.amnezia.vpn.protocol.ProtocolState.UNKNOWN
import org.amnezia.vpn.protocol.VpnException
import org.amnezia.vpn.protocol.VpnStartException
-import org.amnezia.vpn.protocol.awg.Awg
-import org.amnezia.vpn.protocol.cloak.Cloak
-import org.amnezia.vpn.protocol.openvpn.OpenVpn
import org.amnezia.vpn.protocol.putStatus
-import org.amnezia.vpn.protocol.wireguard.Wireguard
import org.amnezia.vpn.util.Log
import org.amnezia.vpn.util.Prefs
import org.amnezia.vpn.util.net.NetworkState
@@ -63,6 +59,7 @@ import org.json.JSONObject
private const val TAG = "AmneziaVpnService"
const val ACTION_DISCONNECT = "org.amnezia.vpn.action.disconnect"
+const val ACTION_CONNECT = "org.amnezia.vpn.action.connect"
const val MSG_VPN_CONFIG = "VPN_CONFIG"
const val MSG_ERROR = "ERROR"
@@ -73,19 +70,18 @@ const val AFTER_PERMISSION_CHECK = "AFTER_PERMISSION_CHECK"
private const val PREFS_CONFIG_KEY = "LAST_CONF"
private const val PREFS_SERVER_NAME = "LAST_SERVER_NAME"
private const val PREFS_SERVER_INDEX = "LAST_SERVER_INDEX"
-private const val PROCESS_NAME = "org.amnezia.vpn:amneziaVpnService"
// private const val STATISTICS_SENDING_TIMEOUT = 1000L
private const val TRAFFIC_STATS_UPDATE_TIMEOUT = 1000L
private const val DISCONNECT_TIMEOUT = 5000L
private const val STOP_SERVICE_TIMEOUT = 5000L
-class AmneziaVpnService : VpnService() {
+@SuppressLint("Registered")
+open class AmneziaVpnService : VpnService() {
private lateinit var mainScope: CoroutineScope
private lateinit var connectionScope: CoroutineScope
private var isServiceBound = false
- private var protocol: Protocol? = null
- private val protocolCache = mutableMapOf()
+ private var vpnProto: VpnProto? = null
private var protocolState = MutableStateFlow(UNKNOWN)
private var serverName: String? = null
private var serverIndex: Int = -1
@@ -105,7 +101,7 @@ class AmneziaVpnService : VpnService() {
// private var statisticsSendingJob: Job? = null
private lateinit var networkState: NetworkState
private lateinit var trafficStats: TrafficStats
- private var disconnectReceiver: BroadcastReceiver? = null
+ private var controlReceiver: BroadcastReceiver? = null
private var notificationStateReceiver: BroadcastReceiver? = null
private var screenOnReceiver: BroadcastReceiver? = null
private var screenOffReceiver: BroadcastReceiver? = null
@@ -116,7 +112,6 @@ class AmneziaVpnService : VpnService() {
private val connectionExceptionHandler = CoroutineExceptionHandler { _, e ->
protocolState.value = DISCONNECTED
- protocol = null
when (e) {
is IllegalArgumentException,
is VpnStartException,
@@ -227,7 +222,8 @@ class AmneziaVpnService : VpnService() {
connect(intent?.getStringExtra(MSG_VPN_CONFIG))
}
ServiceCompat.startForeground(
- this, NOTIFICATION_ID, serviceNotification.buildNotification(serverName, protocolState.value),
+ this, NOTIFICATION_ID,
+ serviceNotification.buildNotification(serverName, vpnProto?.label, protocolState.value),
foregroundServiceTypeCompat
)
return START_REDELIVER_INTENT
@@ -292,9 +288,17 @@ class AmneziaVpnService : VpnService() {
private fun registerBroadcastReceivers() {
Log.d(TAG, "Register broadcast receivers")
- disconnectReceiver = registerBroadcastReceiver(ACTION_DISCONNECT, ContextCompat.RECEIVER_NOT_EXPORTED) {
- Log.d(TAG, "Broadcast request received: $ACTION_DISCONNECT")
- disconnect()
+ controlReceiver = registerBroadcastReceiver(
+ arrayOf(ACTION_CONNECT, ACTION_DISCONNECT), ContextCompat.RECEIVER_NOT_EXPORTED
+ ) {
+ it?.action?.let { action ->
+ Log.d(TAG, "Broadcast request received: $action")
+ when (action) {
+ ACTION_CONNECT -> connect()
+ ACTION_DISCONNECT -> disconnect()
+ else -> Log.w(TAG, "Unknown action received: $action")
+ }
+ }
}
notificationStateReceiver = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
@@ -340,10 +344,10 @@ class AmneziaVpnService : VpnService() {
private fun unregisterBroadcastReceivers() {
Log.d(TAG, "Unregister broadcast receivers")
- unregisterBroadcastReceiver(disconnectReceiver)
+ unregisterBroadcastReceiver(controlReceiver)
unregisterBroadcastReceiver(notificationStateReceiver)
unregisterScreenStateBroadcastReceivers()
- disconnectReceiver = null
+ controlReceiver = null
notificationStateReceiver = null
}
@@ -356,7 +360,7 @@ class AmneziaVpnService : VpnService() {
protocolState.drop(1).collect { protocolState ->
Log.d(TAG, "Protocol state changed: $protocolState")
- serviceNotification.updateNotification(serverName, protocolState)
+ serviceNotification.updateNotification(serverName, vpnProto?.label, protocolState)
clientMessengers.send {
ServiceEvent.STATUS_CHANGED.packToMessage {
@@ -364,7 +368,7 @@ class AmneziaVpnService : VpnService() {
}
}
- VpnStateStore.store { VpnState(protocolState, serverName, serverIndex) }
+ VpnStateStore.store { VpnState(protocolState, serverName, serverIndex, vpnProto) }
when (protocolState) {
CONNECTED -> {
@@ -421,7 +425,7 @@ class AmneziaVpnService : VpnService() {
@MainThread
private fun enableNotification() {
registerScreenStateBroadcastReceivers()
- serviceNotification.updateNotification(serverName, protocolState.value)
+ serviceNotification.updateNotification(serverName, vpnProto?.label, protocolState.value)
launchTrafficStatsUpdate()
}
@@ -484,8 +488,6 @@ class AmneziaVpnService : VpnService() {
Log.d(TAG, "Start VPN connection")
- protocolState.value = CONNECTING
-
val config = parseConfigToJson(vpnConfig)
saveServerData(config)
if (config == null) {
@@ -494,6 +496,16 @@ class AmneziaVpnService : VpnService() {
return
}
+ try {
+ vpnProto = VpnProto.get(config.getString("protocol"))
+ } catch (e: Exception) {
+ onError("Invalid VPN config: ${e.message}")
+ protocolState.value = DISCONNECTED
+ return
+ }
+
+ protocolState.value = CONNECTING
+
if (!checkPermission()) {
protocolState.value = DISCONNECTED
return
@@ -503,8 +515,10 @@ class AmneziaVpnService : VpnService() {
disconnectionJob?.join()
disconnectionJob = null
- protocol = getProtocol(config.getString("protocol"))
- protocol?.startVpn(config, Builder(), ::protect)
+ vpnProto?.protocol?.let { protocol ->
+ protocol.initialize(applicationContext, protocolState, ::onError)
+ protocol.startVpn(config, Builder(), ::protect)
+ }
}
}
@@ -520,8 +534,8 @@ class AmneziaVpnService : VpnService() {
connectionJob?.join()
connectionJob = null
- protocol?.stopVpn()
- protocol = null
+ vpnProto?.protocol?.stopVpn()
+
try {
withTimeout(DISCONNECT_TIMEOUT) {
// waiting for disconnect state
@@ -543,22 +557,10 @@ class AmneziaVpnService : VpnService() {
protocolState.value = RECONNECTING
connectionJob = connectionScope.launch {
- protocol?.reconnectVpn(Builder())
+ vpnProto?.protocol?.reconnectVpn(Builder())
}
}
- @MainThread
- private fun getProtocol(protocolName: String): Protocol =
- protocolCache[protocolName]
- ?: when (protocolName) {
- "wireguard" -> Wireguard()
- "awg" -> Awg()
- "openvpn" -> OpenVpn()
- "cloak" -> Cloak()
- else -> throw IllegalArgumentException("Protocol '$protocolName' not found")
- }.apply { initialize(applicationContext, protocolState, ::onError) }
- .also { protocolCache[protocolName] = it }
-
/**
* Utils methods
*/
@@ -603,6 +605,7 @@ class AmneziaVpnService : VpnService() {
if (prepare(applicationContext) != null) {
Intent(this, VpnRequestActivity::class.java).apply {
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
+ putExtra(EXTRA_PROTOCOL, vpnProto)
}.also {
startActivity(it)
}
@@ -612,9 +615,9 @@ class AmneziaVpnService : VpnService() {
}
companion object {
- fun isRunning(context: Context): Boolean =
+ fun isRunning(context: Context, processName: String): Boolean =
context.getSystemService()!!.runningAppProcesses.any {
- it.processName == PROCESS_NAME && it.importance <= IMPORTANCE_FOREGROUND_SERVICE
+ it.processName == processName && it.importance <= IMPORTANCE_FOREGROUND_SERVICE
}
}
}
diff --git a/client/android/src/org/amnezia/vpn/AwgService.kt b/client/android/src/org/amnezia/vpn/AwgService.kt
new file mode 100644
index 00000000..ebdbe543
--- /dev/null
+++ b/client/android/src/org/amnezia/vpn/AwgService.kt
@@ -0,0 +1,3 @@
+package org.amnezia.vpn
+
+class AwgService : AmneziaVpnService()
diff --git a/client/android/src/org/amnezia/vpn/OpenVpnService.kt b/client/android/src/org/amnezia/vpn/OpenVpnService.kt
new file mode 100644
index 00000000..72c8bab1
--- /dev/null
+++ b/client/android/src/org/amnezia/vpn/OpenVpnService.kt
@@ -0,0 +1,3 @@
+package org.amnezia.vpn
+
+class OpenVpnService : AmneziaVpnService()
diff --git a/client/android/src/org/amnezia/vpn/ServiceNotification.kt b/client/android/src/org/amnezia/vpn/ServiceNotification.kt
index efdd04d3..f4707731 100644
--- a/client/android/src/org/amnezia/vpn/ServiceNotification.kt
+++ b/client/android/src/org/amnezia/vpn/ServiceNotification.kt
@@ -59,14 +59,14 @@ class ServiceNotification(private val context: Context) {
formatSpeedString(rxString, txString)
}
- fun buildNotification(serverName: String?, state: ProtocolState): Notification {
+ fun buildNotification(serverName: String?, protocol: String?, state: ProtocolState): Notification {
val speedString = if (state == CONNECTED) zeroSpeed else null
Log.d(TAG, "Build notification: $serverName, $state")
return notificationBuilder
.setSmallIcon(R.drawable.ic_amnezia_round)
- .setContentTitle(serverName ?: "AmneziaVPN")
+ .setContentTitle((serverName ?: "AmneziaVPN") + (protocol?.let { " $it" } ?: ""))
.setContentText(context.getString(state))
.setSubText(speedString)
.setWhen(System.currentTimeMillis())
@@ -96,10 +96,10 @@ class ServiceNotification(private val context: Context) {
}
@SuppressLint("MissingPermission")
- fun updateNotification(serverName: String?, state: ProtocolState) {
+ fun updateNotification(serverName: String?, protocol: String?, state: ProtocolState) {
if (context.isNotificationPermissionGranted()) {
Log.d(TAG, "Update notification: $serverName, $state")
- notificationManager.notify(NOTIFICATION_ID, buildNotification(serverName, state))
+ notificationManager.notify(NOTIFICATION_ID, buildNotification(serverName, protocol, state))
}
}
@@ -125,7 +125,7 @@ class ServiceNotification(private val context: Context) {
context,
DISCONNECT_REQUEST_CODE,
Intent(ACTION_DISCONNECT).apply {
- setPackage("org.amnezia.vpn")
+ setPackage(context.packageName)
},
PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
)
@@ -135,10 +135,12 @@ class ServiceNotification(private val context: Context) {
DISCONNECTED -> {
Action(
0, context.getString(R.string.connect),
- createServicePendingIntent(
+ PendingIntent.getBroadcast(
context,
CONNECT_REQUEST_CODE,
- Intent(context, AmneziaVpnService::class.java),
+ Intent(ACTION_CONNECT).apply {
+ setPackage(context.packageName)
+ },
PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
)
)
@@ -148,13 +150,6 @@ class ServiceNotification(private val context: Context) {
}
}
- private val createServicePendingIntent: (Context, Int, Intent, Int) -> PendingIntent =
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
- PendingIntent::getForegroundService
- } else {
- PendingIntent::getService
- }
-
companion object {
fun createNotificationChannel(context: Context) {
with(NotificationManagerCompat.from(context)) {
diff --git a/client/android/src/org/amnezia/vpn/VpnProto.kt b/client/android/src/org/amnezia/vpn/VpnProto.kt
new file mode 100644
index 00000000..508ce226
--- /dev/null
+++ b/client/android/src/org/amnezia/vpn/VpnProto.kt
@@ -0,0 +1,67 @@
+package org.amnezia.vpn
+
+import org.amnezia.vpn.protocol.Protocol
+import org.amnezia.vpn.protocol.awg.Awg
+import org.amnezia.vpn.protocol.cloak.Cloak
+import org.amnezia.vpn.protocol.openvpn.OpenVpn
+import org.amnezia.vpn.protocol.wireguard.Wireguard
+import org.amnezia.vpn.protocol.xray.Xray
+
+enum class VpnProto(
+ val label: String,
+ val processName: String,
+ val serviceClass: Class
+) {
+ WIREGUARD(
+ "WireGuard",
+ "org.amnezia.vpn:amneziaAwgService",
+ AwgService::class.java
+ ) {
+ override fun createProtocol(): Protocol = Wireguard()
+ },
+
+ AWG(
+ "AmneziaWG",
+ "org.amnezia.vpn:amneziaAwgService",
+ AwgService::class.java
+ ) {
+ override fun createProtocol(): Protocol = Awg()
+ },
+
+ OPENVPN(
+ "OpenVPN",
+ "org.amnezia.vpn:amneziaOpenVpnService",
+ OpenVpnService::class.java
+ ) {
+ override fun createProtocol(): Protocol = OpenVpn()
+ },
+
+ CLOAK(
+ "Cloak",
+ "org.amnezia.vpn:amneziaOpenVpnService",
+ OpenVpnService::class.java
+ ) {
+ override fun createProtocol(): Protocol = Cloak()
+ },
+
+ XRAY(
+ "XRay",
+ "org.amnezia.vpn:amneziaXrayService",
+ XrayService::class.java
+ ) {
+ override fun createProtocol(): Protocol = Xray()
+ };
+
+ private var _protocol: Protocol? = null
+ val protocol: Protocol
+ get() {
+ if (_protocol == null) _protocol = createProtocol()
+ return _protocol ?: throw AssertionError("Set to null by another thread")
+ }
+
+ protected abstract fun createProtocol(): Protocol
+
+ companion object {
+ fun get(protocolName: String): VpnProto = VpnProto.valueOf(protocolName.uppercase())
+ }
+}
\ No newline at end of file
diff --git a/client/android/src/org/amnezia/vpn/VpnRequestActivity.kt b/client/android/src/org/amnezia/vpn/VpnRequestActivity.kt
index 12d3fb3d..c24f5a19 100644
--- a/client/android/src/org/amnezia/vpn/VpnRequestActivity.kt
+++ b/client/android/src/org/amnezia/vpn/VpnRequestActivity.kt
@@ -7,6 +7,7 @@ import android.content.Intent
import android.content.res.Configuration.UI_MODE_NIGHT_MASK
import android.content.res.Configuration.UI_MODE_NIGHT_YES
import android.net.VpnService
+import android.os.Build
import android.os.Bundle
import android.provider.Settings
import android.widget.Toast
@@ -18,9 +19,11 @@ import androidx.core.content.getSystemService
import org.amnezia.vpn.util.Log
private const val TAG = "VpnRequestActivity"
+const val EXTRA_PROTOCOL = "PROTOCOL"
class VpnRequestActivity : ComponentActivity() {
+ private var vpnProto: VpnProto? = null
private var userPresentReceiver: BroadcastReceiver? = null
private val requestLauncher =
registerForActivityResult(StartActivityForResult(), ::checkRequestResult)
@@ -28,6 +31,12 @@ class VpnRequestActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
Log.d(TAG, "Start request activity")
+ vpnProto = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
+ intent.extras?.getSerializable(EXTRA_PROTOCOL, VpnProto::class.java)
+ } else {
+ @Suppress("DEPRECATION")
+ intent.extras?.getSerializable(EXTRA_PROTOCOL) as VpnProto
+ }
val requestIntent = VpnService.prepare(applicationContext)
if (requestIntent != null) {
if (getSystemService()!!.isKeyguardLocked) {
@@ -66,10 +75,18 @@ class VpnRequestActivity : ComponentActivity() {
private fun onPermissionGranted() {
Toast.makeText(this, resources.getString(R.string.vpnGranted), Toast.LENGTH_LONG).show()
- Intent(applicationContext, AmneziaVpnService::class.java).apply {
- putExtra(AFTER_PERMISSION_CHECK, true)
- }.also {
- ContextCompat.startForegroundService(this, it)
+ vpnProto?.let { proto ->
+ Intent(applicationContext, proto.serviceClass).apply {
+ putExtra(AFTER_PERMISSION_CHECK, true)
+ }.also {
+ ContextCompat.startForegroundService(this, it)
+ }
+ } ?: run {
+ Intent(this, AmneziaActivity::class.java).apply {
+ addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
+ }.also {
+ startActivity(it)
+ }
}
}
diff --git a/client/android/src/org/amnezia/vpn/VpnState.kt b/client/android/src/org/amnezia/vpn/VpnState.kt
index fbc4ef59..94039dc1 100644
--- a/client/android/src/org/amnezia/vpn/VpnState.kt
+++ b/client/android/src/org/amnezia/vpn/VpnState.kt
@@ -1,19 +1,22 @@
package org.amnezia.vpn
import android.app.Application
+import androidx.datastore.core.CorruptionException
import androidx.datastore.core.MultiProcessDataStoreFactory
import androidx.datastore.core.Serializer
+import androidx.datastore.core.handlers.ReplaceFileCorruptionHandler
import androidx.datastore.dataStoreFile
-import java.io.ByteArrayInputStream
-import java.io.ByteArrayOutputStream
import java.io.InputStream
-import java.io.ObjectInputStream
-import java.io.ObjectOutputStream
import java.io.OutputStream
-import java.io.Serializable
-import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.Flow
-import kotlinx.coroutines.withContext
+import kotlinx.coroutines.flow.catch
+import kotlinx.coroutines.flow.firstOrNull
+import kotlinx.serialization.ExperimentalSerializationApi
+import kotlinx.serialization.Serializable
+import kotlinx.serialization.SerializationException
+import kotlinx.serialization.decodeFromByteArray
+import kotlinx.serialization.encodeToByteArray
+import kotlinx.serialization.protobuf.ProtoBuf
import org.amnezia.vpn.protocol.ProtocolState
import org.amnezia.vpn.protocol.ProtocolState.DISCONNECTED
import org.amnezia.vpn.util.Log
@@ -21,13 +24,14 @@ import org.amnezia.vpn.util.Log
private const val TAG = "VpnState"
private const val STORE_FILE_NAME = "vpnState"
+@Serializable
data class VpnState(
val protocolState: ProtocolState,
val serverName: String? = null,
- val serverIndex: Int = -1
-) : Serializable {
+ val serverIndex: Int = -1,
+ val vpnProto: VpnProto? = null
+) {
companion object {
- private const val serialVersionUID: Long = -1760654961004181606
val defaultState: VpnState = VpnState(DISCONNECTED)
}
}
@@ -37,7 +41,11 @@ object VpnStateStore {
private val dataStore = MultiProcessDataStoreFactory.create(
serializer = VpnStateSerializer(),
- produceFile = { app.dataStoreFile(STORE_FILE_NAME) }
+ produceFile = { app.dataStoreFile(STORE_FILE_NAME) },
+ corruptionHandler = ReplaceFileCorruptionHandler { e ->
+ Log.e(TAG, "VpnState DataStore corrupted: $e")
+ VpnState.defaultState
+ }
)
fun init(app: Application) {
@@ -45,36 +53,36 @@ object VpnStateStore {
this.app = app
}
- fun dataFlow(): Flow = dataStore.data
+ fun dataFlow(): Flow = dataStore.data.catch { e ->
+ Log.e(TAG, "Failed to read VpnState from store: ${e.message}")
+ emit(VpnState.defaultState)
+ }
+
+ suspend fun getVpnState(): VpnState = dataFlow().firstOrNull() ?: VpnState.defaultState
suspend fun store(f: (vpnState: VpnState) -> VpnState) {
try {
dataStore.updateData(f)
- } catch (e : Exception) {
+ } catch (e: Exception) {
Log.e(TAG, "Failed to store VpnState: $e")
+ Log.w(TAG, "Remove DataStore file")
+ app.dataStoreFile(STORE_FILE_NAME).delete()
}
}
}
+@OptIn(ExperimentalSerializationApi::class)
private class VpnStateSerializer : Serializer {
override val defaultValue: VpnState = VpnState.defaultState
- override suspend fun readFrom(input: InputStream): VpnState {
- return withContext(Dispatchers.IO) {
- val bios = ByteArrayInputStream(input.readBytes())
- ObjectInputStream(bios).use {
- it.readObject() as VpnState
- }
- }
+ override suspend fun readFrom(input: InputStream): VpnState = try {
+ ProtoBuf.decodeFromByteArray(input.readBytes())
+ } catch (e: SerializationException) {
+ Log.e(TAG, "Failed to deserialize data: $e")
+ throw CorruptionException("Failed to deserialize data", e)
}
- override suspend fun writeTo(t: VpnState, output: OutputStream) {
- withContext(Dispatchers.IO) {
- val baos = ByteArrayOutputStream()
- ObjectOutputStream(baos).use {
- it.writeObject(t)
- }
- output.write(baos.toByteArray())
- }
- }
+ @Suppress("BlockingMethodInNonBlockingContext")
+ override suspend fun writeTo(t: VpnState, output: OutputStream) =
+ output.write(ProtoBuf.encodeToByteArray(t))
}
diff --git a/client/android/src/org/amnezia/vpn/XrayService.kt b/client/android/src/org/amnezia/vpn/XrayService.kt
new file mode 100644
index 00000000..2efcb4c9
--- /dev/null
+++ b/client/android/src/org/amnezia/vpn/XrayService.kt
@@ -0,0 +1,3 @@
+package org.amnezia.vpn
+
+class XrayService : AmneziaVpnService()
diff --git a/client/android/wireguard/src/main/kotlin/org/amnezia/vpn/protocol/wireguard/Wireguard.kt b/client/android/wireguard/src/main/kotlin/org/amnezia/vpn/protocol/wireguard/Wireguard.kt
index 09482918..690510eb 100644
--- a/client/android/wireguard/src/main/kotlin/org/amnezia/vpn/protocol/wireguard/Wireguard.kt
+++ b/client/android/wireguard/src/main/kotlin/org/amnezia/vpn/protocol/wireguard/Wireguard.kt
@@ -1,12 +1,9 @@
package org.amnezia.vpn.protocol.wireguard
-import android.content.Context
import android.net.VpnService.Builder
import java.util.TreeMap
-import kotlinx.coroutines.flow.MutableStateFlow
import org.amnezia.awg.GoBackend
import org.amnezia.vpn.protocol.Protocol
-import org.amnezia.vpn.protocol.ProtocolState
import org.amnezia.vpn.protocol.ProtocolState.CONNECTED
import org.amnezia.vpn.protocol.ProtocolState.DISCONNECTED
import org.amnezia.vpn.protocol.Statistics
@@ -78,9 +75,8 @@ open class Wireguard : Protocol() {
}
}
- override fun initialize(context: Context, state: MutableStateFlow, onError: (String) -> Unit) {
- super.initialize(context, state, onError)
- loadSharedLibrary(context, "wg-go")
+ override fun internalInit() {
+ if (!isInitialized) loadSharedLibrary(context, "wg-go")
}
override fun startVpn(config: JSONObject, vpnBuilder: Builder, protect: (Int) -> Boolean) {
diff --git a/client/android/xray/build.gradle.kts b/client/android/xray/build.gradle.kts
new file mode 100644
index 00000000..f21a12a3
--- /dev/null
+++ b/client/android/xray/build.gradle.kts
@@ -0,0 +1,19 @@
+plugins {
+ id(libs.plugins.android.library.get().pluginId)
+ id(libs.plugins.kotlin.android.get().pluginId)
+}
+
+kotlin {
+ jvmToolchain(17)
+}
+
+android {
+ namespace = "org.amnezia.vpn.protocol.xray"
+}
+
+dependencies {
+ compileOnly(project(":utils"))
+ compileOnly(project(":protocolApi"))
+ implementation(project(":xray:libXray"))
+ implementation(libs.kotlinx.coroutines)
+}
diff --git a/client/android/xray/libXray/build.gradle.kts b/client/android/xray/libXray/build.gradle.kts
new file mode 100644
index 00000000..99b9db36
--- /dev/null
+++ b/client/android/xray/libXray/build.gradle.kts
@@ -0,0 +1,6 @@
+@file:Suppress("UnstableApiUsage")
+
+configurations {
+ maybeCreate("default")
+}
+artifacts.add("default", file("libxray.aar"))
diff --git a/client/android/xray/src/main/kotlin/Xray.kt b/client/android/xray/src/main/kotlin/Xray.kt
new file mode 100644
index 00000000..b4d0b51f
--- /dev/null
+++ b/client/android/xray/src/main/kotlin/Xray.kt
@@ -0,0 +1,237 @@
+package org.amnezia.vpn.protocol.xray
+
+import android.content.Context
+import android.net.VpnService.Builder
+import java.io.File
+import java.io.IOException
+import go.Seq
+import org.amnezia.vpn.protocol.Protocol
+import org.amnezia.vpn.protocol.ProtocolState.CONNECTED
+import org.amnezia.vpn.protocol.ProtocolState.DISCONNECTED
+import org.amnezia.vpn.protocol.Statistics
+import org.amnezia.vpn.protocol.VpnStartException
+import org.amnezia.vpn.protocol.xray.libXray.DialerController
+import org.amnezia.vpn.protocol.xray.libXray.LibXray
+import org.amnezia.vpn.protocol.xray.libXray.Logger
+import org.amnezia.vpn.protocol.xray.libXray.Tun2SocksConfig
+import org.amnezia.vpn.util.Log
+import org.amnezia.vpn.util.net.InetNetwork
+import org.amnezia.vpn.util.net.parseInetAddress
+import org.json.JSONObject
+
+/**
+ * Config example:
+ * {
+ * "appSplitTunnelType": 0,
+ * "config_version": 0,
+ * "description": "Server 1",
+ * "dns1": "1.1.1.1",
+ * "dns2": "1.0.0.1",
+ * "hostName": "100.100.100.0",
+ * "protocol": "xray",
+ * "splitTunnelApps": [],
+ * "splitTunnelSites": [],
+ * "splitTunnelType": 0,
+ * "xray_config_data": {
+ * "inbounds": [
+ * {
+ * "listen": "127.0.0.1",
+ * "port": 8080,
+ * "protocol": "socks",
+ * "settings": {
+ * "udp": true
+ * }
+ * }
+ * ],
+ * "log": {
+ * "loglevel": "error"
+ * },
+ * "outbounds": [
+ * {
+ * "protocol": "vless",
+ * "settings": {
+ * "vnext": [
+ * {
+ * "address": "100.100.100.0",
+ * "port": 443,
+ * "users": [
+ * {
+ * "encryption": "none",
+ * "flow": "xtls-rprx-vision",
+ * "id": "id"
+ * }
+ * ]
+ * }
+ * ]
+ * },
+ * "streamSettings": {
+ * "network": "tcp",
+ * "realitySettings": {
+ * "fingerprint": "chrome",
+ * "publicKey": "publicKey",
+ * "serverName": "google.com",
+ * "shortId": "id",
+ * "spiderX": ""
+ * },
+ * "security": "reality"
+ * }
+ * }
+ * ]
+ * }
+ * }
+ *
+ */
+
+private const val TAG = "Xray"
+private const val LIBXRAY_TAG = "libXray"
+
+class Xray : Protocol() {
+
+ private var isRunning: Boolean = false
+ override val statistics: Statistics = Statistics.EMPTY_STATISTICS
+
+ override fun internalInit() {
+ Seq.setContext(context)
+ if (!isInitialized) {
+ LibXray.initLogger(object : Logger {
+ override fun warning(s: String) = Log.w(LIBXRAY_TAG, s)
+
+ override fun error(s: String) = Log.e(LIBXRAY_TAG, s)
+
+ override fun write(msg: ByteArray): Long {
+ Log.w(LIBXRAY_TAG, String(msg))
+ return msg.size.toLong()
+ }
+ }).isNotNullOrBlank { err ->
+ Log.w(TAG, "Failed to initialize logger: $err")
+ }
+ }
+ }
+
+ override fun startVpn(config: JSONObject, vpnBuilder: Builder, protect: (Int) -> Boolean) {
+ if (isRunning) {
+ Log.w(TAG, "XRay already running")
+ return
+ }
+
+ val xrayJsonConfig = config.getJSONObject("xray_config_data")
+ val xrayConfig = parseConfig(config, xrayJsonConfig)
+
+ // for debug
+ // xrayJsonConfig.getJSONObject("log").put("loglevel", "debug")
+ xrayJsonConfig.getJSONObject("log").put("loglevel", "warning")
+ // disable access log
+ xrayJsonConfig.getJSONObject("log").put("access", "none")
+
+ // replace socks address
+ // (xrayJsonConfig.getJSONArray("inbounds")[0] as JSONObject).put("listen", "::1")
+
+ start(xrayConfig, xrayJsonConfig.toString(), vpnBuilder, protect)
+ state.value = CONNECTED
+ isRunning = true
+ }
+
+ private fun parseConfig(config: JSONObject, xrayJsonConfig: JSONObject): XrayConfig {
+ return XrayConfig.build {
+ addAddress(XrayConfig.DEFAULT_IPV4_ADDRESS)
+
+ config.optString("dns1").let {
+ if (it.isNotBlank()) addDnsServer(parseInetAddress(it))
+ }
+
+ config.optString("dns2").let {
+ if (it.isNotBlank()) addDnsServer(parseInetAddress(it))
+ }
+
+ addRoute(InetNetwork("0.0.0.0", 0))
+ addRoute(InetNetwork("2000::0", 3))
+ config.getString("hostName").let {
+ excludeRoute(InetNetwork(it, 32))
+ }
+
+ config.optString("mtu").let {
+ if (it.isNotBlank()) setMtu(it.toInt())
+ }
+
+ val socksConfig = xrayJsonConfig.getJSONArray("inbounds")[0] as JSONObject
+ socksConfig.getInt("port").let { setSocksPort(it) }
+
+ configSplitTunneling(config)
+ configAppSplitTunneling(config)
+ }
+ }
+
+ private fun start(config: XrayConfig, configJson: String, vpnBuilder: Builder, protect: (Int) -> Boolean) {
+ buildVpnInterface(config, vpnBuilder)
+
+ DialerController { protect(it.toInt()) }.also {
+ LibXray.registerDialerController(it).isNotNullOrBlank { err ->
+ throw VpnStartException("Failed to register dialer controller: $err")
+ }
+ LibXray.registerListenerController(it).isNotNullOrBlank { err ->
+ throw VpnStartException("Failed to register listener controller: $err")
+ }
+ }
+
+ vpnBuilder.establish().use { tunFd ->
+ if (tunFd == null) {
+ throw VpnStartException("Create VPN interface: permission not granted or revoked")
+ }
+ Log.d(TAG, "Run tun2Socks")
+ runTun2Socks(config, tunFd.detachFd())
+
+ Log.d(TAG, "Run XRay")
+ Log.i(TAG, "xray ${LibXray.xrayVersion()}")
+ val assetsPath = context.getDir("assets", Context.MODE_PRIVATE).absolutePath
+ LibXray.initXray(assetsPath)
+ val geoDir = File(assetsPath, "geo").absolutePath
+ val configPath = File(context.cacheDir, "config.json")
+ Log.d(TAG, "xray.location.asset: $geoDir")
+ Log.d(TAG, "config: $configPath")
+ try {
+ configPath.writeText(configJson)
+ } catch (e: IOException) {
+ LibXray.stopTun2Socks()
+ throw VpnStartException("Failed to write xray config: ${e.message}")
+ }
+ LibXray.runXray(geoDir, configPath.absolutePath, config.maxMemory).isNotNullOrBlank { err ->
+ LibXray.stopTun2Socks()
+ throw VpnStartException("Failed to start xray: $err")
+ }
+ }
+ }
+
+ override fun stopVpn() {
+ LibXray.stopXray().isNotNullOrBlank { err ->
+ Log.e(TAG, "Failed to stop XRay: $err")
+ }
+ LibXray.stopTun2Socks().isNotNullOrBlank { err ->
+ Log.e(TAG, "Failed to stop tun2Socks: $err")
+ }
+
+ isRunning = false
+ state.value = DISCONNECTED
+ }
+
+ override fun reconnectVpn(vpnBuilder: Builder) {
+ state.value = CONNECTED
+ }
+
+ private fun runTun2Socks(config: XrayConfig, fd: Int) {
+ val tun2SocksConfig = Tun2SocksConfig().apply {
+ mtu = config.mtu.toLong()
+ proxy = "socks5://127.0.0.1:${config.socksPort}"
+ device = "fd://$fd"
+ logLevel = "warning"
+ }
+ LibXray.startTun2Socks(tun2SocksConfig, fd.toLong()).isNotNullOrBlank { err ->
+ throw VpnStartException("Failed to start tun2socks: $err")
+ }
+ }
+}
+
+private fun String?.isNotNullOrBlank(block: (String) -> Unit) {
+ if (!this.isNullOrBlank()) {
+ block(this)
+ }
+}
diff --git a/client/android/xray/src/main/kotlin/XrayConfig.kt b/client/android/xray/src/main/kotlin/XrayConfig.kt
new file mode 100644
index 00000000..821a1c2f
--- /dev/null
+++ b/client/android/xray/src/main/kotlin/XrayConfig.kt
@@ -0,0 +1,42 @@
+package org.amnezia.vpn.protocol.xray
+
+import org.amnezia.vpn.protocol.ProtocolConfig
+import org.amnezia.vpn.util.net.InetNetwork
+
+private const val XRAY_DEFAULT_MTU = 1500
+private const val XRAY_DEFAULT_MAX_MEMORY: Long = 50 shl 20 // 50 MB
+
+class XrayConfig protected constructor(
+ protocolConfigBuilder: ProtocolConfig.Builder,
+ val socksPort: Int,
+ val maxMemory: Long,
+) : ProtocolConfig(protocolConfigBuilder) {
+
+ protected constructor(builder: Builder) : this(
+ builder,
+ builder.socksPort,
+ builder.maxMemory
+ )
+
+ class Builder : ProtocolConfig.Builder(false) {
+ internal var socksPort: Int = 0
+ private set
+
+ internal var maxMemory: Long = XRAY_DEFAULT_MAX_MEMORY
+ private set
+
+ override var mtu: Int = XRAY_DEFAULT_MTU
+
+ fun setSocksPort(port: Int) = apply { socksPort = port }
+
+ fun setMaxMemory(maxMemory: Long) = apply { this.maxMemory = maxMemory }
+
+ override fun build(): XrayConfig = configBuild().run { XrayConfig(this@Builder) }
+ }
+
+ companion object {
+ internal val DEFAULT_IPV4_ADDRESS: InetNetwork = InetNetwork("10.0.42.2", 30)
+
+ inline fun build(block: Builder.() -> Unit): XrayConfig = Builder().apply(block).build()
+ }
+}
diff --git a/client/cmake/android.cmake b/client/cmake/android.cmake
index c39642ff..13c357bd 100644
--- a/client/cmake/android.cmake
+++ b/client/cmake/android.cmake
@@ -52,3 +52,6 @@ foreach(abi IN ITEMS ${QT_ANDROID_ABIS})
${CMAKE_CURRENT_SOURCE_DIR}/3rd-prebuilt/3rd-prebuilt/libssh/android/${abi}/libssh.so
)
endforeach()
+
+file(COPY ${CMAKE_CURRENT_SOURCE_DIR}/3rd-prebuilt/3rd-prebuilt/xray/android/libxray.aar
+ DESTINATION ${CMAKE_CURRENT_SOURCE_DIR}/android/xray/libXray)
diff --git a/client/containers/containers_defs.cpp b/client/containers/containers_defs.cpp
index 3c2a3861..8276bc93 100644
--- a/client/containers/containers_defs.cpp
+++ b/client/containers/containers_defs.cpp
@@ -305,6 +305,7 @@ bool ContainerProps::isSupportedByCurrentPlatform(DockerContainer c)
case DockerContainer::ShadowSocks: return false;
case DockerContainer::Awg: return true;
case DockerContainer::Cloak: return true;
+ case DockerContainer::Xray: return true;
default: return false;
}