Merge branch 'dev' into trans

This commit is contained in:
StrikerRUS 2024-06-19 02:31:04 +03:00
commit f1271da527
62 changed files with 1308 additions and 174 deletions

View file

@ -2,7 +2,7 @@ cmake_minimum_required(VERSION 3.25.0 FATAL_ERROR)
set(PROJECT AmneziaVPN) set(PROJECT AmneziaVPN)
project(${PROJECT} VERSION 4.5.3.0 project(${PROJECT} VERSION 4.6.0.0
DESCRIPTION "AmneziaVPN" DESCRIPTION "AmneziaVPN"
HOMEPAGE_URL "https://amnezia.org/" HOMEPAGE_URL "https://amnezia.org/"
) )
@ -11,7 +11,7 @@ string(TIMESTAMP CURRENT_DATE "%Y-%m-%d")
set(RELEASE_DATE "${CURRENT_DATE}") set(RELEASE_DATE "${CURRENT_DATE}")
set(APP_MAJOR_VERSION ${CMAKE_PROJECT_VERSION_MAJOR}.${CMAKE_PROJECT_VERSION_MINOR}.${CMAKE_PROJECT_VERSION_PATCH}) 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") if(${CMAKE_SYSTEM_NAME} STREQUAL "Linux")
set(MZ_PLATFORM_NAME "linux") set(MZ_PLATFORM_NAME "linux")

@ -1 +1 @@
Subproject commit eb43e90f389745af6d7ca3be92a96e400ba6dc6c Subproject commit ea49bf8796afbc5bd70a0f98f4d99c9ea4792d80

View file

@ -351,6 +351,9 @@ void AmneziaApplication::initModels()
m_sftpConfigModel.reset(new SftpConfigModel(this)); m_sftpConfigModel.reset(new SftpConfigModel(this));
m_engine->rootContext()->setContextProperty("SftpConfigModel", m_sftpConfigModel.get()); m_engine->rootContext()->setContextProperty("SftpConfigModel", m_sftpConfigModel.get());
m_socks5ConfigModel.reset(new Socks5ProxyConfigModel(this));
m_engine->rootContext()->setContextProperty("Socks5ProxyConfigModel", m_socks5ConfigModel.get());
m_clientManagementModel.reset(new ClientManagementModel(m_settings, this)); m_clientManagementModel.reset(new ClientManagementModel(m_settings, this));
m_engine->rootContext()->setContextProperty("ClientManagementModel", m_clientManagementModel.get()); m_engine->rootContext()->setContextProperty("ClientManagementModel", m_clientManagementModel.get());
connect(m_clientManagementModel.get(), &ClientManagementModel::adminConfigRevoked, m_serversModel.get(), connect(m_clientManagementModel.get(), &ClientManagementModel::adminConfigRevoked, m_serversModel.get(),

View file

@ -41,6 +41,7 @@
#include "ui/models/protocols_model.h" #include "ui/models/protocols_model.h"
#include "ui/models/servers_model.h" #include "ui/models/servers_model.h"
#include "ui/models/services/sftpConfigModel.h" #include "ui/models/services/sftpConfigModel.h"
#include "ui/models/services/socks5ProxyConfigModel.h"
#include "ui/models/sites_model.h" #include "ui/models/sites_model.h"
#include "ui/models/clientManagementModel.h" #include "ui/models/clientManagementModel.h"
#include "ui/models/appSplitTunnelingModel.h" #include "ui/models/appSplitTunnelingModel.h"
@ -114,6 +115,7 @@ private:
#endif #endif
QScopedPointer<SftpConfigModel> m_sftpConfigModel; QScopedPointer<SftpConfigModel> m_sftpConfigModel;
QScopedPointer<Socks5ProxyConfigModel> m_socks5ConfigModel;
QSharedPointer<VpnConnection> m_vpnConnection; QSharedPointer<VpnConnection> m_vpnConnection;
QThread m_vpnConnectionThread; QThread m_vpnConnectionThread;

View file

@ -136,8 +136,34 @@
</activity> </activity>
<service <service
android:name=".AmneziaVpnService" android:name=".AwgService"
android:process=":amneziaVpnService" android:process=":amneziaAwgService"
android:permission="android.permission.BIND_VPN_SERVICE"
android:foregroundServiceType="systemExempted"
android:exported="false"
tools:ignore="ForegroundServicePermission">
<intent-filter>
<action android:name="android.net.VpnService" />
</intent-filter>
</service>
<service
android:name=".OpenVpnService"
android:process=":amneziaOpenVpnService"
android:permission="android.permission.BIND_VPN_SERVICE"
android:foregroundServiceType="systemExempted"
android:exported="false"
tools:ignore="ForegroundServicePermission">
<intent-filter>
<action android:name="android.net.VpnService" />
</intent-filter>
</service>
<service
android:name=".XrayService"
android:process=":amneziaXrayService"
android:permission="android.permission.BIND_VPN_SERVICE" android:permission="android.permission.BIND_VPN_SERVICE"
android:foregroundServiceType="systemExempted" android:foregroundServiceType="systemExempted"
android:exported="false" android:exported="false"

View file

@ -3,6 +3,7 @@ import com.android.build.gradle.internal.api.BaseVariantOutputImpl
plugins { plugins {
alias(libs.plugins.android.application) alias(libs.plugins.android.application)
alias(libs.plugins.kotlin.android) alias(libs.plugins.kotlin.android)
alias(libs.plugins.kotlin.serialization)
id("property-delegate") id("property-delegate")
} }
@ -98,7 +99,6 @@ android {
} }
dependencies { dependencies {
implementation(fileTree(mapOf("dir" to "libs", "include" to listOf("*.jar", "*.aar"))))
implementation(project(":qt")) implementation(project(":qt"))
implementation(project(":utils")) implementation(project(":utils"))
implementation(project(":protocolApi")) implementation(project(":protocolApi"))
@ -106,9 +106,11 @@ dependencies {
implementation(project(":awg")) implementation(project(":awg"))
implementation(project(":openvpn")) implementation(project(":openvpn"))
implementation(project(":cloak")) implementation(project(":cloak"))
implementation(project(":xray"))
implementation(libs.androidx.core) implementation(libs.androidx.core)
implementation(libs.androidx.activity) implementation(libs.androidx.activity)
implementation(libs.kotlinx.coroutines) implementation(libs.kotlinx.coroutines)
implementation(libs.kotlinx.serialization.protobuf)
implementation(libs.bundles.androidx.camera) implementation(libs.bundles.androidx.camera)
implementation(libs.google.mlkit) implementation(libs.google.mlkit)
implementation(libs.androidx.datastore) implementation(libs.androidx.datastore)

View file

@ -8,6 +8,7 @@ androidx-camera = "1.3.0"
androidx-security-crypto = "1.1.0-alpha06" androidx-security-crypto = "1.1.0-alpha06"
androidx-datastore = "1.1.0-beta01" androidx-datastore = "1.1.0-beta01"
kotlinx-coroutines = "1.7.3" kotlinx-coroutines = "1.7.3"
kotlinx-serialization = "1.6.3"
google-mlkit = "17.2.0" google-mlkit = "17.2.0"
[libraries] [libraries]
@ -21,6 +22,7 @@ androidx-camera-view = { module = "androidx.camera:camera-view", version.ref = "
androidx-security-crypto = { module = "androidx.security:security-crypto-ktx", version.ref = "androidx-security-crypto" } androidx-security-crypto = { module = "androidx.security:security-crypto-ktx", version.ref = "androidx-security-crypto" }
androidx-datastore = { module = "androidx.datastore:datastore-preferences", version.ref = "androidx-datastore" } androidx-datastore = { module = "androidx.datastore:datastore-preferences", version.ref = "androidx-datastore" }
kotlinx-coroutines = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-android", version.ref = "kotlinx-coroutines" } kotlinx-coroutines = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-android", version.ref = "kotlinx-coroutines" }
kotlinx-serialization-protobuf = { module = "org.jetbrains.kotlinx:kotlinx-serialization-protobuf", version.ref = "kotlinx-serialization" }
google-mlkit = { module = "com.google.mlkit:barcode-scanning", version.ref = "google-mlkit" } google-mlkit = { module = "com.google.mlkit:barcode-scanning", version.ref = "google-mlkit" }
[bundles] [bundles]
@ -35,3 +37,4 @@ androidx-camera = [
android-application = { id = "com.android.application", version.ref = "agp" } android-application = { id = "com.android.application", version.ref = "agp" }
android-library = { id = "com.android.library", version.ref = "agp" } android-library = { id = "com.android.library", version.ref = "agp" }
kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" } kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin"}

View file

@ -1,15 +1,13 @@
package org.amnezia.vpn.protocol.openvpn package org.amnezia.vpn.protocol.openvpn
import android.content.Context
import android.net.VpnService.Builder import android.net.VpnService.Builder
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.cancel
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import net.openvpn.ovpn3.ClientAPI_Config import net.openvpn.ovpn3.ClientAPI_Config
import org.amnezia.vpn.protocol.BadConfigException import org.amnezia.vpn.protocol.BadConfigException
import org.amnezia.vpn.protocol.Protocol import org.amnezia.vpn.protocol.Protocol
import org.amnezia.vpn.protocol.ProtocolState
import org.amnezia.vpn.protocol.ProtocolState.DISCONNECTED import org.amnezia.vpn.protocol.ProtocolState.DISCONNECTED
import org.amnezia.vpn.protocol.Statistics import org.amnezia.vpn.protocol.Statistics
import org.amnezia.vpn.protocol.VpnStartException import org.amnezia.vpn.protocol.VpnStartException
@ -37,7 +35,6 @@ import org.json.JSONObject
open class OpenVpn : Protocol() { open class OpenVpn : Protocol() {
private lateinit var context: Context
private var openVpnClient: OpenVpnClient? = null private var openVpnClient: OpenVpnClient? = null
private lateinit var scope: CoroutineScope private lateinit var scope: CoroutineScope
@ -53,10 +50,11 @@ open class OpenVpn : Protocol() {
return Statistics.EMPTY_STATISTICS return Statistics.EMPTY_STATISTICS
} }
override fun initialize(context: Context, state: MutableStateFlow<ProtocolState>, onError: (String) -> Unit) { override fun internalInit() {
super.initialize(context, state, onError) if (!isInitialized) loadSharedLibrary(context, "ovpn3")
loadSharedLibrary(context, "ovpn3") if (this::scope.isInitialized) {
this.context = context scope.cancel()
}
scope = CoroutineScope(Dispatchers.IO) scope = CoroutineScope(Dispatchers.IO)
} }

View file

@ -27,14 +27,21 @@ private const val SPLIT_TUNNEL_EXCLUDE = 2
abstract class Protocol { abstract class Protocol {
abstract val statistics: Statistics abstract val statistics: Statistics
protected lateinit var context: Context
protected lateinit var state: MutableStateFlow<ProtocolState> protected lateinit var state: MutableStateFlow<ProtocolState>
protected lateinit var onError: (String) -> Unit protected lateinit var onError: (String) -> Unit
protected var isInitialized: Boolean = false
open fun initialize(context: Context, state: MutableStateFlow<ProtocolState>, onError: (String) -> Unit) { fun initialize(context: Context, state: MutableStateFlow<ProtocolState>, onError: (String) -> Unit) {
this.context = context
this.state = state this.state = state
this.onError = onError this.onError = onError
internalInit()
isInitialized = true
} }
protected abstract fun internalInit()
abstract fun startVpn(config: JSONObject, vpnBuilder: Builder, protect: (Int) -> Boolean) abstract fun startVpn(config: JSONObject, vpnBuilder: Builder, protect: (Int) -> Boolean)
abstract fun stopVpn() abstract fun stopVpn()

View file

@ -21,5 +21,5 @@ android {
} }
dependencies { dependencies {
implementation(fileTree(mapOf("dir" to "../libs", "include" to listOf("*.jar", "*.aar")))) implementation(fileTree(mapOf("dir" to "../libs", "include" to listOf("*.jar"))))
} }

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.3 KiB

After

Width:  |  Height:  |  Size: 6.5 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.1 KiB

After

Width:  |  Height:  |  Size: 8.2 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 KiB

After

Width:  |  Height:  |  Size: 2.6 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 KiB

After

Width:  |  Height:  |  Size: 3.1 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.4 KiB

After

Width:  |  Height:  |  Size: 3.8 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.6 KiB

After

Width:  |  Height:  |  Size: 4.7 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.7 KiB

After

Width:  |  Height:  |  Size: 9.3 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.4 KiB

After

Width:  |  Height:  |  Size: 12 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 19 KiB

After

Width:  |  Height:  |  Size: 15 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.1 KiB

After

Width:  |  Height:  |  Size: 21 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 30 KiB

After

Width:  |  Height:  |  Size: 22 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 30 KiB

Before After
Before After

View file

@ -36,6 +36,8 @@ include(":wireguard")
include(":awg") include(":awg")
include(":openvpn") include(":openvpn")
include(":cloak") include(":cloak")
include(":xray")
include(":xray:libXray")
// get values from gradle or local properties // get values from gradle or local properties
val androidBuildToolsVersion: String by gradleProperties val androidBuildToolsVersion: String by gradleProperties

View file

@ -34,6 +34,7 @@ import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.async
import kotlinx.coroutines.cancel import kotlinx.coroutines.cancel
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
@ -43,6 +44,8 @@ import org.amnezia.vpn.protocol.getStatus
import org.amnezia.vpn.qt.QtAndroidController import org.amnezia.vpn.qt.QtAndroidController
import org.amnezia.vpn.util.Log import org.amnezia.vpn.util.Log
import org.amnezia.vpn.util.Prefs import org.amnezia.vpn.util.Prefs
import org.json.JSONException
import org.json.JSONObject
import org.qtproject.qt.android.bindings.QtActivity import org.qtproject.qt.android.bindings.QtActivity
private const val TAG = "AmneziaActivity" private const val TAG = "AmneziaActivity"
@ -59,6 +62,7 @@ class AmneziaActivity : QtActivity() {
private lateinit var mainScope: CoroutineScope private lateinit var mainScope: CoroutineScope
private val qtInitialized = CompletableDeferred<Unit>() private val qtInitialized = CompletableDeferred<Unit>()
private var vpnProto: VpnProto? = null
private var isWaitingStatus = true private var isWaitingStatus = true
private var isServiceConnected = false private var isServiceConnected = false
private var isInBoundState = false private var isInBoundState = false
@ -141,6 +145,7 @@ class AmneziaActivity : QtActivity() {
override fun onBindingDied(name: ComponentName?) { override fun onBindingDied(name: ComponentName?) {
Log.w(TAG, "Binding to the ${name?.flattenToString()} unexpectedly died") Log.w(TAG, "Binding to the ${name?.flattenToString()} unexpectedly died")
doUnbindService() doUnbindService()
QtAndroidController.onServiceDisconnected()
doBindService() doBindService()
} }
} }
@ -153,15 +158,20 @@ class AmneziaActivity : QtActivity() {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
Log.d(TAG, "Create Amnezia activity: $intent") Log.d(TAG, "Create Amnezia activity: $intent")
mainScope = CoroutineScope(SupervisorJob() + Dispatchers.Main.immediate) mainScope = CoroutineScope(SupervisorJob() + Dispatchers.Main.immediate)
val proto = mainScope.async(Dispatchers.IO) {
VpnStateStore.getVpnState().vpnProto
}
vpnServiceMessenger = IpcMessenger( vpnServiceMessenger = IpcMessenger(
"VpnService", "VpnService",
onDeadObjectException = { onDeadObjectException = {
doUnbindService() doUnbindService()
QtAndroidController.onServiceDisconnected()
doBindService() doBindService()
} }
) )
registerBroadcastReceivers() registerBroadcastReceivers()
intent?.let(::processIntent) intent?.let(::processIntent)
runBlocking { vpnProto = proto.await() }
} }
private fun registerBroadcastReceivers() { private fun registerBroadcastReceivers() {
@ -209,13 +219,18 @@ class AmneziaActivity : QtActivity() {
Log.d(TAG, "Start Amnezia activity") Log.d(TAG, "Start Amnezia activity")
mainScope.launch { mainScope.launch {
qtInitialized.await() qtInitialized.await()
doBindService() vpnProto?.let { proto ->
if (AmneziaVpnService.isRunning(applicationContext, proto.processName)) {
doBindService()
}
}
} }
} }
override fun onStop() { override fun onStop() {
Log.d(TAG, "Stop Amnezia activity") Log.d(TAG, "Stop Amnezia activity")
doUnbindService() doUnbindService()
QtAndroidController.onServiceDisconnected()
super.onStop() super.onStop()
} }
@ -269,10 +284,12 @@ class AmneziaActivity : QtActivity() {
@MainThread @MainThread
private fun doBindService() { private fun doBindService() {
Log.d(TAG, "Bind service") Log.d(TAG, "Bind service")
Intent(this, AmneziaVpnService::class.java).also { vpnProto?.let { proto ->
bindService(it, serviceConnection, BIND_ABOVE_CLIENT and BIND_AUTO_CREATE) Intent(this, proto.serviceClass).also {
bindService(it, serviceConnection, BIND_ABOVE_CLIENT and BIND_AUTO_CREATE)
}
isInBoundState = true
} }
isInBoundState = true
} }
@MainThread @MainThread
@ -280,7 +297,6 @@ class AmneziaActivity : QtActivity() {
if (isInBoundState) { if (isInBoundState) {
Log.d(TAG, "Unbind service") Log.d(TAG, "Unbind service")
isWaitingStatus = true isWaitingStatus = true
QtAndroidController.onServiceDisconnected()
isServiceConnected = false isServiceConnected = false
vpnServiceMessenger.send(Action.UNREGISTER_CLIENT, activityMessenger) vpnServiceMessenger.send(Action.UNREGISTER_CLIENT, activityMessenger)
vpnServiceMessenger.reset() vpnServiceMessenger.reset()
@ -365,13 +381,31 @@ class AmneziaActivity : QtActivity() {
@MainThread @MainThread
private fun startVpn(vpnConfig: String) { private fun startVpn(vpnConfig: String) {
if (isServiceConnected) { getVpnProto(vpnConfig)?.let { proto ->
connectToVpn(vpnConfig) Log.d(TAG, "Proto from config: $proto, current proto: $vpnProto")
} else { if (isServiceConnected) {
if (proto == vpnProto) {
connectToVpn(vpnConfig)
return
}
doUnbindService()
}
vpnProto = proto
isWaitingStatus = false isWaitingStatus = false
startVpnService(vpnConfig) startVpnService(vpnConfig, proto)
doBindService() 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) { private fun connectToVpn(vpnConfig: String) {
@ -383,15 +417,15 @@ class AmneziaActivity : QtActivity() {
} }
} }
private fun startVpnService(vpnConfig: String) { private fun startVpnService(vpnConfig: String, proto: VpnProto) {
Log.d(TAG, "Start VPN service") Log.d(TAG, "Start VPN service: $proto")
Intent(this, AmneziaVpnService::class.java).apply { Intent(this, proto.serviceClass).apply {
putExtra(MSG_VPN_CONFIG, vpnConfig) putExtra(MSG_VPN_CONFIG, vpnConfig)
}.also { }.also {
try { try {
ContextCompat.startForegroundService(this, it) ContextCompat.startForegroundService(this, it)
} catch (e: SecurityException) { } catch (e: SecurityException) {
Log.e(TAG, "Failed to start AmneziaVpnService: $e") Log.e(TAG, "Failed to start ${proto.serviceClass.simpleName}: $e")
QtAndroidController.onServiceError() QtAndroidController.onServiceError()
} }
} }

View file

@ -39,6 +39,9 @@ class AmneziaTileService : TileService() {
@Volatile @Volatile
private var isServiceConnected = false private var isServiceConnected = false
@Volatile
private var vpnProto: VpnProto? = null
private var isInBoundState = false private var isInBoundState = false
@Volatile @Volatile
private var isVpnConfigExists = false private var isVpnConfigExists = false
@ -94,16 +97,21 @@ class AmneziaTileService : TileService() {
override fun onStartListening() { override fun onStartListening() {
super.onStartListening() super.onStartListening()
Log.d(TAG, "Start listening") scope.launch {
if (AmneziaVpnService.isRunning(applicationContext)) { Log.d(TAG, "Start listening")
Log.d(TAG, "Vpn service is running") vpnProto = VpnStateStore.getVpnState().vpnProto
doBindService() vpnProto.also { proto ->
} else { if (proto != null && AmneziaVpnService.isRunning(applicationContext, proto.processName)) {
Log.d(TAG, "Vpn service is not running") Log.d(TAG, "Vpn service is running")
isServiceConnected = false doBindService()
updateVpnState(DISCONNECTED) } else {
Log.d(TAG, "Vpn service is not running")
isServiceConnected = false
updateVpnState(DISCONNECTED)
}
}
vpnStateListeningJob = launchVpnStateListening()
} }
vpnStateListeningJob = launchVpnStateListening()
} }
override fun onStopListening() { override fun onStopListening() {
@ -124,7 +132,7 @@ class AmneziaTileService : TileService() {
} }
private fun onClickInternal() { private fun onClickInternal() {
if (isVpnConfigExists) { if (isVpnConfigExists && vpnProto != null) {
Log.d(TAG, "Change VPN state") Log.d(TAG, "Change VPN state")
if (qsTile.state == Tile.STATE_INACTIVE) { if (qsTile.state == Tile.STATE_INACTIVE) {
Log.d(TAG, "Start VPN") Log.d(TAG, "Start VPN")
@ -147,10 +155,12 @@ class AmneziaTileService : TileService() {
private fun doBindService() { private fun doBindService() {
Log.d(TAG, "Bind service") Log.d(TAG, "Bind service")
Intent(this, AmneziaVpnService::class.java).also { vpnProto?.let { proto ->
bindService(it, serviceConnection, BIND_ABOVE_CLIENT) Intent(this, proto.serviceClass).also {
bindService(it, serviceConnection, BIND_ABOVE_CLIENT)
}
isInBoundState = true
} }
isInBoundState = true
} }
private fun doUnbindService() { private fun doUnbindService() {
@ -180,6 +190,7 @@ class AmneziaTileService : TileService() {
if (VpnService.prepare(applicationContext) != null) { if (VpnService.prepare(applicationContext) != null) {
Intent(this, VpnRequestActivity::class.java).apply { Intent(this, VpnRequestActivity::class.java).apply {
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
putExtra(EXTRA_PROTOCOL, vpnProto)
}.also { }.also {
startActivityAndCollapseCompat(it) startActivityAndCollapseCompat(it)
} }
@ -189,14 +200,16 @@ class AmneziaTileService : TileService() {
} }
private fun startVpnService() { private fun startVpnService() {
try { vpnProto?.let { proto ->
ContextCompat.startForegroundService( try {
applicationContext, ContextCompat.startForegroundService(
Intent(this, AmneziaVpnService::class.java) applicationContext,
) Intent(this, proto.serviceClass)
} catch (e: SecurityException) { )
Log.e(TAG, "Failed to start AmneziaVpnService: $e") } 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) private fun connectToVpn() = vpnServiceMessenger.send(Action.CONNECT)
@ -220,11 +233,8 @@ class AmneziaTileService : TileService() {
} }
} }
private fun updateVpnState(state: ProtocolState) { private fun updateVpnState(state: ProtocolState) =
scope.launch { scope.launch { VpnStateStore.store { it.copy(protocolState = state) } }
VpnStateStore.store { it.copy(protocolState = state) }
}
}
private fun launchVpnStateListening() = private fun launchVpnStateListening() =
scope.launch { VpnStateStore.dataFlow().collectLatest(::updateTile) } scope.launch { VpnStateStore.dataFlow().collectLatest(::updateTile) }
@ -232,9 +242,10 @@ class AmneziaTileService : TileService() {
private fun updateTile(vpnState: VpnState) { private fun updateTile(vpnState: VpnState) {
Log.d(TAG, "Update tile: $vpnState") Log.d(TAG, "Update tile: $vpnState")
isVpnConfigExists = vpnState.serverName != null isVpnConfigExists = vpnState.serverName != null
vpnProto = vpnState.vpnProto
val tile = qsTile ?: return val tile = qsTile ?: return
tile.apply { tile.apply {
label = vpnState.serverName ?: DEFAULT_TILE_LABEL label = (vpnState.serverName ?: DEFAULT_TILE_LABEL) + (vpnProto?.let { " ${it.label}" } ?: "")
when (val protocolState = vpnState.protocolState) { when (val protocolState = vpnState.protocolState) {
CONNECTED -> { CONNECTED -> {
state = Tile.STATE_ACTIVE state = Tile.STATE_ACTIVE

View file

@ -1,5 +1,6 @@
package org.amnezia.vpn package org.amnezia.vpn
import android.annotation.SuppressLint
import android.app.ActivityManager import android.app.ActivityManager
import android.app.ActivityManager.RunningAppProcessInfo.IMPORTANCE_FOREGROUND_SERVICE import android.app.ActivityManager.RunningAppProcessInfo.IMPORTANCE_FOREGROUND_SERVICE
import android.app.NotificationManager import android.app.NotificationManager
@ -39,7 +40,6 @@ import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withTimeout import kotlinx.coroutines.withTimeout
import org.amnezia.vpn.protocol.BadConfigException import org.amnezia.vpn.protocol.BadConfigException
import org.amnezia.vpn.protocol.LoadLibraryException 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.CONNECTED
import org.amnezia.vpn.protocol.ProtocolState.CONNECTING import org.amnezia.vpn.protocol.ProtocolState.CONNECTING
import org.amnezia.vpn.protocol.ProtocolState.DISCONNECTED 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.ProtocolState.UNKNOWN
import org.amnezia.vpn.protocol.VpnException import org.amnezia.vpn.protocol.VpnException
import org.amnezia.vpn.protocol.VpnStartException 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.putStatus
import org.amnezia.vpn.protocol.wireguard.Wireguard
import org.amnezia.vpn.util.Log import org.amnezia.vpn.util.Log
import org.amnezia.vpn.util.Prefs import org.amnezia.vpn.util.Prefs
import org.amnezia.vpn.util.net.NetworkState import org.amnezia.vpn.util.net.NetworkState
@ -63,6 +59,7 @@ import org.json.JSONObject
private const val TAG = "AmneziaVpnService" private const val TAG = "AmneziaVpnService"
const val ACTION_DISCONNECT = "org.amnezia.vpn.action.disconnect" 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_VPN_CONFIG = "VPN_CONFIG"
const val MSG_ERROR = "ERROR" 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_CONFIG_KEY = "LAST_CONF"
private const val PREFS_SERVER_NAME = "LAST_SERVER_NAME" private const val PREFS_SERVER_NAME = "LAST_SERVER_NAME"
private const val PREFS_SERVER_INDEX = "LAST_SERVER_INDEX" 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 STATISTICS_SENDING_TIMEOUT = 1000L
private const val TRAFFIC_STATS_UPDATE_TIMEOUT = 1000L private const val TRAFFIC_STATS_UPDATE_TIMEOUT = 1000L
private const val DISCONNECT_TIMEOUT = 5000L private const val DISCONNECT_TIMEOUT = 5000L
private const val STOP_SERVICE_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 mainScope: CoroutineScope
private lateinit var connectionScope: CoroutineScope private lateinit var connectionScope: CoroutineScope
private var isServiceBound = false private var isServiceBound = false
private var protocol: Protocol? = null private var vpnProto: VpnProto? = null
private val protocolCache = mutableMapOf<String, Protocol>()
private var protocolState = MutableStateFlow(UNKNOWN) private var protocolState = MutableStateFlow(UNKNOWN)
private var serverName: String? = null private var serverName: String? = null
private var serverIndex: Int = -1 private var serverIndex: Int = -1
@ -105,7 +101,7 @@ class AmneziaVpnService : VpnService() {
// private var statisticsSendingJob: Job? = null // private var statisticsSendingJob: Job? = null
private lateinit var networkState: NetworkState private lateinit var networkState: NetworkState
private lateinit var trafficStats: TrafficStats private lateinit var trafficStats: TrafficStats
private var disconnectReceiver: BroadcastReceiver? = null private var controlReceiver: BroadcastReceiver? = null
private var notificationStateReceiver: BroadcastReceiver? = null private var notificationStateReceiver: BroadcastReceiver? = null
private var screenOnReceiver: BroadcastReceiver? = null private var screenOnReceiver: BroadcastReceiver? = null
private var screenOffReceiver: BroadcastReceiver? = null private var screenOffReceiver: BroadcastReceiver? = null
@ -116,7 +112,6 @@ class AmneziaVpnService : VpnService() {
private val connectionExceptionHandler = CoroutineExceptionHandler { _, e -> private val connectionExceptionHandler = CoroutineExceptionHandler { _, e ->
protocolState.value = DISCONNECTED protocolState.value = DISCONNECTED
protocol = null
when (e) { when (e) {
is IllegalArgumentException, is IllegalArgumentException,
is VpnStartException, is VpnStartException,
@ -227,7 +222,8 @@ class AmneziaVpnService : VpnService() {
connect(intent?.getStringExtra(MSG_VPN_CONFIG)) connect(intent?.getStringExtra(MSG_VPN_CONFIG))
} }
ServiceCompat.startForeground( ServiceCompat.startForeground(
this, NOTIFICATION_ID, serviceNotification.buildNotification(serverName, protocolState.value), this, NOTIFICATION_ID,
serviceNotification.buildNotification(serverName, vpnProto?.label, protocolState.value),
foregroundServiceTypeCompat foregroundServiceTypeCompat
) )
return START_REDELIVER_INTENT return START_REDELIVER_INTENT
@ -292,9 +288,17 @@ class AmneziaVpnService : VpnService() {
private fun registerBroadcastReceivers() { private fun registerBroadcastReceivers() {
Log.d(TAG, "Register broadcast receivers") Log.d(TAG, "Register broadcast receivers")
disconnectReceiver = registerBroadcastReceiver(ACTION_DISCONNECT, ContextCompat.RECEIVER_NOT_EXPORTED) { controlReceiver = registerBroadcastReceiver(
Log.d(TAG, "Broadcast request received: $ACTION_DISCONNECT") arrayOf(ACTION_CONNECT, ACTION_DISCONNECT), ContextCompat.RECEIVER_NOT_EXPORTED
disconnect() ) {
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) { notificationStateReceiver = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
@ -340,10 +344,10 @@ class AmneziaVpnService : VpnService() {
private fun unregisterBroadcastReceivers() { private fun unregisterBroadcastReceivers() {
Log.d(TAG, "Unregister broadcast receivers") Log.d(TAG, "Unregister broadcast receivers")
unregisterBroadcastReceiver(disconnectReceiver) unregisterBroadcastReceiver(controlReceiver)
unregisterBroadcastReceiver(notificationStateReceiver) unregisterBroadcastReceiver(notificationStateReceiver)
unregisterScreenStateBroadcastReceivers() unregisterScreenStateBroadcastReceivers()
disconnectReceiver = null controlReceiver = null
notificationStateReceiver = null notificationStateReceiver = null
} }
@ -356,7 +360,7 @@ class AmneziaVpnService : VpnService() {
protocolState.drop(1).collect { protocolState -> protocolState.drop(1).collect { protocolState ->
Log.d(TAG, "Protocol state changed: $protocolState") Log.d(TAG, "Protocol state changed: $protocolState")
serviceNotification.updateNotification(serverName, protocolState) serviceNotification.updateNotification(serverName, vpnProto?.label, protocolState)
clientMessengers.send { clientMessengers.send {
ServiceEvent.STATUS_CHANGED.packToMessage { 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) { when (protocolState) {
CONNECTED -> { CONNECTED -> {
@ -421,7 +425,7 @@ class AmneziaVpnService : VpnService() {
@MainThread @MainThread
private fun enableNotification() { private fun enableNotification() {
registerScreenStateBroadcastReceivers() registerScreenStateBroadcastReceivers()
serviceNotification.updateNotification(serverName, protocolState.value) serviceNotification.updateNotification(serverName, vpnProto?.label, protocolState.value)
launchTrafficStatsUpdate() launchTrafficStatsUpdate()
} }
@ -484,8 +488,6 @@ class AmneziaVpnService : VpnService() {
Log.d(TAG, "Start VPN connection") Log.d(TAG, "Start VPN connection")
protocolState.value = CONNECTING
val config = parseConfigToJson(vpnConfig) val config = parseConfigToJson(vpnConfig)
saveServerData(config) saveServerData(config)
if (config == null) { if (config == null) {
@ -494,6 +496,16 @@ class AmneziaVpnService : VpnService() {
return 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()) { if (!checkPermission()) {
protocolState.value = DISCONNECTED protocolState.value = DISCONNECTED
return return
@ -503,8 +515,10 @@ class AmneziaVpnService : VpnService() {
disconnectionJob?.join() disconnectionJob?.join()
disconnectionJob = null disconnectionJob = null
protocol = getProtocol(config.getString("protocol")) vpnProto?.protocol?.let { protocol ->
protocol?.startVpn(config, Builder(), ::protect) protocol.initialize(applicationContext, protocolState, ::onError)
protocol.startVpn(config, Builder(), ::protect)
}
} }
} }
@ -520,8 +534,8 @@ class AmneziaVpnService : VpnService() {
connectionJob?.join() connectionJob?.join()
connectionJob = null connectionJob = null
protocol?.stopVpn() vpnProto?.protocol?.stopVpn()
protocol = null
try { try {
withTimeout(DISCONNECT_TIMEOUT) { withTimeout(DISCONNECT_TIMEOUT) {
// waiting for disconnect state // waiting for disconnect state
@ -543,22 +557,10 @@ class AmneziaVpnService : VpnService() {
protocolState.value = RECONNECTING protocolState.value = RECONNECTING
connectionJob = connectionScope.launch { 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 * Utils methods
*/ */
@ -603,6 +605,7 @@ class AmneziaVpnService : VpnService() {
if (prepare(applicationContext) != null) { if (prepare(applicationContext) != null) {
Intent(this, VpnRequestActivity::class.java).apply { Intent(this, VpnRequestActivity::class.java).apply {
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
putExtra(EXTRA_PROTOCOL, vpnProto)
}.also { }.also {
startActivity(it) startActivity(it)
} }
@ -612,9 +615,9 @@ class AmneziaVpnService : VpnService() {
} }
companion object { companion object {
fun isRunning(context: Context): Boolean = fun isRunning(context: Context, processName: String): Boolean =
context.getSystemService<ActivityManager>()!!.runningAppProcesses.any { context.getSystemService<ActivityManager>()!!.runningAppProcesses.any {
it.processName == PROCESS_NAME && it.importance <= IMPORTANCE_FOREGROUND_SERVICE it.processName == processName && it.importance <= IMPORTANCE_FOREGROUND_SERVICE
} }
} }
} }

View file

@ -0,0 +1,3 @@
package org.amnezia.vpn
class AwgService : AmneziaVpnService()

View file

@ -0,0 +1,3 @@
package org.amnezia.vpn
class OpenVpnService : AmneziaVpnService()

View file

@ -59,14 +59,14 @@ class ServiceNotification(private val context: Context) {
formatSpeedString(rxString, txString) 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 val speedString = if (state == CONNECTED) zeroSpeed else null
Log.d(TAG, "Build notification: $serverName, $state") Log.d(TAG, "Build notification: $serverName, $state")
return notificationBuilder return notificationBuilder
.setSmallIcon(R.drawable.ic_amnezia_round) .setSmallIcon(R.drawable.ic_amnezia_round)
.setContentTitle(serverName ?: "AmneziaVPN") .setContentTitle((serverName ?: "AmneziaVPN") + (protocol?.let { " $it" } ?: ""))
.setContentText(context.getString(state)) .setContentText(context.getString(state))
.setSubText(speedString) .setSubText(speedString)
.setWhen(System.currentTimeMillis()) .setWhen(System.currentTimeMillis())
@ -96,10 +96,10 @@ class ServiceNotification(private val context: Context) {
} }
@SuppressLint("MissingPermission") @SuppressLint("MissingPermission")
fun updateNotification(serverName: String?, state: ProtocolState) { fun updateNotification(serverName: String?, protocol: String?, state: ProtocolState) {
if (context.isNotificationPermissionGranted()) { if (context.isNotificationPermissionGranted()) {
Log.d(TAG, "Update notification: $serverName, $state") 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, context,
DISCONNECT_REQUEST_CODE, DISCONNECT_REQUEST_CODE,
Intent(ACTION_DISCONNECT).apply { Intent(ACTION_DISCONNECT).apply {
setPackage("org.amnezia.vpn") setPackage(context.packageName)
}, },
PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
) )
@ -135,10 +135,12 @@ class ServiceNotification(private val context: Context) {
DISCONNECTED -> { DISCONNECTED -> {
Action( Action(
0, context.getString(R.string.connect), 0, context.getString(R.string.connect),
createServicePendingIntent( PendingIntent.getBroadcast(
context, context,
CONNECT_REQUEST_CODE, CONNECT_REQUEST_CODE,
Intent(context, AmneziaVpnService::class.java), Intent(ACTION_CONNECT).apply {
setPackage(context.packageName)
},
PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT 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 { companion object {
fun createNotificationChannel(context: Context) { fun createNotificationChannel(context: Context) {
with(NotificationManagerCompat.from(context)) { with(NotificationManagerCompat.from(context)) {

View file

@ -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<out AmneziaVpnService>
) {
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())
}
}

View file

@ -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_MASK
import android.content.res.Configuration.UI_MODE_NIGHT_YES import android.content.res.Configuration.UI_MODE_NIGHT_YES
import android.net.VpnService import android.net.VpnService
import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.provider.Settings import android.provider.Settings
import android.widget.Toast import android.widget.Toast
@ -18,9 +19,11 @@ import androidx.core.content.getSystemService
import org.amnezia.vpn.util.Log import org.amnezia.vpn.util.Log
private const val TAG = "VpnRequestActivity" private const val TAG = "VpnRequestActivity"
const val EXTRA_PROTOCOL = "PROTOCOL"
class VpnRequestActivity : ComponentActivity() { class VpnRequestActivity : ComponentActivity() {
private var vpnProto: VpnProto? = null
private var userPresentReceiver: BroadcastReceiver? = null private var userPresentReceiver: BroadcastReceiver? = null
private val requestLauncher = private val requestLauncher =
registerForActivityResult(StartActivityForResult(), ::checkRequestResult) registerForActivityResult(StartActivityForResult(), ::checkRequestResult)
@ -28,6 +31,12 @@ class VpnRequestActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
Log.d(TAG, "Start request activity") 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) val requestIntent = VpnService.prepare(applicationContext)
if (requestIntent != null) { if (requestIntent != null) {
if (getSystemService<KeyguardManager>()!!.isKeyguardLocked) { if (getSystemService<KeyguardManager>()!!.isKeyguardLocked) {
@ -66,10 +75,18 @@ class VpnRequestActivity : ComponentActivity() {
private fun onPermissionGranted() { private fun onPermissionGranted() {
Toast.makeText(this, resources.getString(R.string.vpnGranted), Toast.LENGTH_LONG).show() Toast.makeText(this, resources.getString(R.string.vpnGranted), Toast.LENGTH_LONG).show()
Intent(applicationContext, AmneziaVpnService::class.java).apply { vpnProto?.let { proto ->
putExtra(AFTER_PERMISSION_CHECK, true) Intent(applicationContext, proto.serviceClass).apply {
}.also { putExtra(AFTER_PERMISSION_CHECK, true)
ContextCompat.startForegroundService(this, it) }.also {
ContextCompat.startForegroundService(this, it)
}
} ?: run {
Intent(this, AmneziaActivity::class.java).apply {
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
}.also {
startActivity(it)
}
} }
} }

View file

@ -1,19 +1,22 @@
package org.amnezia.vpn package org.amnezia.vpn
import android.app.Application import android.app.Application
import androidx.datastore.core.CorruptionException
import androidx.datastore.core.MultiProcessDataStoreFactory import androidx.datastore.core.MultiProcessDataStoreFactory
import androidx.datastore.core.Serializer import androidx.datastore.core.Serializer
import androidx.datastore.core.handlers.ReplaceFileCorruptionHandler
import androidx.datastore.dataStoreFile import androidx.datastore.dataStoreFile
import java.io.ByteArrayInputStream
import java.io.ByteArrayOutputStream
import java.io.InputStream import java.io.InputStream
import java.io.ObjectInputStream
import java.io.ObjectOutputStream
import java.io.OutputStream import java.io.OutputStream
import java.io.Serializable
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.Flow 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
import org.amnezia.vpn.protocol.ProtocolState.DISCONNECTED import org.amnezia.vpn.protocol.ProtocolState.DISCONNECTED
import org.amnezia.vpn.util.Log import org.amnezia.vpn.util.Log
@ -21,13 +24,14 @@ import org.amnezia.vpn.util.Log
private const val TAG = "VpnState" private const val TAG = "VpnState"
private const val STORE_FILE_NAME = "vpnState" private const val STORE_FILE_NAME = "vpnState"
@Serializable
data class VpnState( data class VpnState(
val protocolState: ProtocolState, val protocolState: ProtocolState,
val serverName: String? = null, val serverName: String? = null,
val serverIndex: Int = -1 val serverIndex: Int = -1,
) : Serializable { val vpnProto: VpnProto? = null
) {
companion object { companion object {
private const val serialVersionUID: Long = -1760654961004181606
val defaultState: VpnState = VpnState(DISCONNECTED) val defaultState: VpnState = VpnState(DISCONNECTED)
} }
} }
@ -37,7 +41,11 @@ object VpnStateStore {
private val dataStore = MultiProcessDataStoreFactory.create( private val dataStore = MultiProcessDataStoreFactory.create(
serializer = VpnStateSerializer(), 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) { fun init(app: Application) {
@ -45,36 +53,36 @@ object VpnStateStore {
this.app = app this.app = app
} }
fun dataFlow(): Flow<VpnState> = dataStore.data fun dataFlow(): Flow<VpnState> = 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) { suspend fun store(f: (vpnState: VpnState) -> VpnState) {
try { try {
dataStore.updateData(f) dataStore.updateData(f)
} catch (e : Exception) { } catch (e: Exception) {
Log.e(TAG, "Failed to store VpnState: $e") 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<VpnState> { private class VpnStateSerializer : Serializer<VpnState> {
override val defaultValue: VpnState = VpnState.defaultState override val defaultValue: VpnState = VpnState.defaultState
override suspend fun readFrom(input: InputStream): VpnState { override suspend fun readFrom(input: InputStream): VpnState = try {
return withContext(Dispatchers.IO) { ProtoBuf.decodeFromByteArray<VpnState>(input.readBytes())
val bios = ByteArrayInputStream(input.readBytes()) } catch (e: SerializationException) {
ObjectInputStream(bios).use { Log.e(TAG, "Failed to deserialize data: $e")
it.readObject() as VpnState throw CorruptionException("Failed to deserialize data", e)
}
}
} }
override suspend fun writeTo(t: VpnState, output: OutputStream) { @Suppress("BlockingMethodInNonBlockingContext")
withContext(Dispatchers.IO) { override suspend fun writeTo(t: VpnState, output: OutputStream) =
val baos = ByteArrayOutputStream() output.write(ProtoBuf.encodeToByteArray(t))
ObjectOutputStream(baos).use {
it.writeObject(t)
}
output.write(baos.toByteArray())
}
}
} }

View file

@ -0,0 +1,3 @@
package org.amnezia.vpn
class XrayService : AmneziaVpnService()

View file

@ -1,12 +1,9 @@
package org.amnezia.vpn.protocol.wireguard package org.amnezia.vpn.protocol.wireguard
import android.content.Context
import android.net.VpnService.Builder import android.net.VpnService.Builder
import java.util.TreeMap import java.util.TreeMap
import kotlinx.coroutines.flow.MutableStateFlow
import org.amnezia.awg.GoBackend import org.amnezia.awg.GoBackend
import org.amnezia.vpn.protocol.Protocol 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.CONNECTED
import org.amnezia.vpn.protocol.ProtocolState.DISCONNECTED import org.amnezia.vpn.protocol.ProtocolState.DISCONNECTED
import org.amnezia.vpn.protocol.Statistics import org.amnezia.vpn.protocol.Statistics
@ -78,9 +75,8 @@ open class Wireguard : Protocol() {
} }
} }
override fun initialize(context: Context, state: MutableStateFlow<ProtocolState>, onError: (String) -> Unit) { override fun internalInit() {
super.initialize(context, state, onError) if (!isInitialized) loadSharedLibrary(context, "wg-go")
loadSharedLibrary(context, "wg-go")
} }
override fun startVpn(config: JSONObject, vpnBuilder: Builder, protect: (Int) -> Boolean) { override fun startVpn(config: JSONObject, vpnBuilder: Builder, protect: (Int) -> Boolean) {

View file

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

View file

@ -0,0 +1,6 @@
@file:Suppress("UnstableApiUsage")
configurations {
maybeCreate("default")
}
artifacts.add("default", file("libxray.aar"))

View file

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

View file

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

View file

@ -52,3 +52,6 @@ foreach(abi IN ITEMS ${QT_ANDROID_ABIS})
${CMAKE_CURRENT_SOURCE_DIR}/3rd-prebuilt/3rd-prebuilt/libssh/android/${abi}/libssh.so ${CMAKE_CURRENT_SOURCE_DIR}/3rd-prebuilt/3rd-prebuilt/libssh/android/${abi}/libssh.so
) )
endforeach() endforeach()
file(COPY ${CMAKE_CURRENT_SOURCE_DIR}/3rd-prebuilt/3rd-prebuilt/xray/android/libxray.aar
DESTINATION ${CMAKE_CURRENT_SOURCE_DIR}/android/xray/libXray)

View file

@ -69,6 +69,8 @@ QVector<amnezia::Proto> ContainerProps::protocolsForContainer(amnezia::DockerCon
case DockerContainer::Sftp: return { Proto::Sftp }; case DockerContainer::Sftp: return { Proto::Sftp };
case DockerContainer::Socks5Proxy: return { Proto::Socks5Proxy };
default: return { defaultProtocol(container) }; default: return { defaultProtocol(container) };
} }
} }
@ -98,7 +100,8 @@ QMap<DockerContainer, QString> ContainerProps::containerHumanNames()
{ DockerContainer::TorWebSite, QObject::tr("Website in Tor network") }, { DockerContainer::TorWebSite, QObject::tr("Website in Tor network") },
{ DockerContainer::Dns, QObject::tr("AmneziaDNS") }, { DockerContainer::Dns, QObject::tr("AmneziaDNS") },
{ DockerContainer::Sftp, QObject::tr("SFTP file sharing service") } }; { DockerContainer::Sftp, QObject::tr("SFTP file sharing service") },
{ DockerContainer::Socks5Proxy, QObject::tr("SOCKS5 proxy server") } };
} }
QMap<DockerContainer, QString> ContainerProps::containerDescriptions() QMap<DockerContainer, QString> ContainerProps::containerDescriptions()
@ -131,7 +134,9 @@ QMap<DockerContainer, QString> ContainerProps::containerDescriptions()
{ DockerContainer::Dns, { DockerContainer::Dns,
QObject::tr("Replace the current DNS server with your own. This will increase your privacy level.") }, QObject::tr("Replace the current DNS server with your own. This will increase your privacy level.") },
{ DockerContainer::Sftp, { DockerContainer::Sftp,
QObject::tr("Create a file vault on your server to securely store and transfer files.") } }; QObject::tr("Create a file vault on your server to securely store and transfer files.") },
{ DockerContainer::Socks5Proxy,
QObject::tr("") } };
} }
QMap<DockerContainer, QString> ContainerProps::containerDetailedDescriptions() QMap<DockerContainer, QString> ContainerProps::containerDetailedDescriptions()
@ -239,7 +244,8 @@ QMap<DockerContainer, QString> ContainerProps::containerDetailedDescriptions()
QObject::tr("After installation, Amnezia will create a\n\n file storage on your server. " QObject::tr("After installation, Amnezia will create a\n\n file storage on your server. "
"You will be able to access it using\n FileZilla or other SFTP clients, " "You will be able to access it using\n FileZilla or other SFTP clients, "
"as well as mount the disk on your device to access\n it directly from your device.\n\n" "as well as mount the disk on your device to access\n it directly from your device.\n\n"
"For more detailed information, you can\n find it in the support section under \"Create SFTP file storage.\" ") } "For more detailed information, you can\n find it in the support section under \"Create SFTP file storage.\" ") },
{ DockerContainer::Socks5Proxy, QObject::tr("SOCKS5 proxy server") }
}; };
} }
@ -264,6 +270,7 @@ Proto ContainerProps::defaultProtocol(DockerContainer c)
case DockerContainer::TorWebSite: return Proto::TorWebSite; case DockerContainer::TorWebSite: return Proto::TorWebSite;
case DockerContainer::Dns: return Proto::Dns; case DockerContainer::Dns: return Proto::Dns;
case DockerContainer::Sftp: return Proto::Sftp; case DockerContainer::Sftp: return Proto::Sftp;
case DockerContainer::Socks5Proxy: return Proto::Socks5Proxy;
default: return Proto::Any; default: return Proto::Any;
} }
} }
@ -297,6 +304,7 @@ bool ContainerProps::isSupportedByCurrentPlatform(DockerContainer c)
case DockerContainer::ShadowSocks: return false; case DockerContainer::ShadowSocks: return false;
case DockerContainer::Awg: return true; case DockerContainer::Awg: return true;
case DockerContainer::Cloak: return true; case DockerContainer::Cloak: return true;
case DockerContainer::Xray: return true;
default: return false; default: return false;
} }
@ -366,6 +374,7 @@ bool ContainerProps::isShareable(DockerContainer container)
case DockerContainer::TorWebSite: return false; case DockerContainer::TorWebSite: return false;
case DockerContainer::Dns: return false; case DockerContainer::Dns: return false;
case DockerContainer::Sftp: return false; case DockerContainer::Sftp: return false;
case DockerContainer::Socks5Proxy: return false;
default: return true; default: return true;
} }
} }

View file

@ -28,7 +28,8 @@ namespace amnezia
// non-vpn // non-vpn
TorWebSite, TorWebSite,
Dns, Dns,
Sftp Sftp,
Socks5Proxy
}; };
Q_ENUM_NS(DockerContainer) Q_ENUM_NS(DockerContainer)
} // namespace ContainerEnumNS } // namespace ContainerEnumNS

View file

@ -40,6 +40,28 @@ void ApiController::processApiConfig(const QString &protocol, const ApiControlle
return; return;
} else if (protocol == configKey::awg) { } else if (protocol == configKey::awg) {
config.replace("$WIREGUARD_CLIENT_PRIVATE_KEY", apiPayloadData.wireGuardClientPrivKey); config.replace("$WIREGUARD_CLIENT_PRIVATE_KEY", apiPayloadData.wireGuardClientPrivKey);
auto serverConfig = QJsonDocument::fromJson(config.toUtf8()).object();
auto containers = serverConfig.value(config_key::containers).toArray();
if (containers.isEmpty()) {
return;
}
auto container = containers.at(0).toObject();
QString containerName = ContainerProps::containerTypeToString(DockerContainer::Awg);
auto containerConfig = container.value(containerName).toObject();
auto protocolConfig = QJsonDocument::fromJson(containerConfig.value(config_key::last_config).toString().toUtf8()).object();
containerConfig[config_key::junkPacketCount] = protocolConfig.value(config_key::junkPacketCount);
containerConfig[config_key::junkPacketMinSize] = protocolConfig.value(config_key::junkPacketMinSize);
containerConfig[config_key::junkPacketMaxSize] = protocolConfig.value(config_key::junkPacketMaxSize);
containerConfig[config_key::initPacketJunkSize] = protocolConfig.value(config_key::initPacketJunkSize);
containerConfig[config_key::responsePacketJunkSize] = protocolConfig.value(config_key::responsePacketJunkSize);
containerConfig[config_key::initPacketMagicHeader] = protocolConfig.value(config_key::initPacketMagicHeader);
containerConfig[config_key::responsePacketMagicHeader] = protocolConfig.value(config_key::responsePacketMagicHeader);
containerConfig[config_key::underloadPacketMagicHeader] = protocolConfig.value(config_key::underloadPacketMagicHeader);
containerConfig[config_key::transportPacketMagicHeader] = protocolConfig.value(config_key::transportPacketMagicHeader);
container[containerName] = containerConfig;
containers.replace(0, container);
serverConfig[config_key::containers] = containers;
config = QString(QJsonDocument(serverConfig).toJson());
} }
return; return;
} }

View file

@ -106,7 +106,7 @@ ErrorCode ServerController::runContainerScript(const ServerCredentials &credenti
if (e) if (e)
return e; return e;
QString runner = QString("sudo docker exec -i $CONTAINER_NAME bash %1 ").arg(fileName); QString runner = QString("sudo docker exec -i $CONTAINER_NAME sh %1 ").arg(fileName);
e = runScript(credentials, replaceVars(runner, genVarsForScript(credentials, container)), cbReadStdOut, cbReadStdErr); e = runScript(credentials, replaceVars(runner, genVarsForScript(credentials, container)), cbReadStdOut, cbReadStdErr);
QString remover = QString("sudo docker exec -i $CONTAINER_NAME rm %1 ").arg(fileName); QString remover = QString("sudo docker exec -i $CONTAINER_NAME rm %1 ").arg(fileName);
@ -376,6 +376,10 @@ bool ServerController::isReinstallContainerRequired(DockerContainer container, c
return true; return true;
} }
if (container == DockerContainer::Socks5Proxy) {
return true;
}
return false; return false;
} }
@ -516,6 +520,7 @@ ServerController::Vars ServerController::genVarsForScript(const ServerCredential
const QJsonObject &amneziaWireguarConfig = config.value(ProtocolProps::protoToString(Proto::Awg)).toObject(); const QJsonObject &amneziaWireguarConfig = config.value(ProtocolProps::protoToString(Proto::Awg)).toObject();
const QJsonObject &xrayConfig = config.value(ProtocolProps::protoToString(Proto::Xray)).toObject(); const QJsonObject &xrayConfig = config.value(ProtocolProps::protoToString(Proto::Xray)).toObject();
const QJsonObject &sftpConfig = config.value(ProtocolProps::protoToString(Proto::Sftp)).toObject(); const QJsonObject &sftpConfig = config.value(ProtocolProps::protoToString(Proto::Sftp)).toObject();
const QJsonObject &socks5ProxyConfig = config.value(ProtocolProps::protoToString(Proto::Socks5Proxy)).toObject();
Vars vars; Vars vars;
@ -613,6 +618,14 @@ ServerController::Vars ServerController::genVarsForScript(const ServerCredential
vars.append({ { "$UNDERLOAD_PACKET_MAGIC_HEADER", amneziaWireguarConfig.value(config_key::underloadPacketMagicHeader).toString() } }); vars.append({ { "$UNDERLOAD_PACKET_MAGIC_HEADER", amneziaWireguarConfig.value(config_key::underloadPacketMagicHeader).toString() } });
vars.append({ { "$TRANSPORT_PACKET_MAGIC_HEADER", amneziaWireguarConfig.value(config_key::transportPacketMagicHeader).toString() } }); vars.append({ { "$TRANSPORT_PACKET_MAGIC_HEADER", amneziaWireguarConfig.value(config_key::transportPacketMagicHeader).toString() } });
// Socks5 proxy vars
vars.append({ { "$SOCKS5_PROXY_PORT", socks5ProxyConfig.value(config_key::port).toString(protocols::socks5Proxy::defaultPort) } });
auto username = socks5ProxyConfig.value(config_key:: userName).toString();
auto password = socks5ProxyConfig.value(config_key::password).toString();
QString socks5user = (!username.isEmpty() && !password.isEmpty()) ? QString("users %1:CL:%2").arg(username, password) : "";
vars.append({ { "$SOCKS5_USER", socks5user } });
vars.append({ { "$SOCKS5_AUTH_TYPE", socks5user.isEmpty() ? "none" : "strong" } });
QString serverIp = NetworkUtilities::getIPAddress(credentials.hostName); QString serverIp = NetworkUtilities::getIPAddress(credentials.hostName);
if (!serverIp.isEmpty()) { if (!serverIp.isEmpty()) {
vars.append({ { "$SERVER_IP_ADDRESS", serverIp } }); vars.append({ { "$SERVER_IP_ADDRESS", serverIp } });

View file

@ -18,6 +18,7 @@ QString amnezia::scriptFolder(amnezia::DockerContainer container)
case DockerContainer::TorWebSite: return QLatin1String("website_tor"); case DockerContainer::TorWebSite: return QLatin1String("website_tor");
case DockerContainer::Dns: return QLatin1String("dns"); case DockerContainer::Dns: return QLatin1String("dns");
case DockerContainer::Sftp: return QLatin1String("sftp"); case DockerContainer::Sftp: return QLatin1String("sftp");
case DockerContainer::Socks5Proxy: return QLatin1String("socks5_proxy");
default: return QString(); default: return QString();
} }
} }

View file

@ -77,7 +77,8 @@ QMap<amnezia::Proto, QString> ProtocolProps::protocolHumanNames()
{ Proto::TorWebSite, "Website in Tor network" }, { Proto::TorWebSite, "Website in Tor network" },
{ Proto::Dns, "DNS Service" }, { Proto::Dns, "DNS Service" },
{ Proto::Sftp, QObject::tr("SFTP service") } }; { Proto::Sftp, QObject::tr("SFTP service") },
{ Proto::Socks5Proxy, QObject::tr("SOCKS5 proxy server") } };
} }
QMap<amnezia::Proto, QString> ProtocolProps::protocolDescriptions() QMap<amnezia::Proto, QString> ProtocolProps::protocolDescriptions()
@ -102,6 +103,7 @@ amnezia::ServiceType ProtocolProps::protocolService(Proto p)
case Proto::TorWebSite: return ServiceType::Other; case Proto::TorWebSite: return ServiceType::Other;
case Proto::Dns: return ServiceType::Other; case Proto::Dns: return ServiceType::Other;
case Proto::Sftp: return ServiceType::Other; case Proto::Sftp: return ServiceType::Other;
case Proto::Socks5Proxy: return ServiceType::Other;
default: return ServiceType::Other; default: return ServiceType::Other;
} }
} }
@ -113,6 +115,7 @@ int ProtocolProps::getPortForInstall(Proto p)
case WireGuard: case WireGuard:
case ShadowSocks: case ShadowSocks:
case OpenVpn: case OpenVpn:
case Socks5Proxy:
return QRandomGenerator::global()->bounded(30000, 50000); return QRandomGenerator::global()->bounded(30000, 50000);
default: default:
return defaultPort(p); return defaultPort(p);
@ -135,6 +138,7 @@ int ProtocolProps::defaultPort(Proto p)
case Proto::TorWebSite: return -1; case Proto::TorWebSite: return -1;
case Proto::Dns: return 53; case Proto::Dns: return 53;
case Proto::Sftp: return 222; case Proto::Sftp: return 222;
case Proto::Socks5Proxy: return 38080;
default: return -1; default: return -1;
} }
} }
@ -154,6 +158,7 @@ bool ProtocolProps::defaultPortChangeable(Proto p)
case Proto::TorWebSite: return false; case Proto::TorWebSite: return false;
case Proto::Dns: return false; case Proto::Dns: return false;
case Proto::Sftp: return true; case Proto::Sftp: return true;
case Proto::Socks5Proxy: return true;
default: return false; default: return false;
} }
} }
@ -175,6 +180,7 @@ TransportProto ProtocolProps::defaultTransportProto(Proto p)
case Proto::TorWebSite: return TransportProto::Tcp; case Proto::TorWebSite: return TransportProto::Tcp;
case Proto::Dns: return TransportProto::Udp; case Proto::Dns: return TransportProto::Udp;
case Proto::Sftp: return TransportProto::Tcp; case Proto::Sftp: return TransportProto::Tcp;
case Proto::Socks5Proxy: return TransportProto::Tcp;
} }
} }
@ -195,6 +201,7 @@ bool ProtocolProps::defaultTransportProtoChangeable(Proto p)
case Proto::TorWebSite: return false; case Proto::TorWebSite: return false;
case Proto::Dns: return false; case Proto::Dns: return false;
case Proto::Sftp: return false; case Proto::Sftp: return false;
case Proto::Socks5Proxy: return false;
default: return false; default: return false;
} }
return false; return false;

View file

@ -84,6 +84,7 @@ namespace amnezia
constexpr char awg[] = "awg"; constexpr char awg[] = "awg";
constexpr char xray[] = "xray"; constexpr char xray[] = "xray";
constexpr char ssxray[] = "ssxray"; constexpr char ssxray[] = "ssxray";
constexpr char socks5proxy[] = "socks5proxy";
constexpr char configVersion[] = "config_version"; constexpr char configVersion[] = "config_version";
@ -216,6 +217,14 @@ namespace amnezia
constexpr char defaultUnderloadPacketMagicHeader[] = "1766607858"; constexpr char defaultUnderloadPacketMagicHeader[] = "1766607858";
} }
namespace socks5Proxy
{
constexpr char defaultUserName[] = "proxy_user";
constexpr char defaultPort[] = "38080";
constexpr char proxyConfigPath[] = "/usr/local/3proxy/conf/3proxy.cfg";
}
} // namespace protocols } // namespace protocols
namespace ProtocolEnumNS namespace ProtocolEnumNS
@ -244,7 +253,8 @@ namespace amnezia
// non-vpn // non-vpn
TorWebSite, TorWebSite,
Dns, Dns,
Sftp Sftp,
Socks5Proxy
}; };
Q_ENUM_NS(Proto) Q_ENUM_NS(Proto)

View file

@ -198,7 +198,7 @@
<file>ui/qml/Pages2/PageProtocolOpenVpnSettings.qml</file> <file>ui/qml/Pages2/PageProtocolOpenVpnSettings.qml</file>
<file>ui/qml/Pages2/PageProtocolShadowSocksSettings.qml</file> <file>ui/qml/Pages2/PageProtocolShadowSocksSettings.qml</file>
<file>ui/qml/Pages2/PageProtocolCloakSettings.qml</file> <file>ui/qml/Pages2/PageProtocolCloakSettings.qml</file>
<file>ui/qml/Pages2/PageProtocolXraySettings.qml</file> <file>ui/qml/Pages2/PageProtocolXraySettings.qml</file>
<file>ui/qml/Pages2/PageProtocolRaw.qml</file> <file>ui/qml/Pages2/PageProtocolRaw.qml</file>
<file>ui/qml/Pages2/PageSettingsLogging.qml</file> <file>ui/qml/Pages2/PageSettingsLogging.qml</file>
<file>ui/qml/Pages2/PageServiceSftpSettings.qml</file> <file>ui/qml/Pages2/PageServiceSftpSettings.qml</file>
@ -239,5 +239,10 @@
<file>images/controls/alert-circle.svg</file> <file>images/controls/alert-circle.svg</file>
<file>images/controls/file-check-2.svg</file> <file>images/controls/file-check-2.svg</file>
<file>ui/qml/Controls2/WarningType.qml</file> <file>ui/qml/Controls2/WarningType.qml</file>
<file>ui/qml/Pages2/PageServiceSocksProxySettings.qml</file>
<file>server_scripts/socks5_proxy/run_container.sh</file>
<file>server_scripts/socks5_proxy/Dockerfile</file>
<file>server_scripts/socks5_proxy/configure_container.sh</file>
<file>server_scripts/socks5_proxy/start.sh</file>
</qresource> </qresource>
</RCC> </RCC>

View file

@ -0,0 +1,10 @@
FROM 3proxy/3proxy:latest
LABEL maintainer="AmneziaVPN"
RUN mkdir -p /opt/amnezia
RUN echo -e "#!/bin/bash\ntail -f /dev/null" > /opt/amnezia/start.sh
RUN chmod a+x /opt/amnezia/start.sh
ENTRYPOINT [ "/bin/sh", "/opt/amnezia/start.sh" ]
CMD [ "" ]

View file

@ -0,0 +1,12 @@
#!/bin/sh
echo -e "#!/bin/3proxy" > /usr/local/3proxy/conf/3proxy.cfg
echo -e "config /usr/local/3proxy/conf/3proxy.cfg" >> /usr/local/3proxy/conf/3proxy.cfg
echo -e "timeouts 1 5 30 60 180 1800 15 60" >> /usr/local/3proxy/conf/3proxy.cfg
echo -e "$SOCKS5_USER" >> /usr/local/3proxy/conf/3proxy.cfg
echo -e "log /usr/local/3proxy/logs/3proxy.log" >> /usr/local/3proxy/conf/3proxy.cfg
echo -e "logformat \"-\\\"\"+_G{\"\"time_unix\"\":%t, \"\"proxy\"\":{\"\"type:\"\":\"\"%N\"\", \"\"port\"\":%p}, \"\"error\"\":{\"\"code\"\":\"\"%E\"\"}, \"\"auth\"\":{\"\"user\"\":\"\"%U\"\"}, \"\"client\"\":{\"\"ip\"\":\"\"%C\"\", \"\"port\"\":%c}, \"\"server\"\":{\"\"ip\"\":\"\"%R\"\", \"\"port\"\":%r}, \"\"bytes\"\":{\"\"sent\"\":%O, \"\"received\"\":%I}, \"\"request\"\":{\"\"hostname\"\":\"\"%n\"\"}, \"\"message\"\":\"\"%T\"\"}\"" >> /usr/local/3proxy/conf/3proxy.cfg
echo -e "auth $SOCKS5_AUTH_TYPE" >> /usr/local/3proxy/conf/3proxy.cfg
echo -e "socks -p$SOCKS5_PROXY_PORT" >> /usr/local/3proxy/conf/3proxy.cfg

View file

@ -0,0 +1,5 @@
sudo docker run -d \
--restart always \
-p $SOCKS5_PROXY_PORT:$SOCKS5_PROXY_PORT/tcp \
--name $CONTAINER_NAME \
$CONTAINER_NAME

View file

@ -0,0 +1,7 @@
#!/bin/sh
# This scripts copied from Amnezia client to Docker container to /opt/amnezia and launched every time container starts
echo "Container startup"
/bin/3proxy /usr/local/3proxy/conf/3proxy.cfg

View file

@ -1144,7 +1144,7 @@ Already installed containers were found on the server. All installed containers
<message> <message>
<location filename="../ui/qml/Pages2/PageSettingsAppSplitTunneling.qml" line="278"/> <location filename="../ui/qml/Pages2/PageSettingsAppSplitTunneling.qml" line="278"/>
<source>Executable file (*.*)</source> <source>Executable file (*.*)</source>
<translation>ि (**)</translation> <translation>ि (*.*)</translation>
</message> </message>
</context> </context>
<context> <context>
@ -1276,7 +1276,7 @@ Already installed containers were found on the server. All installed containers
<location filename="../ui/qml/Pages2/PageSettingsBackup.qml" line="102"/> <location filename="../ui/qml/Pages2/PageSettingsBackup.qml" line="102"/>
<location filename="../ui/qml/Pages2/PageSettingsBackup.qml" line="134"/> <location filename="../ui/qml/Pages2/PageSettingsBackup.qml" line="134"/>
<source>Backup files (*.backup)</source> <source>Backup files (*.backup)</source>
<translation> (*.)</translation> <translation> (*.backup)</translation>
</message> </message>
<message> <message>
<location filename="../ui/qml/Pages2/PageSettingsBackup.qml" line="111"/> <location filename="../ui/qml/Pages2/PageSettingsBackup.qml" line="111"/>
@ -1480,7 +1480,7 @@ Already installed containers were found on the server. All installed containers
<message> <message>
<location filename="../ui/qml/Pages2/PageSettingsLogging.qml" line="143"/> <location filename="../ui/qml/Pages2/PageSettingsLogging.qml" line="143"/>
<source>Logs files (*.log)</source> <source>Logs files (*.log)</source>
<translation> (*.)</translation> <translation> (*.log)</translation>
</message> </message>
<message> <message>
<location filename="../ui/qml/Pages2/PageSettingsLogging.qml" line="152"/> <location filename="../ui/qml/Pages2/PageSettingsLogging.qml" line="152"/>

View file

@ -123,6 +123,9 @@ void InstallController::install(DockerContainer container, int port, TransportPr
} else if (container == DockerContainer::Sftp) { } else if (container == DockerContainer::Sftp) {
containerConfig.insert(config_key::userName, protocols::sftp::defaultUserName); containerConfig.insert(config_key::userName, protocols::sftp::defaultUserName);
containerConfig.insert(config_key::password, Utils::getRandomString(10)); containerConfig.insert(config_key::password, Utils::getRandomString(10));
} else if (container == DockerContainer::Socks5Proxy) {
containerConfig.insert(config_key::userName, protocols::socks5Proxy::defaultUserName);
containerConfig.insert(config_key::password, Utils::getRandomString(10));
} }
config.insert(config_key::container, ContainerProps::containerToString(container)); config.insert(config_key::container, ContainerProps::containerToString(container));
@ -362,7 +365,7 @@ ErrorCode InstallController::getAlreadyInstalledContainers(const ServerCredentia
if (containerInfo.isEmpty()) { if (containerInfo.isEmpty()) {
continue; continue;
} }
const static QRegularExpression containerAndPortRegExp("(amnezia[-a-z]*).*?:([0-9]*)->[0-9]*/(udp|tcp).*"); const static QRegularExpression containerAndPortRegExp("(amnezia[-a-z0-9]*).*?:([0-9]*)->[0-9]*/(udp|tcp).*");
QRegularExpressionMatch containerAndPortMatch = containerAndPortRegExp.match(containerInfo); QRegularExpressionMatch containerAndPortMatch = containerAndPortRegExp.match(containerInfo);
if (containerAndPortMatch.hasMatch()) { if (containerAndPortMatch.hasMatch()) {
QString name = containerAndPortMatch.captured(1); QString name = containerAndPortMatch.captured(1);
@ -427,6 +430,20 @@ ErrorCode InstallController::getAlreadyInstalledContainers(const ServerCredentia
containerConfig.insert(config_key::userName, userName); containerConfig.insert(config_key::userName, userName);
containerConfig.insert(config_key::password, password); containerConfig.insert(config_key::password, password);
} else if (protocol == Proto::Socks5Proxy) {
QString proxyConfig = serverController->getTextFileFromContainer(container, credentials,
protocols::socks5Proxy::proxyConfigPath, errorCode);
const static QRegularExpression usernameAndPasswordRegExp("users (\\w+):CL:(\\w+)");
QRegularExpressionMatch usernameAndPasswordMatch = usernameAndPasswordRegExp.match(proxyConfig);
if (usernameAndPasswordMatch.hasMatch()) {
QString userName = usernameAndPasswordMatch.captured(1);
QString password = usernameAndPasswordMatch.captured(2);
containerConfig.insert(config_key::userName, userName);
containerConfig.insert(config_key::password, password);
}
} }
config.insert(config_key::container, ContainerProps::containerToString(container)); config.insert(config_key::container, ContainerProps::containerToString(container));
@ -603,6 +620,10 @@ void InstallController::clearCachedProfile(QSharedPointer<ServerController> serv
int serverIndex = m_serversModel->getProcessedServerIndex(); int serverIndex = m_serversModel->getProcessedServerIndex();
DockerContainer container = static_cast<DockerContainer>(m_containersModel->getProcessedContainerIndex()); DockerContainer container = static_cast<DockerContainer>(m_containersModel->getProcessedContainerIndex());
if (ContainerProps::containerService(container) == ServiceType::Other) {
return;
}
QJsonObject containerConfig = m_containersModel->getContainerConfig(container); QJsonObject containerConfig = m_containersModel->getContainerConfig(container);
ServerCredentials serverCredentials = ServerCredentials serverCredentials =
qvariant_cast<ServerCredentials>(m_serversModel->data(serverIndex, ServersModel::Roles::CredentialsRole)); qvariant_cast<ServerCredentials>(m_serversModel->data(serverIndex, ServersModel::Roles::CredentialsRole));

View file

@ -35,6 +35,7 @@ namespace PageLoader
PageServiceSftpSettings, PageServiceSftpSettings,
PageServiceTorWebsiteSettings, PageServiceTorWebsiteSettings,
PageServiceDnsSettings, PageServiceDnsSettings,
PageServiceSocksProxySettings,
PageSetupWizardStart, PageSetupWizardStart,
PageSetupWizardCredentials, PageSetupWizardCredentials,

View file

@ -281,7 +281,8 @@ ErrorCode ClientManagementModel::wgShow(const DockerContainer container, const S
} }
}; };
for (int i = 0; i < peerList.size() && i < transferredDataList.size(); ++i) { for (int i = 0; i < peerList.size() && i < transferredDataList.size() && i < latestHandshakeList.size(); ++i) {
const auto transferredData = getStrValue(transferredDataList[i]).split(","); const auto transferredData = getStrValue(transferredDataList[i]).split(",");
auto latestHandshake = getStrValue(latestHandshakeList[i]); auto latestHandshake = getStrValue(latestHandshakeList[i]);
auto serverBytesReceived = transferredData.front().trimmed(); auto serverBytesReceived = transferredData.front().trimmed();

View file

@ -86,6 +86,7 @@ PageLoader::PageEnum ProtocolsModel::protocolPage(Proto protocol) const
case Proto::TorWebSite: return PageLoader::PageEnum::PageServiceTorWebsiteSettings; case Proto::TorWebSite: return PageLoader::PageEnum::PageServiceTorWebsiteSettings;
case Proto::Dns: return PageLoader::PageEnum::PageServiceDnsSettings; case Proto::Dns: return PageLoader::PageEnum::PageServiceDnsSettings;
case Proto::Sftp: return PageLoader::PageEnum::PageServiceSftpSettings; case Proto::Sftp: return PageLoader::PageEnum::PageServiceSftpSettings;
case Proto::Socks5Proxy: return PageLoader::PageEnum::PageServiceSocksProxySettings;
default: return PageLoader::PageEnum::PageProtocolOpenVpnSettings; default: return PageLoader::PageEnum::PageProtocolOpenVpnSettings;
} }
} }

View file

@ -548,6 +548,8 @@ QStringList ServersModel::getAllInstalledServicesName(const int serverIndex)
servicesName.append("SFTP"); servicesName.append("SFTP");
} else if (container == DockerContainer::TorWebSite) { } else if (container == DockerContainer::TorWebSite) {
servicesName.append("TOR"); servicesName.append("TOR");
} else if (container == DockerContainer::Socks5Proxy) {
servicesName.append("SOCKS5");
} }
} }
} }
@ -615,15 +617,18 @@ bool ServersModel::isDefaultServerDefaultContainerHasSplitTunneling()
{ {
auto server = m_servers.at(m_defaultServerIndex).toObject(); auto server = m_servers.at(m_defaultServerIndex).toObject();
auto defaultContainer = ContainerProps::containerFromString(server.value(config_key::defaultContainer).toString()); auto defaultContainer = ContainerProps::containerFromString(server.value(config_key::defaultContainer).toString());
auto containerConfig = server.value(config_key::containers).toArray().at(defaultContainer).toObject();
auto protocolConfig = containerConfig.value(ContainerProps::containerTypeToString(defaultContainer)).toObject();
if (defaultContainer == DockerContainer::Awg || defaultContainer == DockerContainer::WireGuard) { auto containers = server.value(config_key::containers).toArray();
return !(protocolConfig.value(config_key::last_config).toString().contains("AllowedIPs = 0.0.0.0/0, ::/0")); for (auto i = 0; i < containers.size(); i++) {
} else if (defaultContainer == DockerContainer::Cloak || defaultContainer == DockerContainer::OpenVpn auto container = containers.at(i).toObject();
|| defaultContainer == DockerContainer::ShadowSocks) { if (defaultContainer == DockerContainer::Awg || defaultContainer == DockerContainer::WireGuard) {
return !(protocolConfig.value(config_key::last_config).toString().contains("redirect-gateway")); auto containerConfig = container.value(ContainerProps::containerTypeToString(defaultContainer)).toObject();
return !(containerConfig.value(config_key::last_config).toString().contains("AllowedIPs = 0.0.0.0/0, ::/0"));
} else if (defaultContainer == DockerContainer::Cloak || defaultContainer == DockerContainer::OpenVpn
|| defaultContainer == DockerContainer::ShadowSocks) {
auto containerConfig = container.value(ContainerProps::containerTypeToString(DockerContainer::OpenVpn)).toObject();
return !(containerConfig.value(config_key::last_config).toString().contains("redirect-gateway"));
}
} }
return false; return false;
} }

View file

@ -0,0 +1,80 @@
#include "socks5ProxyConfigModel.h"
#include "protocols/protocols_defs.h"
Socks5ProxyConfigModel::Socks5ProxyConfigModel(QObject *parent) : QAbstractListModel(parent)
{
}
int Socks5ProxyConfigModel::rowCount(const QModelIndex &parent) const
{
Q_UNUSED(parent);
return 1;
}
bool Socks5ProxyConfigModel::setData(const QModelIndex &index, const QVariant &value, int role)
{
if (!index.isValid() || index.row() < 0 || index.row() >= ContainerProps::allContainers().size()) {
return false;
}
switch (role) {
case Roles::PortRole: m_protocolConfig.insert(config_key::port, value.toString()); break;
case Roles::UserNameRole: m_protocolConfig.insert(config_key::userName, value.toString()); break;
case Roles::PasswordRole: m_protocolConfig.insert(config_key::password, value.toString()); break;
}
emit dataChanged(index, index, QList { role });
return true;
}
QVariant Socks5ProxyConfigModel::data(const QModelIndex &index, int role) const
{
if (!index.isValid() || index.row() < 0 || index.row() >= rowCount()) {
return false;
}
switch (role) {
case Roles::PortRole: return m_protocolConfig.value(config_key::port).toString();
case Roles::UserNameRole:
return m_protocolConfig.value(config_key::userName).toString();
case Roles::PasswordRole: return m_protocolConfig.value(config_key::password).toString();
}
return QVariant();
}
void Socks5ProxyConfigModel::updateModel(const QJsonObject &config)
{
beginResetModel();
m_container = ContainerProps::containerFromString(config.value(config_key::container).toString());
m_fullConfig = config;
QJsonObject protocolConfig = config.value(config_key::socks5proxy).toObject();
m_protocolConfig.insert(config_key::userName,
protocolConfig.value(config_key::userName).toString());
m_protocolConfig.insert(config_key::password, protocolConfig.value(config_key::password).toString());
m_protocolConfig.insert(config_key::port, protocolConfig.value(config_key::port).toString());
endResetModel();
}
QJsonObject Socks5ProxyConfigModel::getConfig()
{
m_fullConfig.insert(config_key::socks5proxy, m_protocolConfig);
return m_fullConfig;
}
QHash<int, QByteArray> Socks5ProxyConfigModel::roleNames() const
{
QHash<int, QByteArray> roles;
roles[PortRole] = "port";
roles[UserNameRole] = "username";
roles[PasswordRole] = "password";
return roles;
}

View file

@ -0,0 +1,40 @@
#ifndef SOCKS5PROXYCONFIGMODEL_H
#define SOCKS5PROXYCONFIGMODEL_H
#include <QAbstractListModel>
#include <QJsonObject>
#include "containers/containers_defs.h"
class Socks5ProxyConfigModel : public QAbstractListModel
{
Q_OBJECT
public:
enum Roles {
PortRole = Qt::UserRole + 1,
UserNameRole,
PasswordRole
};
explicit Socks5ProxyConfigModel(QObject *parent = nullptr);
int rowCount(const QModelIndex &parent = QModelIndex()) const override;
bool setData(const QModelIndex &index, const QVariant &value, int role) override;
QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
public slots:
void updateModel(const QJsonObject &config);
QJsonObject getConfig();
protected:
QHash<int, QByteArray> roleNames() const override;
private:
DockerContainer m_container;
QJsonObject m_protocolConfig;
QJsonObject m_fullConfig;
};
#endif // SOCKS5PROXYCONFIGMODEL_H

View file

@ -0,0 +1,385 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import SortFilterProxyModel 0.2
import PageEnum 1.0
import ContainerProps 1.0
import "./"
import "../Controls2"
import "../Controls2/TextTypes"
import "../Config"
import "../Components"
PageType {
id: root
defaultActiveFocusItem: listview
Connections {
target: InstallController
function onUpdateContainerFinished() {
PageController.showNotificationMessage(qsTr("Settings updated successfully"))
}
}
Item {
id: focusItem
KeyNavigation.tab: backButton
}
ColumnLayout {
id: backButtonLayout
anchors.top: parent.top
anchors.left: parent.left
anchors.right: parent.right
anchors.topMargin: 20
BackButtonType {
id: backButton
KeyNavigation.tab: listview
}
}
FlickableType {
id: fl
anchors.top: backButtonLayout.bottom
anchors.bottom: parent.bottom
contentHeight: listview.implicitHeight
ListView {
id: listview
width: parent.width
height: listview.contentItem.height
clip: true
interactive: false
model: Socks5ProxyConfigModel
onFocusChanged: {
if (focus) {
listview.currentItem.focusItemId.forceActiveFocus()
}
}
delegate: Item {
implicitWidth: listview.width
implicitHeight: content.implicitHeight
property alias focusItemId: hostLabel.rightButton
ColumnLayout {
id: content
anchors.top: parent.top
anchors.left: parent.left
anchors.right: parent.right
spacing: 0
HeaderType {
Layout.fillWidth: true
Layout.leftMargin: 16
Layout.rightMargin: 16
headerText: qsTr("SOCKS5 settings")
}
LabelWithButtonType {
id: hostLabel
Layout.fillWidth: true
Layout.topMargin: 32
parentFlickable: fl
KeyNavigation.tab: portLabel.rightButton
text: qsTr("Host")
descriptionText: ServersModel.getProcessedServerData("hostName")
descriptionOnTop: true
rightImageSource: "qrc:/images/controls/copy.svg"
rightImageColor: "#D7D8DB"
clickedFunction: function() {
GC.copyToClipBoard(descriptionText)
PageController.showNotificationMessage(qsTr("Copied"))
if (!GC.isMobile()) {
this.rightButton.forceActiveFocus()
}
}
}
LabelWithButtonType {
id: portLabel
Layout.fillWidth: true
text: qsTr("Port")
descriptionText: port
descriptionOnTop: true
parentFlickable: fl
KeyNavigation.tab: usernameLabel.rightButton
rightImageSource: "qrc:/images/controls/copy.svg"
rightImageColor: "#D7D8DB"
clickedFunction: function() {
GC.copyToClipBoard(descriptionText)
PageController.showNotificationMessage(qsTr("Copied"))
if (!GC.isMobile()) {
this.rightButton.forceActiveFocus()
}
}
}
LabelWithButtonType {
id: usernameLabel
Layout.fillWidth: true
text: qsTr("User name")
descriptionText: username
descriptionOnTop: true
parentFlickable: fl
KeyNavigation.tab: passwordLabel.eyeButton
rightImageSource: "qrc:/images/controls/copy.svg"
rightImageColor: "#D7D8DB"
clickedFunction: function() {
GC.copyToClipBoard(descriptionText)
PageController.showNotificationMessage(qsTr("Copied"))
if (!GC.isMobile()) {
this.rightButton.forceActiveFocus()
}
}
}
LabelWithButtonType {
id: passwordLabel
Layout.fillWidth: true
text: qsTr("Password")
descriptionText: password
descriptionOnTop: true
parentFlickable: fl
eyeButton.KeyNavigation.tab: passwordLabel.rightButton
rightButton.KeyNavigation.tab: changeSettingsButton
rightImageSource: "qrc:/images/controls/copy.svg"
rightImageColor: "#D7D8DB"
buttonImageSource: hideDescription ? "qrc:/images/controls/eye.svg" : "qrc:/images/controls/eye-off.svg"
clickedFunction: function() {
GC.copyToClipBoard(descriptionText)
PageController.showNotificationMessage(qsTr("Copied"))
if (!GC.isMobile()) {
this.rightButton.forceActiveFocus()
}
}
}
DrawerType2 {
id: changeSettingsDrawer
parent: root
anchors.fill: parent
expandedHeight: root.height * 0.9
onClosed: {
if (!GC.isMobile()) {
focusItem.forceActiveFocus()
}
}
expandedContent: ColumnLayout {
property string tempPort: port
property string tempUsername: username
property string tempPassword: password
anchors.top: parent.top
anchors.left: parent.left
anchors.right: parent.right
anchors.topMargin: 32
anchors.leftMargin: 16
anchors.rightMargin: 16
spacing: 0
Connections {
target: changeSettingsDrawer
function onOpened() {
if (!GC.isMobile()) {
drawerFocusItem.forceActiveFocus()
}
tempPort = port
tempUsername = username
tempPassword = password
}
function onClosed() {
port = tempPort
username = tempUsername
password = tempPassword
portTextField.textFieldText = port
usernameTextField.textFieldText = username
passwordTextField.textFieldText = password
}
}
Item {
id: drawerFocusItem
KeyNavigation.tab: portTextField.textField
}
HeaderType {
Layout.fillWidth: true
headerText: qsTr("SOCKS5 settings")
}
TextFieldWithHeaderType {
id: portTextField
Layout.fillWidth: true
Layout.topMargin: 40
parentFlickable: fl
headerText: qsTr("Port")
textFieldText: port
textField.maximumLength: 5
textField.validator: IntValidator { bottom: 1; top: 65535 }
textField.onEditingFinished: {
textFieldText = textField.text.replace(/^\s+|\s+$/g, '')
if (textFieldText !== port) {
port = textFieldText
}
}
KeyNavigation.tab: usernameTextField.textField
}
TextFieldWithHeaderType {
id: usernameTextField
Layout.fillWidth: true
Layout.topMargin: 16
parentFlickable: fl
headerText: qsTr("Username")
textFieldPlaceholderText: "username"
textFieldText: username
textField.maximumLength: 32
textField.onEditingFinished: {
textFieldText = textField.text.replace(/^\s+|\s+$/g, '')
if (textFieldText !== username) {
username = textFieldText
}
}
KeyNavigation.tab: passwordTextField.textField
}
TextFieldWithHeaderType {
id: passwordTextField
property bool hidePassword: true
Layout.fillWidth: true
Layout.topMargin: 16
parentFlickable: fl
headerText: qsTr("Password")
textFieldPlaceholderText: "password"
textFieldText: password
textField.maximumLength: 32
textField.echoMode: hidePassword ? TextInput.Password : TextInput.Normal
buttonImageSource: textFieldText !== "" ? (hidePassword ? "qrc:/images/controls/eye.svg" : "qrc:/images/controls/eye-off.svg")
: ""
clickedFunc: function() {
hidePassword = !hidePassword
}
textField.onFocusChanged: {
textFieldText = textField.text.replace(/^\s+|\s+$/g, '')
if (textFieldText !== password) {
password = textFieldText
}
}
KeyNavigation.tab: saveButton
}
BasicButtonType {
id: saveButton
Layout.fillWidth: true
Layout.topMargin: 24
Layout.bottomMargin: 24
text: qsTr("Change connection settings")
Keys.onTabPressed: lastItemTabClicked(drawerFocusItem)
clickedFunc: function() {
forceActiveFocus()
if (!portTextField.textField.acceptableInput) {
portTextField.errorText = qsTr("The port must be in the range of 1 to 65535")
return
}
if (usernameTextField.textFieldText && passwordTextField.textFieldText === "") {
passwordTextField.errorText = qsTr("Password cannot be empty")
return
} else if (usernameTextField.textFieldText === "" && passwordTextField.textFieldText) {
usernameTextField.errorText = qsTr("Username cannot be empty")
return
}
PageController.goToPage(PageEnum.PageSetupWizardInstalling)
InstallController.updateContainer(Socks5ProxyConfigModel.getConfig())
tempPort = portTextField.textFieldText
tempUsername = usernameTextField.textFieldText
tempPassword = passwordTextField.textFieldText
changeSettingsDrawer.close()
}
}
}
}
BasicButtonType {
id: changeSettingsButton
Layout.fillWidth: true
Layout.topMargin: 24
Layout.bottomMargin: 24
Layout.leftMargin: 16
Layout.rightMargin: 16
text: qsTr("Change connection settings")
Keys.onTabPressed: lastItemTabClicked(focusItem)
clickedFunc: function() {
forceActiveFocus()
changeSettingsDrawer.open()
}
}
}
}
}
}
}

View file

@ -18,6 +18,8 @@ import "../Components"
PageType { PageType {
id: root id: root
property bool isClearCacheVisible: ServersModel.isProcessedServerHasWriteAccess() && !ContainersModel.isServiceContainer(ContainersModel.getProcessedContainerIndex())
defaultActiveFocusItem: focusItem defaultActiveFocusItem: focusItem
Item { Item {
@ -103,6 +105,7 @@ PageType {
case ProtocolEnum.Awg: AwgConfigModel.updateModel(ProtocolsModel.getConfig()); break; case ProtocolEnum.Awg: AwgConfigModel.updateModel(ProtocolsModel.getConfig()); break;
case ProtocolEnum.Xray: XrayConfigModel.updateModel(ProtocolsModel.getConfig()); break; case ProtocolEnum.Xray: XrayConfigModel.updateModel(ProtocolsModel.getConfig()); break;
case ProtocolEnum.Ipsec: Ikev2ConfigModel.updateModel(ProtocolsModel.getConfig()); break; case ProtocolEnum.Ipsec: Ikev2ConfigModel.updateModel(ProtocolsModel.getConfig()); break;
case ProtocolEnum.Socks5Proxy: Socks5ProxyConfigModel.updateModel(ProtocolsModel.getConfig()); break;
} }
PageController.goToPage(protocolPage); PageController.goToPage(protocolPage);
} }
@ -124,7 +127,7 @@ PageType {
Layout.fillWidth: true Layout.fillWidth: true
visible: ServersModel.isProcessedServerHasWriteAccess() visible: root.isClearCacheVisible
KeyNavigation.tab: removeButton KeyNavigation.tab: removeButton
text: qsTr("Clear %1 profile").arg(ContainersModel.getProcessedContainerName()) text: qsTr("Clear %1 profile").arg(ContainersModel.getProcessedContainerName())
@ -167,7 +170,7 @@ PageType {
Layout.leftMargin: 16 Layout.leftMargin: 16
Layout.rightMargin: 16 Layout.rightMargin: 16
visible: ServersModel.isProcessedServerHasWriteAccess() visible: root.isClearCacheVisible
} }
LabelWithButtonType { LabelWithButtonType {

View file

@ -261,6 +261,11 @@ PageType {
Keys.onTabPressed: lastItemTabClicked(focusItem) Keys.onTabPressed: lastItemTabClicked(focusItem)
clickedFunc: function() { clickedFunc: function() {
if (!port.textField.acceptableInput) {
port.errorText = qsTr("The port must be in the range of 1 to 65535")
return
}
PageController.goToPage(PageEnum.PageSetupWizardInstalling); PageController.goToPage(PageEnum.PageSetupWizardInstalling);
InstallController.install(dockerContainer, port.textFieldText, transportProtoSelector.currentIndex) InstallController.install(dockerContainer, port.textFieldText, transportProtoSelector.currentIndex)
} }